From 3789703c571a18165dd673acf0e45120df1bda9f Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 14:10:08 -0300 Subject: [PATCH 01/96] docs(odd): add repo-agentic-setup feature task doc --- odd/tasks/repo-agentic-setup.md | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 odd/tasks/repo-agentic-setup.md diff --git a/odd/tasks/repo-agentic-setup.md b/odd/tasks/repo-agentic-setup.md new file mode 100644 index 0000000000..0ec9a4d61c --- /dev/null +++ b/odd/tasks/repo-agentic-setup.md @@ -0,0 +1,51 @@ +# Feature: repo-agentic-setup — agentic development setup for evolution-in-purple + +## Objective + +Organize `purple-dice/evolution-in-purple` (fork of Evolution API 2.3.7) for +agentic development following the orbit conventions, adapted to a single +TypeScript repo. This engine will replace Evolution API in three consumers: +orbit, canal-whatsapp (alien) and stenfar. + +## Scope + +- Canonical `AGENTS.md` (index + hard rules), `CLAUDE.md` as `@AGENTS.md` pointer. +- `odd/agent-protocol.md`, `odd/tasks/`, `odd/archive/`. +- Project skills in `.claude/skills/purple-*`, shared with Pi via `.pi/settings.json`. +- Automated gates: prettier hook, git-gate (relative paths), check-docs, + integrated with the existing Husky hooks (not a second hook system). +- CI: PR checks (tsc, build, lint, check-docs); image publish adapted to the fork. +- Own `VERSION` + `CHANGELOG.md` section, separated from upstream history. + +## Non-goals + +- No source (`src/`) changes. No upstream backports (separate feature). +- No OpenSpec, no gga (can be added later pointing at `AGENTS.md`). +- No removal of telemetry/license yet (separate feature). + +## Parallel plan (isolated worktrees, disjoint surfaces, parent commits) + +| Task | Surface | Worktree branch | +|---|---|---| +| T1 Core docs | `AGENTS.md`, `CLAUDE.md`, `odd/agent-protocol.md`, `odd/archive/.gitkeep` | `agentic/t1-docs` | +| T2 Skills | `.claude/skills/purple-*/**`, `.pi/settings.json`, `.pi/subagents.json` | `agentic/t2-skills` | +| T3 Tooling/CI | `.claude/settings.json`, `.claude/hooks/**`, `.husky/**`, `Makefile`, `scripts/check-docs.sh`, `.github/workflows/**`, `VERSION`, `CHANGELOG.md` | `agentic/t3-tooling` | +| T4 Integrate | merge T1–T3 into `chore/agentic-setup`, cross-check links, run checks | `chore/agentic-setup` | + +## Checklist + +- [ ] T1 Core docs +- [ ] T2 Skills +- [ ] T3 Tooling/CI +- [ ] T4 Integrate and verify + +## Acceptance criteria + +- `AGENTS.md` is the single source; `CLAUDE.md` only points to it. +- Every skill has valid frontmatter with a `Trigger:` line and is listed in `AGENTS.md`. +- `make check-docs` passes; `npm run build` and `npm run lint:check` unaffected. +- Hooks use repo-relative paths only. + +## Evidence + +(commit ids recorded per task) From 901c00df40b1620977974eadea3e5e52972b79ba Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 14:13:22 -0300 Subject: [PATCH 02/96] docs(agents): rewrite AGENTS.md as canonical index and add agent protocol CLAUDE.md now points to AGENTS.md. Adds odd/agent-protocol.md adapted from orbit and odd/archive/. --- AGENTS.md | 473 ++++++++++++++---------------------------- CLAUDE.md | 224 +------------------- odd/agent-protocol.md | 75 +++++++ odd/archive/.gitkeep | 0 4 files changed, 238 insertions(+), 534 deletions(-) create mode 100644 odd/agent-protocol.md create mode 100644 odd/archive/.gitkeep diff --git a/AGENTS.md b/AGENTS.md index 143e6c748e..ae16157031 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,355 +1,204 @@ -# Evolution API - AI Agent Guidelines - -This document provides comprehensive guidelines for AI agents (Claude, GPT, Cursor, etc.) working with the Evolution API codebase. - -## Project Overview - -**Evolution API** is a production-ready, multi-tenant WhatsApp API platform built with Node.js, TypeScript, and Express.js. It supports multiple WhatsApp providers and extensive integrations with chatbots, CRM systems, and messaging platforms. - -## Project Structure & Module Organization - -### Core Directories -- **`src/`** – TypeScript source code with modular architecture - - `api/controllers/` – HTTP route handlers (thin layer) - - `api/services/` – Business logic (core functionality) - - `api/routes/` – Express route definitions (RouterBroker pattern) - - `api/integrations/` – External service integrations - - `channel/` – WhatsApp providers (Baileys, Business API, Evolution) - - `chatbot/` – AI/Bot integrations (OpenAI, Dify, Typebot, Chatwoot) - - `event/` – Event systems (WebSocket, RabbitMQ, SQS, NATS, Pusher) - - `storage/` – File storage (S3, MinIO) - - `dto/` – Data Transfer Objects (simple classes, no decorators) - - `guards/` – Authentication/authorization middleware - - `types/` – TypeScript type definitions - - `repository/` – Data access layer (Prisma) -- **`prisma/`** – Database schemas and migrations - - `postgresql-schema.prisma` / `mysql-schema.prisma` – Provider-specific schemas - - `postgresql-migrations/` / `mysql-migrations/` – Provider-specific migrations -- **`config/`** – Environment and application configuration -- **`utils/`** – Shared utilities and helper functions -- **`validate/`** – JSONSchema7 validation schemas -- **`exceptions/`** – Custom HTTP exception classes -- **`cache/`** – Redis and local cache implementations - -### Build & Deployment -- **`dist/`** – Build output (do not edit directly) -- **`public/`** – Static assets and media files -- **`Docker*`**, **`docker-compose*.yaml`** – Containerization and local development stack - -## Build, Test, and Development Commands - -### Development Workflow -```bash -# Development server with hot reload -npm run dev:server +# AGENTS.md + +Canonical index for `purple-dice/evolution-in-purple`, a fork of Evolution API 2.3.7 (TypeScript, Express, +Prisma, Baileys 7.0.0-rc.9). Read by Pi (gentle-ai) and Claude Code; `CLAUDE.md` only points here. Detailed +patterns live in the skills under `.claude/skills/`, loaded on demand. + +This engine replaces Evolution API for three consumer products: **orbit**, **canal-whatsapp** (a.k.a. alien) +and **stenfar**. Anything they consume is a public contract. + +## Repo map -# Direct execution for testing -npm start +| Path | Content | +| --- | --- | +| `src/main.ts` | Bootstrap (Express server, routers, monitors) | +| `src/api/controllers/` | HTTP handlers (thin layer) | +| `src/api/services/` | Business logic; `monitor.service.ts` owns the WhatsApp instance registry | +| `src/api/routes/` | Express routes (`RouterBroker` pattern) | +| `src/api/guards/` | API-key auth and instance resolution (security-critical) | +| `src/api/dto/`, `src/validate/` | Plain-class DTOs and JSONSchema7 validation | +| `src/api/integrations/channel/` | WhatsApp providers: Baileys (`whatsapp/`), Meta Business API (`meta/`), `evolution/` | +| `src/api/integrations/chatbot/` | Chatwoot, Typebot, OpenAI, Dify, Flowise, N8N, EvoAI, EvolutionBot | +| `src/api/integrations/event/` | Webhook, WebSocket, RabbitMQ, SQS, NATS, Kafka, Pusher | +| `src/api/integrations/storage/` | S3 / MinIO | +| `src/config/`, `src/cache/`, `src/utils/`, `src/exceptions/` | Env config, Redis/local cache, helpers, HTTP exceptions | +| `prisma/` | `postgresql-schema.prisma` / `mysql-schema.prisma` + provider-specific migration folders | +| `odd/` | Active feature docs (`odd/tasks/`), closed ones (`odd/archive/`), `odd/agent-protocol.md` | +| `.claude/skills/purple-*` | Project skills (shared with Pi via `.pi/settings.json`) | +| `VERSION`, `CHANGELOG.md` | Fork versioning, managed by the `purple-release` skill | +| `dist/` | Build output — never edit | -# Production build and run -npm run build -npm run start:prod -``` +## Commands & danger zones -### Code Quality -```bash -# Linting and formatting -npm run lint # ESLint with auto-fix -npm run lint:check # ESLint check only +- **Local / safe:** `npm run dev:server` (tsx watch), `npm start`, `npm run build` (`tsc --noEmit && tsup`), + `npm run lint:check`, `npx tsc --noEmit`, `make check-docs`, `npm run db:generate`, + `docker-compose -f docker-compose.dev.yaml up -d`. +- **Database:** always `export DATABASE_PROVIDER=postgresql` (or `mysql`) first. `npm run db:migrate:dev` + only against a local database. Every schema change ships migrations for BOTH providers. +- **PROD — never without explicit user approval:** `npm run db:deploy`, image publish workflows, any deploy, + release tag, or push to a shared environment. +- `npm run lint` auto-fixes files; prefer `lint:check` when verifying. -# Commit with conventional commits -npm run commit # Interactive commit with Commitizen -``` +## Commits -### Database Management -```bash -# Set database provider first (CRITICAL) -export DATABASE_PROVIDER=postgresql # or mysql +- Conventional Commits (enforced by commitlint), English, subject <= 100 chars. +- Tests and docs travel in the same commit as the behavior they cover. +- Husky runs `lint-staged` on pre-commit and `build` + `lint:check` on pre-push. Never `--no-verify`. +- No `Co-Authored-By` or AI attribution. +- The parent agent commits; subagents never commit (`odd/agent-protocol.md` §2). +- Push, PR and merge are user decisions. Releases only via the `purple-release` skill. -# Generate Prisma client -npm run db:generate +## Hard rules -# Development migrations (with provider sync) -npm run db:migrate:dev # Unix/Mac -npm run db:migrate:dev:win # Windows +### (a) Consumer contract -# Production deployment -npm run db:deploy # Unix/Mac -npm run db:deploy:win # Windows +Webhook payloads (event names, field names, shapes) and REST routes (paths, params, response shapes) +consumed by orbit, canal-whatsapp or stenfar MUST NOT break without versioning and a `CHANGELOG.md` note. +Adding optional fields is fine; renaming, removing, retyping or changing semantics is a breaking change. +Procedure: skill `purple-contract`. -# Database tools -npm run db:studio # Open Prisma Studio -``` +### (b) Upstream sync -### Docker Development -```bash -# Start local services (Redis, PostgreSQL, etc.) -docker-compose up -d +- We cherry-pick from `evolution-foundation/evolution-api` (git remote `upstream`); we never merge it wholesale. +- NEVER import license activation, telemetry or EvoHub code. +- Record the source PR number and/or upstream sha in the commit body (e.g. `Upstream: #1234 / abc1234`). +- Procedure: skill `purple-upstream-sync`. -# Full development stack -docker-compose -f docker-compose.dev.yaml up -d -``` +### (c) Multi-instance isolation -## Coding Standards & Architecture Patterns +Several clients share one server. Every operation is scoped by `instanceName` / `instanceId`; every Prisma +query on tenant data includes `where: { instanceId }`. A request authenticated for one instance must never +read or mutate another. Changes to auth guards, API-key handling or instance resolution are +**security-critical**: call them out in the PR and get explicit review. -### Code Style (Enforced by ESLint + Prettier) -- **TypeScript strict mode** with full type coverage -- **2-space indentation**, single quotes, trailing commas -- **120-character line limit** -- **Import order** via `simple-import-sort` -- **File naming**: `feature.kind.ts` (e.g., `whatsapp.baileys.service.ts`) -- **Naming conventions**: - - Classes: `PascalCase` - - Functions/variables: `camelCase` - - Constants: `UPPER_SNAKE_CASE` - - Files: `kebab-case.type.ts` +### (d) WhatsApp JIDs -### Architecture Patterns +Preserve `@lid` addressing end to end. Never strip a JID with `split('@')[0]` (or equivalent) to build an +outbound target — that turns a `@lid` into a bogus phone number and the message goes nowhere or to the wrong +user. Normalize only through the existing JID helpers. Detail: skill `purple-baileys`. -#### Service Layer Pattern -```typescript -export class ExampleService { - constructor(private readonly waMonitor: WAMonitoringService) {} - - private readonly logger = new Logger('ExampleService'); - - public async create(instance: InstanceDto, data: ExampleDto) { - // Business logic here - return { example: { ...instance, data } }; - } - - public async find(instance: InstanceDto): Promise { - try { - const result = await this.waMonitor.waInstances[instance.instanceName].findData(); - return result || null; // Return null on not found (Evolution pattern) - } catch (error) { - this.logger.error('Error finding data:', error); - return null; // Return null on error (Evolution pattern) - } - } -} -``` +## Skills per task -#### Controller Pattern (Thin Layer) -```typescript -export class ExampleController { - constructor(private readonly exampleService: ExampleService) {} - - public async createExample(instance: InstanceDto, data: ExampleDto) { - return this.exampleService.create(instance, data); - } -} -``` +Read the matching skill BEFORE writing code. Skills live in `.claude/skills//SKILL.md`. -#### RouterBroker Pattern -```typescript -export class ExampleRouter extends RouterBroker { - constructor(...guards: any[]) { - super(); - this.router.post(this.routerPath('create'), ...guards, async (req, res) => { - const response = await this.dataValidate({ - request: req, - schema: exampleSchema, // JSONSchema7 - ClassRef: ExampleDto, - execute: (instance, data) => controller.createExample(instance, data), - }); - res.status(201).json(response); - }); - } -} -``` +| Task | Skills | +| --- | --- | +| Any TypeScript change in `src/` | `purple-typescript` | +| Baileys channel, JIDs, connection/session, message send/receive | `purple-baileys` + `purple-typescript` | +| Webhook payload, event, REST route or DTO consumed externally | `purple-contract` + `purple-typescript` | +| Cherry-picking / porting from upstream | `purple-upstream-sync` | +| Version bump, `CHANGELOG.md`, image publish | `purple-release` | -#### DTO Pattern (Simple Classes) -```typescript -// CORRECT - Evolution API pattern (no decorators) -export class ExampleDto { - name: string; - description?: string; - enabled: boolean; -} +## How this repo fits ODD -// INCORRECT - Don't use class-validator decorators -export class BadExampleDto { - @IsString() // ❌ Evolution API doesn't use decorators - name: string; -} -``` +ODD (gentle-ai) is the workflow; its protocol is not repeated here. What this repo adds: -#### Validation Pattern (JSONSchema7) -```typescript -import { JSONSchema7 } from 'json-schema'; -import { v4 } from 'uuid'; - -export const exampleSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - name: { type: 'string' }, - description: { type: 'string' }, - enabled: { type: 'boolean' }, - }, - required: ['name', 'enabled'], -}; -``` - -## Multi-Tenant Architecture +- **Feature docs:** each feature lives in `odd/tasks/.md` (objective, scope, checklist, acceptance + criteria, evidence with commit ids). Closed features move to `odd/archive/`. +- **Operational rules** for agents and parents: `odd/agent-protocol.md`. +- **Contract first:** if a change touches anything a consumer reads, the first task states the contract + change (skill `purple-contract`). -### Instance Isolation -- **CRITICAL**: All operations must be scoped by `instanceName` or `instanceId` -- **Database queries**: Always include `where: { instanceId: ... }` -- **Authentication**: Validate instance ownership before operations -- **Data isolation**: Complete separation between tenant instances +## CodeGraph — ordering rule -### WhatsApp Instance Management -```typescript -// Access instance via WAMonitoringService -const waInstance = this.waMonitor.waInstances[instance.instanceName]; -if (!waInstance) { - throw new NotFoundException(`Instance ${instance.instanceName} not found`); -} -``` +- Structural questions (map, call flow, references, impact) are answered with CodeGraph **before** any + `grep`/`find`. Read-only commands only: `status`, `query`, `explore`, `context`, `node`, `files`, + `callers`, `callees`, `impact`, `affected`. +- Each worktree has its own `.codegraph/` index. `index` only to recover a corrupt index. +- Fall back to `grep`/`find`/`read` only if CodeGraph fails, and say so explicitly. -## Database Patterns +## Verification -### Multi-Provider Support -- **PostgreSQL**: Uses `@db.Integer`, `@db.JsonB`, `@default(now())` -- **MySQL**: Uses `@db.Int`, `@db.Json`, `@default(now())` -- **Environment**: Set `DATABASE_PROVIDER=postgresql` or `mysql` -- **Migrations**: Provider-specific folders auto-selected +There is no test suite yet (`npm test` is an upstream placeholder). Before handing off a change, run: -### Prisma Repository Pattern -```typescript -// Always use PrismaRepository for database operations -const result = await this.prismaRepository.instance.findUnique({ - where: { name: instanceName }, -}); +```bash +npx tsc --noEmit +npm run build +npm run lint:check +make check-docs # docs drift guard (pointers, skill frontmatter) ``` -## Integration Patterns +Behavior that needs a real WhatsApp account (pairing, sending, receiving) requires a human with a phone; +report it as unverified rather than faking it. When tests are added, they ship with the behavior. -### Channel Integration (WhatsApp Providers) -- **Baileys**: WhatsApp Web with QR code authentication -- **Business API**: Official Meta WhatsApp Business API -- **Evolution API**: Custom WhatsApp integration -- **Pattern**: Extend base channel service classes +## Documentation conventions -### Chatbot Integration -- **Base classes**: Extend `BaseChatbotService` and `BaseChatbotController` -- **Trigger system**: Support keyword, regex, and advanced triggers -- **Session management**: Handle conversation state per user -- **Available integrations**: EvolutionBot, OpenAI, Dify, Typebot, Chatwoot, Flowise, N8N, EvoAI +1. This file is an index: hard rules + pointers. Implementation detail goes to skills. +2. Whoever changes behavior updates the affected skill or this index in the same commit. +3. Changes to the hard rules are decided by the user. +4. Run `make check-docs` before committing doc changes. -### Event Integration -- **Internal events**: EventEmitter2 for application events -- **External events**: WebSocket, RabbitMQ, SQS, NATS, Pusher -- **Webhook delivery**: Reliable delivery with retry logic +## Codebase reference -## Testing Guidelines +Condensed from the upstream Evolution API guidelines. -### Current State -- **No formal test suite** currently implemented -- **Manual testing** is the primary approach -- **Integration testing** in development environment +### Architecture -### Testing Strategy -```typescript -// Place tests in test/ directory as *.test.ts -// Run: npm test (watches test/all.test.ts) - -describe('ExampleService', () => { - it('should create example', async () => { - // Mock external dependencies - // Test business logic - // Assert expected behavior - }); -}); -``` - -### Recommended Approach -- Focus on **critical business logic** in services -- **Mock external dependencies** (WhatsApp APIs, databases) -- **Integration tests** for API endpoints -- **Manual testing** for WhatsApp connection flows +- **Multi-provider DB:** PostgreSQL (`@db.Integer`, `@db.JsonB`) and MySQL (`@db.Int`, `@db.Json`) via Prisma, + provider chosen by `DATABASE_PROVIDER`; `runWithProvider.js` substitutes it into schema/migration paths. +- **Events:** EventEmitter2 internally; external delivery via webhook (with retry), WebSocket, RabbitMQ, SQS, + NATS, Kafka, Pusher. All go through `event.manager.ts`. +- **Cache:** Redis primary, node-cache fallback; set explicit TTLs and invalidate on writes. +- **Instances:** `WAMonitoringService.waInstances[instanceName]` holds live channel services; one WhatsApp + connection per instance with lifecycle management. +- **Media:** stored in S3/MinIO when enabled; otherwise inline base64. -## Commit & Pull Request Guidelines +### Patterns -### Conventional Commits (Enforced by commitlint) -```bash -# Use interactive commit tool -npm run commit +```typescript +// Service: business logic, Evolution returns null on not-found +export class ExampleService { + constructor(private readonly waMonitor: WAMonitoringService) {} + private readonly logger = new Logger('ExampleService'); -# Commit format: type(scope): subject (max 100 chars) -# Types: feat, fix, docs, style, refactor, perf, test, chore, ci, build, revert, security -``` + public async find(instance: InstanceDto): Promise { + try { + return (await this.waMonitor.waInstances[instance.instanceName].findData()) || null; + } catch (error) { + this.logger.error('Error finding data:', error); + return null; + } + } +} -### Examples -- `feat(api): add WhatsApp message status endpoint` -- `fix(baileys): resolve connection timeout issue` -- `docs(readme): update installation instructions` -- `refactor(service): extract common message validation logic` +// Controller: thin pass-through +export class ExampleController { + constructor(private readonly exampleService: ExampleService) {} + public async find(instance: InstanceDto) { + return this.exampleService.find(instance); + } +} -### Pull Request Requirements -- **Clear description** of changes and motivation -- **Linked issues** if applicable -- **Migration impact** (specify database provider) -- **Local testing steps** with screenshots/logs -- **Breaking changes** clearly documented +// Router: RouterBroker + JSONSchema7 validation +this.router.post(this.routerPath('create'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: exampleSchema, + ClassRef: ExampleDto, + execute: (instance, data) => controller.create(instance, data), + }); + res.status(201).json(response); +}); +``` -## Security & Configuration +- **DTOs** are plain classes — no `class-validator` decorators. Validation is JSONSchema7 in `src/validate/` + (`$id: v4()`, explicit `required`). +- **Instance lookup:** throw `NotFoundException` when `waMonitor.waInstances[name]` is missing. +- **Chatbots** extend `BaseChatbotService` / `BaseChatbotController` (keyword, regex, advanced triggers; + per-contact sessions). +- **Channels** extend the base channel service; keep provider-specific logic inside its folder. -### Environment Setup -```bash -# Copy example environment file -cp .env.example .env +### Code style -# NEVER commit secrets to version control -# Set DATABASE_PROVIDER before database commands -export DATABASE_PROVIDER=postgresql # or mysql -``` +- TypeScript strict, 2-space indent, single quotes, trailing commas, 120-char lines, `simple-import-sort`. +- Files `feature.kind.ts` (e.g. `whatsapp.baileys.service.ts`); classes `PascalCase`, functions/variables + `camelCase`, constants `UPPER_SNAKE_CASE`. +- Logging via the project `Logger` (Pino); never log API keys or tokens. +- Code, comments, API responses and docs in English. -### Security Best Practices -- **API key authentication** via `apikey` header -- **Input validation** with JSONSchema7 -- **Rate limiting** on all endpoints -- **Webhook signature validation** -- **Instance-based access control** -- **Secure defaults** for all configurations - -### Vulnerability Reporting -- See `SECURITY.md` for security vulnerability reporting process -- Contact: `contato@evolution-api.com` - -## Communication Standards - -### Language Requirements -- **User communication**: Always respond in Portuguese (PT-BR) -- **Code/comments**: English for technical documentation -- **API responses**: English for consistency -- **Error messages**: Portuguese for user-facing errors - -### Documentation Standards -- **Inline comments**: Document complex business logic -- **API documentation**: Document all public endpoints -- **Integration guides**: Document new integration patterns -- **Migration guides**: Document database schema changes - -## Performance & Scalability - -### Caching Strategy -- **Redis primary**: Distributed caching for production -- **Node-cache fallback**: Local caching when Redis unavailable -- **TTL strategy**: Appropriate cache expiration per data type -- **Cache invalidation**: Proper invalidation on data changes - -### Connection Management -- **Database**: Prisma connection pooling -- **WhatsApp**: One connection per instance with lifecycle management -- **Redis**: Connection pooling and retry logic -- **External APIs**: Rate limiting and retry with exponential backoff - -### Monitoring & Observability -- **Structured logging**: Pino logger with correlation IDs -- **Error tracking**: Comprehensive error scenarios -- **Health checks**: Instance status and connection monitoring -- **Telemetry**: Usage analytics (non-sensitive data only) +### Environment +- Copy `.env.example` to `.env` locally; never commit secrets. +- Key settings: `DATABASE_PROVIDER`, `DATABASE_CONNECTION_URI`, `AUTHENTICATION_API_KEY`, `CACHE_REDIS_*`, + `WEBHOOK_*`, `S3_*`. See `src/config/env.config.ts` for the full list. +- Security reports: `SECURITY.md`. diff --git a/CLAUDE.md b/CLAUDE.md index 7c0e87a040..6eaecd7212 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,223 +1,3 @@ -# CLAUDE.md +@AGENTS.md -This file provides comprehensive guidance to Claude AI when working with the Evolution API codebase. - -## Project Overview - -**Evolution API** is a powerful, production-ready REST API for WhatsApp communication that supports multiple WhatsApp providers: -- **Baileys** (WhatsApp Web) - Open-source WhatsApp Web client -- **Meta Business API** - Official WhatsApp Business API -- **Evolution API** - Custom WhatsApp integration - -Built with **Node.js 20+**, **TypeScript 5+**, and **Express.js**, it provides extensive integrations with chatbots, CRM systems, and messaging platforms in a **multi-tenant architecture**. - -## Common Development Commands - -### Build and Run -```bash -# Development -npm run dev:server # Run in development with hot reload (tsx watch) - -# Production -npm run build # TypeScript check + tsup build -npm run start:prod # Run production build - -# Direct execution -npm start # Run with tsx -``` - -### Code Quality -```bash -npm run lint # ESLint with auto-fix -npm run lint:check # ESLint check only -npm run commit # Interactive commit with commitizen -``` - -### Database Management -```bash -# Set database provider first -export DATABASE_PROVIDER=postgresql # or mysql - -# Generate Prisma client (automatically uses DATABASE_PROVIDER env) -npm run db:generate - -# Deploy migrations (production) -npm run db:deploy # Unix/Mac -npm run db:deploy:win # Windows - -# Development migrations (with sync to provider folder) -npm run db:migrate:dev # Unix/Mac -npm run db:migrate:dev:win # Windows - -# Open Prisma Studio -npm run db:studio - -# Development migrations -npm run db:migrate:dev # Unix/Mac -npm run db:migrate:dev:win # Windows -``` - -### Testing -```bash -npm test # Run tests with watch mode -``` - -## Architecture Overview - -### Core Structure -- **Multi-tenant SaaS**: Complete instance isolation with per-tenant authentication -- **Multi-provider database**: PostgreSQL and MySQL via Prisma ORM with provider-specific schemas and migrations -- **WhatsApp integrations**: Baileys, Meta Business API, and Evolution API with unified interface -- **Event-driven architecture**: EventEmitter2 for internal events + WebSocket, RabbitMQ, SQS, NATS, Pusher for external events -- **Microservices pattern**: Modular integrations for chatbots, storage, and external services - -### Directory Layout -``` -src/ -├── api/ -│ ├── controllers/ # HTTP route handlers (thin layer) -│ ├── services/ # Business logic (core functionality) -│ ├── repository/ # Data access layer (Prisma) -│ ├── dto/ # Data Transfer Objects (simple classes) -│ ├── guards/ # Authentication/authorization middleware -│ ├── integrations/ # External service integrations -│ │ ├── channel/ # WhatsApp providers (Baileys, Business API, Evolution) -│ │ ├── chatbot/ # AI/Bot integrations (OpenAI, Dify, Typebot, Chatwoot) -│ │ ├── event/ # Event systems (WebSocket, RabbitMQ, SQS, NATS, Pusher) -│ │ └── storage/ # File storage (S3, MinIO) -│ ├── routes/ # Express route definitions (RouterBroker pattern) -│ └── types/ # TypeScript type definitions -├── config/ # Environment and app configuration -├── cache/ # Redis and local cache implementations -├── exceptions/ # Custom HTTP exception classes -├── utils/ # Shared utilities and helpers -└── validate/ # JSONSchema7 validation schemas -``` - -### Key Integration Points - -**Channel Integrations** (`src/api/integrations/channel/`): -- **Baileys**: WhatsApp Web client with QR code authentication -- **Business API**: Official Meta WhatsApp Business API -- **Evolution API**: Custom WhatsApp integration -- Connection lifecycle management per instance with automatic reconnection - -**Chatbot Integrations** (`src/api/integrations/chatbot/`): -- **EvolutionBot**: Native chatbot with trigger system -- **Chatwoot**: Customer service platform integration -- **Typebot**: Visual chatbot flow builder -- **OpenAI**: AI capabilities including GPT and Whisper (audio transcription) -- **Dify**: AI agent workflow platform -- **Flowise**: LangChain visual builder -- **N8N**: Workflow automation platform -- **EvoAI**: Custom AI integration - -**Event Integrations** (`src/api/integrations/event/`): -- **WebSocket**: Real-time Socket.io connections -- **RabbitMQ**: Message queue for async processing -- **Amazon SQS**: Cloud-based message queuing -- **NATS**: High-performance messaging system -- **Pusher**: Real-time push notifications - -**Storage Integrations** (`src/api/integrations/storage/`): -- **AWS S3**: Cloud object storage -- **MinIO**: Self-hosted S3-compatible storage -- Media file management and URL generation - -### Database Schema Management -- Separate schema files: `postgresql-schema.prisma` and `mysql-schema.prisma` -- Environment variable `DATABASE_PROVIDER` determines active database -- Migration folders are provider-specific and auto-selected during deployment - -### Authentication & Security -- **API key-based authentication** via `apikey` header (global or per-instance) -- **Instance-specific tokens** for WhatsApp connection authentication -- **Guards system** for route protection and authorization -- **Input validation** using JSONSchema7 with RouterBroker `dataValidate` -- **Rate limiting** and security middleware -- **Webhook signature validation** for external integrations - -## Important Implementation Details - -### WhatsApp Instance Management -- Each WhatsApp connection is an "instance" with unique name -- Instance data stored in database with connection state -- Session persistence in database or file system (configurable) -- Automatic reconnection handling with exponential backoff - -### Message Queue Architecture -- Supports RabbitMQ, Amazon SQS, and WebSocket for events -- Event types: message.received, message.sent, connection.update, etc. -- Configurable per instance which events to send - -### Media Handling -- Local storage or S3/Minio for media files -- Automatic media download from WhatsApp -- Media URL generation for external access -- Support for audio transcription via OpenAI - -### Multi-tenancy Support -- Instance isolation at database level -- Separate webhook configurations per instance -- Independent integration settings per instance - -## Environment Configuration - -Key environment variables are defined in `.env.example`. The system uses a strongly-typed configuration system via `src/config/env.config.ts`. - -Critical configurations: -- `DATABASE_PROVIDER`: postgresql or mysql -- `DATABASE_CONNECTION_URI`: Database connection string -- `AUTHENTICATION_API_KEY`: Global API authentication -- `REDIS_ENABLED`: Enable Redis cache -- `RABBITMQ_ENABLED`/`SQS_ENABLED`: Message queue options - -## Development Guidelines - -The project follows comprehensive development standards defined in `.cursor/rules/`: - -### Core Principles -- **Always respond in Portuguese (PT-BR)** for user communication -- **Follow established architecture patterns** (Service Layer, RouterBroker, etc.) -- **Robust error handling** with retry logic and graceful degradation -- **Multi-database compatibility** (PostgreSQL and MySQL) -- **Security-first approach** with input validation and rate limiting -- **Performance optimizations** with Redis caching and connection pooling - -### Code Standards -- **TypeScript strict mode** with full type coverage -- **JSONSchema7** for input validation (not class-validator) -- **Conventional Commits** enforced by commitlint -- **ESLint + Prettier** for code formatting -- **Service Object pattern** for business logic -- **RouterBroker pattern** for route handling with `dataValidate` - -### Architecture Patterns -- **Multi-tenant isolation** at database and instance level -- **Event-driven communication** with EventEmitter2 -- **Microservices integration** pattern for external services -- **Connection pooling** and lifecycle management -- **Caching strategy** with Redis primary and Node-cache fallback - -## Testing Approach - -Currently, the project has minimal formal testing infrastructure: -- **Manual testing** is the primary approach -- **Integration testing** in development environment -- **No unit test suite** currently implemented -- Test files can be placed in `test/` directory as `*.test.ts` -- Run `npm test` for watch mode development testing - -### Recommended Testing Strategy -- Focus on **critical business logic** in services -- **Mock external dependencies** (WhatsApp APIs, databases) -- **Integration tests** for API endpoints -- **Manual testing** for WhatsApp connection flows - -## Deployment Considerations - -- Docker support with `Dockerfile` and `docker-compose.yaml` -- Graceful shutdown handling for connections -- Health check endpoints for monitoring -- Sentry integration for error tracking -- Telemetry for usage analytics (non-sensitive data only) \ No newline at end of file +All agent guidance for this repo lives in `AGENTS.md`; do not add content here. diff --git a/odd/agent-protocol.md b/odd/agent-protocol.md new file mode 100644 index 0000000000..1ce1b08eff --- /dev/null +++ b/odd/agent-protocol.md @@ -0,0 +1,75 @@ +# Agent protocol — preventing known failure loops + +Operational rules for agents (and their parents) working in this repo. Adapted from the orbit protocol, where +each rule exists because the loop it prevents actually happened. Imperative, no exceptions without an explicit +note in the feature's tracking doc. + +## 1. Environment first + +Verify against an isolated local stack (`docker-compose -f docker-compose.dev.yaml up -d`, local database, +`DATABASE_PROVIDER` set). If the environment is broken (database down, Prisma client stale, Redis +unreachable), STOP and report `environment: ` — never debug the feature against a broken stack. +Never verify against shared or production instances, and never connect a test to a consumer's real +WhatsApp number. + +**On-device WhatsApp validation** (QR pairing, real send/receive, `@lid` delivery, media) needs a human with +a phone. Agents prepare the steps and expected results; the human runs them. Report it as unverified until +the human confirms. + +## 2. Commits (subagents do NOT commit) + +**The parent owns commits; workers do not.** A worker implements, runs only the focused checks, and hands +back the diff plus evidence. It must not stage, commit or push. + +**Every ODD task closes with at least one Conventional work-unit commit**, and its commit id is recorded in +the feature doc. Slice work into commits of ~400 authored changed lines. Husky runs `lint-staged` on commit +and `build` + `lint:check` on push; never bypass it. + +**One branch / PR per feature phase.** Native review takes the whole branch diff against its base; an +accumulated branch exceeds the reviewer's context budget. Keep each phase under ~400 authored changed lines +and review per branch before starting the next phase. + +Safety note: uncommitted work lives in the worktree and survives a killed session. The parent commits +promptly after each worker returns and never runs `git checkout`/`git clean` while a worker is mid-flight. + +## 3. Verification budget + +At most TWO verification attempts per task, always on isolated state. Then commit what is green and report +exactly what stayed unverified. Do not write ad-hoc probe or soak scripts — use the sanctioned checks +(`npx tsc --noEmit`, `npm run build`, `npm run lint:check`, `make check-docs`, and tests once they exist). + +## 4. Liveness rule (parent) + +If a delegated agent has ~40 turns with no file write, CANCEL and re-scope with evidence. Ask the agent for +its blocker instead of polling it repeatedly. + +## 5. Review findings + +The native review (RDD) owns severity and stop rules; this repo adds no severity semantics of its own. +Non-blocking findings are recorded in the feature doc as follow-ups and never trigger another fix cycle — +advisory-driven iteration never terminates. + +## 6. Worktree/HEAD checks + +Before accepting a delegated report, run: + +``` +git log --oneline -1 +git status -s +``` + +Verify the commit landed on the intended branch and the tree contains only the expected changes. Commits +landing on a detached HEAD or in the wrong order have happened before. + +## 7. Host gotchas (Pi / gentle-pi) + +- When a review stops, run the recovery command that STATUS returns; do not diagnose the stop by hand. +- Fix managed-asset staleness by running the bundle's `gentle-ai sync --agent pi`, not the workspace's. +- `gentle-pi`'s version is pinned in `~/.pi/agent/settings.json` (`packages`), not in this repo's + `package.json`; never add it as a dependency here. + +## 8. Handoff + +The feature doc (`odd/tasks/.md`) is the verbatim spec and the resume point. Keep it current: state, +commit id per task, and known debts. A restart must be cheap — the next session resumes from the doc, not from +memory. Closed feature docs move to `odd/archive/`. diff --git a/odd/archive/.gitkeep b/odd/archive/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 From f27189178df9ef705c24535d9ab1e9bd4094f82c Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 14:13:27 -0300 Subject: [PATCH 03/96] chore(tooling): add agent hooks, docs check, Makefile and fork CI Adds Claude settings with git-gate and prettier hooks, scripts/check-docs.sh wired into Husky pre-commit, a Makefile with a not-prod guard, purple-checks CI, GHCR image publishing on v* tags, VERSION 0.1.0 and a fork CHANGELOG section. Upstream Docker Hub publish workflows are disabled. --- .claude/hooks/git-gate.sh | 85 +++++++++++++++++++ .claude/settings.json | 35 ++++++++ .github/workflows/publish_docker_image.yml | 19 +++-- .../publish_docker_image_homolog.yml | 51 ++--------- .../workflows/publish_docker_image_latest.yml | 51 ++--------- .github/workflows/purple-checks.yml | 41 +++++++++ .husky/pre-commit | 5 ++ CHANGELOG.md | 11 +++ Makefile | 51 +++++++++++ VERSION | 1 + scripts/check-docs.sh | 81 ++++++++++++++++++ 11 files changed, 336 insertions(+), 95 deletions(-) create mode 100755 .claude/hooks/git-gate.sh create mode 100644 .claude/settings.json create mode 100644 .github/workflows/purple-checks.yml create mode 100644 Makefile create mode 100644 VERSION create mode 100755 scripts/check-docs.sh diff --git a/.claude/hooks/git-gate.sh b/.claude/hooks/git-gate.sh new file mode 100755 index 0000000000..6631da281e --- /dev/null +++ b/.claude/hooks/git-gate.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Claude Code PreToolUse hook (Bash matcher). +# Denies git commit/push/merge/rebase when: +# - the command uses --no-verify (bypasses Husky hooks), or +# - `npx tsc --noEmit` fails, or +# - ESLint fails on the staged (commit) or unpushed (push) .ts/.js files under src/. +# Emits a PreToolUse deny decision as JSON; otherwise exits 0 silently. + +set -o pipefail + +INPUT=$(cat) +if command -v jq >/dev/null 2>&1; then + CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) +else + CMD=$INPUT +fi + +GIT_WRITE='(^|[[:space:]&|;(])git([[:space:]]+-[^[:space:]]+)*[[:space:]]+(commit|push|merge|rebase)([[:space:]]|$)' +if ! printf '%s' "$CMD" | grep -qE "$GIT_WRITE"; then + exit 0 +fi + +deny() { + local reason=$1 + if command -v jq >/dev/null 2>&1; then + jq -n --arg reason "$reason" \ + '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":$reason}}' + else + # Fallback: exit code 2 blocks the tool call and shows stderr to the model. + printf '%s\n' "$reason" >&2 + exit 2 + fi + exit 0 +} + +# --no-verify anywhere, or the `git commit -n` short form. +if printf '%s' "$CMD" | grep -qE -- '--no-verify|git[[:space:]]+commit[^|;&]*[[:space:]]-n([[:space:]]|$)'; then + deny "Git gate: --no-verify is not allowed. Fix the failing Husky hooks instead of bypassing them." +fi + +ROOT=${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)} +[ -n "$ROOT" ] || exit 0 +cd "$ROOT" || exit 0 + +if printf '%s' "$CMD" | grep -qE 'git([[:space:]]+-[^[:space:]]+)*[[:space:]]+push'; then + UPSTREAM=$(git rev-parse --abbrev-ref '@{u}' 2>/dev/null) + if [ -n "$UPSTREAM" ]; then + FILES=$(git diff --name-only --diff-filter=d "$UPSTREAM"...HEAD 2>/dev/null) + else + FILES=$(git diff --name-only --diff-filter=d origin/main...HEAD 2>/dev/null) + fi +else + FILES=$(git diff --cached --name-only --diff-filter=d 2>/dev/null) +fi + +CODE_FILES=$(printf '%s\n' "$FILES" | grep -E '^src/.*\.(ts|js)$' || true) +[ -n "$CODE_FILES" ] || exit 0 + +FAILURES="" + +TSC=$(npx --no tsc --noEmit 2>&1) +if [ $? -ne 0 ]; then + FAILURES="${FAILURES}--- TypeScript (tsc --noEmit) --- +${TSC} + +" +fi + +LINT=$(printf '%s\n' "$CODE_FILES" | xargs npx --no eslint 2>&1) +if [ $? -ne 0 ]; then + FAILURES="${FAILURES}--- ESLint (changed files) --- +${LINT} + +" +fi + +if [ -n "$FAILURES" ]; then + deny "Git gate: commit/push/merge/rebase BLOCKED by errors in changed files. + +Fix the errors below and retry. + +${FAILURES}" +fi + +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..df0c1e360f --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,35 @@ +{ + "permissions": { + "allow": [ + "mcp__plugin_engram_engram__mem_search", + "mcp__plugin_engram_engram__mem_get_observation", + "mcp__plugin_engram_engram__mem_save", + "mcp__plugin_engram_engram__mem_context", + "mcp__plugin_engram_engram__mem_session_summary" + ] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/git-gate.sh\"" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "F=$(jq -r '.tool_input.file_path // empty'); [ -z \"$F\" ] && exit 0; case \"$F\" in *.ts|*.js|*.json|*.md) (cd \"$CLAUDE_PROJECT_DIR\" && npx --no prettier --config .prettierrc.js --write --log-level=silent \"$F\" >/dev/null 2>&1) || true ;; esac; exit 0" + } + ] + } + ] + } +} diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml index c5a3996edb..7cb2802fbc 100644 --- a/.github/workflows/publish_docker_image.yml +++ b/.github/workflows/publish_docker_image.yml @@ -1,9 +1,11 @@ -name: Build Docker image +name: Publish Docker image (GHCR) +# Fork publishing: version tags (v*) push to ghcr.io/purple-dice/evolution-in-purple. +# Upstream pushed to Docker Hub (evoapicloud/evolution-api); that is disabled in this fork. on: push: tags: - - "*.*.*" + - 'v*' jobs: build_deploy: @@ -22,8 +24,10 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: evoapicloud/evolution-api - tags: type=semver,pattern=v{{version}} + images: ghcr.io/purple-dice/evolution-in-purple + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -34,8 +38,9 @@ jobs: - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push id: docker_build @@ -47,4 +52,4 @@ jobs: labels: ${{ steps.meta.outputs.labels }} - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} \ No newline at end of file + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/publish_docker_image_homolog.yml b/.github/workflows/publish_docker_image_homolog.yml index a76e1008be..03b0d3f1d1 100644 --- a/.github/workflows/publish_docker_image_homolog.yml +++ b/.github/workflows/publish_docker_image_homolog.yml @@ -1,50 +1,13 @@ -name: Build Docker image +name: Build Docker image (homolog) - disabled +# Disabled in evolution-in-purple: upstream pushed to Docker Hub (evoapicloud/evolution-api), +# which this fork does not own. Fork images are published to GHCR by publish_docker_image.yml +# on v* tags. Kept as a manual no-op so the file can be deleted in a dedicated change. on: - push: - branches: - - develop + workflow_dispatch: jobs: - build_deploy: - name: Build and Deploy + disabled: runs-on: ubuntu-latest - permissions: - contents: read - packages: write steps: - - name: Checkout - uses: actions/checkout@v5 - with: - submodules: recursive - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: evoapicloud/evolution-api - tags: homolog - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push - id: docker_build - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} + - run: echo "Disabled in this fork; see publish_docker_image.yml (GHCR on v* tags)." diff --git a/.github/workflows/publish_docker_image_latest.yml b/.github/workflows/publish_docker_image_latest.yml index f73fe80e4e..60a84f74cd 100644 --- a/.github/workflows/publish_docker_image_latest.yml +++ b/.github/workflows/publish_docker_image_latest.yml @@ -1,50 +1,13 @@ -name: Build Docker image +name: Build Docker image (latest) - disabled +# Disabled in evolution-in-purple: upstream pushed to Docker Hub (evoapicloud/evolution-api), +# which this fork does not own. Fork images are published to GHCR by publish_docker_image.yml +# on v* tags. Kept as a manual no-op so the file can be deleted in a dedicated change. on: - push: - branches: - - main + workflow_dispatch: jobs: - build_deploy: - name: Build and Deploy + disabled: runs-on: ubuntu-latest - permissions: - contents: read - packages: write steps: - - name: Checkout - uses: actions/checkout@v5 - with: - submodules: recursive - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: evoapicloud/evolution-api - tags: latest - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push - id: docker_build - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} + - run: echo "Disabled in this fork; see publish_docker_image.yml (GHCR on v* tags)." diff --git a/.github/workflows/purple-checks.yml b/.github/workflows/purple-checks.yml new file mode 100644 index 0000000000..bf29412e76 --- /dev/null +++ b/.github/workflows/purple-checks.yml @@ -0,0 +1,41 @@ +name: Purple Checks + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + checks: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + DATABASE_PROVIDER: postgresql + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: 20.x + cache: npm + + - name: Install packages + run: npm ci + + - name: Generate Prisma client + run: npm run db:generate + + - name: Typecheck + run: npx tsc --noEmit + + - name: Build + run: npm run build + + - name: Lint + run: npm run lint:check + + - name: Check docs + run: bash scripts/check-docs.sh diff --git a/.husky/pre-commit b/.husky/pre-commit index 2312dc587f..f07aa1278d 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,6 @@ npx lint-staged + +# Docs drift guard: only when agent docs/tooling are staged. +if git diff --cached --name-only | grep -qE '^(AGENTS\.md|CLAUDE\.md|Makefile|\.claude/|\.pi/|odd/)'; then + bash scripts/check-docs.sh +fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 5102b5375c..480a0d8114 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# evolution-in-purple + +## 0.1.0 (Unreleased) + +### Chores + +* Fork baseline from Evolution API 2.3.7 +* Agentic setup: AGENTS.md, Claude skills and hooks, docs drift check, Makefile, fork CI (`purple-checks.yml`) and GHCR publishing on `v*` tags + +# Upstream Evolution API changelog + # 2.3.7 (2025-12-05) ### Features diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..44416c9005 --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +# evolution-in-purple developer entry points. +# Production deploy targets are intentionally absent until the deploy story is defined. + +IMAGE ?= evolution-in-purple:dev + +.PHONY: help install dev build lint typecheck check-docs hooks-install docker-build _guard-not-prod + +help: ## Show available targets + @grep -hE '^[a-zA-Z][a-zA-Z_-]*:.*## ' $(MAKEFILE_LIST) | \ + awk 'BEGIN { FS = ":.*## " } { printf " %-14s %s\n", $$1, $$2 }' + +install: ## Install dependencies from package-lock.json (npm ci) + npm ci + +dev: _guard-not-prod ## Run the API with hot reload (npm run dev:server) + npm run dev:server + +build: ## Typecheck and build to dist/ (npm run build) + npm run build + +lint: ## ESLint with auto-fix (npm run lint) + npm run lint + +typecheck: ## TypeScript check without emit + npx --no tsc --noEmit + +check-docs: ## Validate agent docs (AGENTS.md, CLAUDE.md, skills, links) + bash scripts/check-docs.sh + +hooks-install: ## Install Husky git hooks (.husky/) + npx --no husky + +docker-build: _guard-not-prod ## Build the local Docker image (default tag evolution-in-purple:dev) + docker build -t $(IMAGE) . + +# Refuses dev-only targets on a host that looks like production. +# Override with FORCE_DEV=1. +_guard-not-prod: + @if [ "$$FORCE_DEV" = "1" ]; then \ + echo "FORCE_DEV=1 - skipping prod-host guard"; \ + exit 0; \ + fi; \ + if [ "$$NODE_ENV" = "production" ]; then \ + echo "Refusing to run: NODE_ENV=production. Set FORCE_DEV=1 to override."; \ + exit 1; \ + fi; \ + if docker ps --format '{{.Names}}' 2>/dev/null | grep -qE '^evolution-in-purple(-prod|-blue|-green)'; then \ + echo "Refusing to run: production evolution-in-purple containers detected on this host."; \ + echo "Set FORCE_DEV=1 to override."; \ + exit 1; \ + fi diff --git a/VERSION b/VERSION new file mode 100644 index 0000000000..6e8bf73aa5 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/scripts/check-docs.sh b/scripts/check-docs.sh new file mode 100755 index 0000000000..df5134a66f --- /dev/null +++ b/scripts/check-docs.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Docs drift guard. Usage: scripts/check-docs.sh [root] +# +# Rules (one `file:line: reason` per violation; exit 1 when any is found): +# a) CLAUDE.md is a pointer to AGENTS.md (references AGENTS.md, at most 20 lines) +# b) every .claude/skills/*/SKILL.md has frontmatter with `name:` and a +# `description:` containing `Trigger:` +# c) every .claude/skills/ name appears in AGENTS.md +# d) relative markdown links in AGENTS.md, odd/agent-protocol.md and +# .claude/skills/**/*.md resolve (http(s)/mailto/anchors are skipped) +# Run via `make check-docs`; also invoked by .husky/pre-commit and CI. +set -euo pipefail + +root=${1:-$(git -C "$(dirname "$0")/.." rev-parse --show-toplevel)} +cd "$root" + +violations=$(mktemp) +trap 'rm -f "$violations"' EXIT +report() { echo "$1" >>"$violations"; } + +# --- a) CLAUDE.md pointer --------------------------------------------------- +if [[ ! -f AGENTS.md ]]; then + report "AGENTS.md:0: missing (canonical agent guide)" +fi +if [[ ! -f CLAUDE.md ]]; then + report "CLAUDE.md:0: missing (must be a pointer to AGENTS.md)" +else + grep -q 'AGENTS\.md' CLAUDE.md || report "CLAUDE.md:1: does not reference AGENTS.md" + lines=$(wc -l /dev/null | + while IFS=: read -r line link; do + target=${link#](} + target=${target%)} + target=${target%% *} # drop optional "title" + target=${target%%#*} # drop anchors + case $target in + '' | http://* | https://* | mailto:*) continue ;; + /*) path=.$target ;; + *) path=$base/$target ;; + esac + [[ -e $path ]] || report "$f:$line: broken link '$target'" + done || true +done + +if [[ -s $violations ]]; then + cat "$violations" + echo "check-docs: $(wc -l <"$violations" | tr -d ' ') violation(s)" >&2 + exit 1 +fi +echo "check-docs: OK" From 453773b21429c45656cad15c20a529fe4786d434 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 14:16:49 -0300 Subject: [PATCH 04/96] docs(skills): add purple-* project skills and Pi settings Adds purple-typescript, purple-baileys, purple-contract (with consumer matrix for orbit, canal-whatsapp and stenfar), purple-upstream-sync and purple-release. .pi/settings.json reuses .claude/skills. --- .claude/skills/purple-baileys/SKILL.md | 69 ++++++++++++++++ .../skills/purple-baileys/references/lid.md | 44 +++++++++++ .claude/skills/purple-contract/SKILL.md | 72 +++++++++++++++++ .../references/consumer-matrix.md | 74 ++++++++++++++++++ .claude/skills/purple-release/SKILL.md | 73 +++++++++++++++++ .claude/skills/purple-typescript/SKILL.md | 78 +++++++++++++++++++ .../references/conventions.md | 44 +++++++++++ .claude/skills/purple-upstream-sync/SKILL.md | 66 ++++++++++++++++ .pi/settings.json | 3 + .pi/subagents.json | 22 ++++++ 10 files changed, 545 insertions(+) create mode 100644 .claude/skills/purple-baileys/SKILL.md create mode 100644 .claude/skills/purple-baileys/references/lid.md create mode 100644 .claude/skills/purple-contract/SKILL.md create mode 100644 .claude/skills/purple-contract/references/consumer-matrix.md create mode 100644 .claude/skills/purple-release/SKILL.md create mode 100644 .claude/skills/purple-typescript/SKILL.md create mode 100644 .claude/skills/purple-typescript/references/conventions.md create mode 100644 .claude/skills/purple-upstream-sync/SKILL.md create mode 100644 .pi/settings.json create mode 100644 .pi/subagents.json diff --git a/.claude/skills/purple-baileys/SKILL.md b/.claude/skills/purple-baileys/SKILL.md new file mode 100644 index 0000000000..34e099441f --- /dev/null +++ b/.claude/skills/purple-baileys/SKILL.md @@ -0,0 +1,69 @@ +--- +name: purple-baileys +description: > + Landmarks and invariants of the Baileys channel in evolution-in-purple: + whatsapp.baileys.service.ts entry points, onWhatsApp guards, the on-WhatsApp cache, + createJid, @lid (LID) addressing rules, QR / pairing code flow and the Baileys version pin. + Read BEFORE touching message receive/send, JID handling or connection logic. + Trigger: change in whatsapp.baileys.service.ts, onWhatsappCache.ts, createJid.ts, @lid / remoteJidAlt handling, QR or pairing, Baileys upgrade. +--- + +# Skill: purple-baileys + +File: `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` (~5k lines). +Baileys is pinned exactly: `"baileys": "7.0.0-rc.9"` (`package.json:80`). Never use a caret; +an upgrade is its own task with manual QR + send/receive verification. + +## Landmarks (verify line numbers before citing; they drift) + +| Area | Location | +|---|---| +| QR / pairing in `connectionUpdate` | `:334`, pairing code `:375-380`, QR limit `:336` | +| Auth state selection (Redis / Prisma / provider files) | `defineAuthState` `:556` | +| Socket creation | `createClient` `:576` | +| Public connect entry | `connectToWhatsapp` `:723` | +| Inbound handler | `messageHandle['messages.upsert']` `:1082` | +| LID → PN swap before emit | `:1478-1480` | +| Contact upsert using `remoteJidAlt` | `:1512-1517` | +| Event dispatch (`connection.update`, `messages.upsert`) | `:1902-1919` | +| Outbound core | `sendMessageWithTyping` `:2289` | +| onWhatsApp guards (throw 400 when `!exists`) | `:2297`, `sendPresence` `:2571`, `blockUser` `:4118` | +| Number verification | `whatsappNumber` `:3507` (cache → Baileys `onWhatsApp` `:3566`) | + +Helpers: `src/utils/onWhatsappCache.ts` (`saveOnWhatsappCache` `:73`, `getOnWhatsappCache` +`:182`, `normalizeJid` `:68`) and `src/utils/createJid.ts` (`createJid` `:35`). + +## @lid rules (hard) + +1. **LID → PN is NOT derivable.** A `@lid` carries no phone number. Never compute, + guess or strip the suffix to obtain one. +2. **PN is only learned** from: `key.remoteJidAlt` / `participantAlt` on received messages, + group participant metadata, or history sync. Persist the mapping when observed + (contact upsert at `:1512-1517`, `saveOnWhatsappCache`). +3. **Never strip `@lid` on outbound.** `createJid` returns `@lid` JIDs unchanged + (`createJid.ts:38-40`); `whatsappNumber` skips Baileys verification for `@lid` and treats an + uncached LID as existing (`:3562`, `:3586-3594`). Keep both behaviors. +4. The inbound swap at `:1478-1480` rewrites `remoteJid` to `remoteJidAlt` only when the alt is + present. Consumers (orbit, stenfar) also read `remoteJidAlt`/`addressingMode` themselves — + changing this is a contract change (`purple-contract`). + +Detail, failure modes and examples: `references/lid.md`. + +## onWhatsApp guards + +`sendMessageWithTyping`, `sendPresence` and `blockUser` all call `whatsappNumber` and throw +`BadRequestException(isWA)` when `!isWA.exists` and the JID is not a group/broadcast. The 400 +body (the `OnWhatsAppDto`) is what consumers see on "number not on WhatsApp". Do not bypass the +guard; fix the cache or the JID instead. + +## QR / pairing + +- `connectToWhatsapp(number?)` — passing a number enables pairing-code mode + (`this.phoneNumber`, `:375-380`); otherwise only QR. +- QR emits `qrcode.updated` with `{ pairingCode, code, base64 }` (`:392`, `:400`); the count is + capped by `QRCODE.LIMIT` (`:336`), after which the instance stops retrying. + +## Verification + +No automated tests. After changes: `npx tsc --noEmit`, `npm run build`, then a manual run +with a test instance — QR scan, inbound 1:1 (PN and LID contact), inbound group, outbound text. diff --git a/.claude/skills/purple-baileys/references/lid.md b/.claude/skills/purple-baileys/references/lid.md new file mode 100644 index 0000000000..6f45c3f734 --- /dev/null +++ b/.claude/skills/purple-baileys/references/lid.md @@ -0,0 +1,44 @@ +# purple-baileys — @lid detail + +## What a LID is + +WhatsApp privacy addressing: some 1:1 and group participants appear as `@lid` +(`key.addressingMode === 'lid'`) instead of `@s.whatsapp.net`. The digits are an opaque +identifier, not a phone number. + +## Where the phone number (PN) can come from + +| Source | Field | Code | +|---|---|---| +| Received message key | `key.remoteJidAlt` (1:1), `key.participantAlt` (groups) | `whatsapp.baileys.service.ts:159-160` (key type), `:1250-1251`, `:1478`, `:1516-1517` | +| Group metadata | participant entries returned by Baileys group metadata | group fetch paths (shape depends on Baileys version — verify) | +| History sync | message keys in `messaging-history.set` | `whatsapp.baileys.service.ts:927` | +| On-WhatsApp cache | `jidOptions`, `lid` | `src/utils/onWhatsappCache.ts:73`, `:182` | + +If none of these has been observed, the PN is unknown. Reply to the LID JID. + +## Outbound + +- `createJid` keeps `@lid`, `@g.us`, `@s.whatsapp.net`, `@broadcast` untouched + (`src/utils/createJid.ts:38-44`); only bare numbers get normalized (BR/MX/AR rules at `:2`, `:17`). +- `whatsappNumber` (`whatsapp.baileys.service.ts:3507`) resolves users from the cache first, then + calls `client.onWhatsApp` only for non-LID JIDs (`:3562-3566`). An uncached LID returns + `exists: true` with `lid: 'lid'` (`:3586-3594`). + +## Known failure modes + +- Stripping `@lid` and sending to `@s.whatsapp.net` → 400 "exists: false" from the guard + or delivery to the wrong account. +- Treating `remoteJid` as the stable chat key: the swap at `:1478-1480` means the same chat can + arrive as PN or LID depending on whether `remoteJidAlt` was present. +- Persisting a LID as a phone number in consumer CRMs. Consumers must read `remoteJidAlt` + (orbit: `orbit-backend/src/evolution-api-webhook/evolution-api-webhook.service.ts:227-231`; + stenfar: `canal-stenfar/src/services/whatsapp/phoneExtractor.js:35-44`). + +## Manual verification matrix + +1. 1:1 inbound from a PN contact → `remoteJid` is `@s.whatsapp.net`. +2. 1:1 inbound from a LID contact with alt → `remoteJid` swapped to PN, `remoteJidAlt` kept. +3. Group inbound with LID participant → `participant` LID, `participantAlt` PN when present. +4. Outbound to a LID JID → delivered; no 400. +5. Outbound to an unknown PN → `onWhatsApp` check, 400 when not registered. diff --git a/.claude/skills/purple-contract/SKILL.md b/.claude/skills/purple-contract/SKILL.md new file mode 100644 index 0000000000..9228b51944 --- /dev/null +++ b/.claude/skills/purple-contract/SKILL.md @@ -0,0 +1,72 @@ +--- +name: purple-contract +description: > + The external contract of evolution-in-purple as consumed by orbit, canal-whatsapp and + stenfar: webhook payload envelope, event naming (MESSAGES_UPSERT vs messages-upsert with + webhookByEvents), and the REST routes each consumer calls. Read BEFORE changing any route, + DTO, response shape, webhook event or payload field. + Trigger: change to a REST route, request/response shape, webhook event name or payload, message key fields (remoteJid, remoteJidAlt), or instance/webhook config. +--- + +# Skill: purple-contract + +This engine replaces Evolution API 2.3.7 for three consumers. **The contract is "behaves +exactly like Evolution 2.3.7"** unless a versioned breaking change says otherwise. +Consumer matrix (endpoint/event × consumer, with source paths): `references/consumer-matrix.md`. + +## Webhook envelope + +Built in `src/api/integrations/event/webhook/webhook.controller.ts:93-103`: + +```ts +{ ...extra, event, instance, data, destination, date_time, sender, server_url, apikey } +``` + +- `event` is the dotted lowercase name (`messages.upsert`, `connection.update`, ...). +- Enabled-events list is stored uppercase with underscores (`MESSAGES_UPSERT`), derived at + `webhook.controller.ts:88`. +- With `webhookByEvents: true` the URL becomes `${url}/${event-kebab}` — e.g. + `/messages-upsert` (`webhook.controller.ts:89`, `:109-110`). orbit depends on this + (routes `whatsapp/messages-upsert`, `qrcode-updated`, `connection-update`). +- canal-whatsapp and stenfar use a single URL (`byEvents: false`) and switch on `event`. + +## Message key fields consumers read + +`data.key.remoteJid`, `remoteJidAlt`, `participant`, `participantAlt`, `addressingMode`, +`fromMe`, `id`. The LID → PN swap (`whatsapp.baileys.service.ts:1478-1480`) is part of the +contract (see `purple-baileys`). + +## REST surface consumers use + +Union of all consumers (details per consumer in the matrix): +`/instance/{create,fetchInstances,connect,connectionState,logout}`, +`/message/{sendText,sendMedia,sendWhatsAppAudio,sendReaction}`, +`/chat/{markMessageAsRead,sendPresence,getBase64FromMediaMessage,findMessages,findContacts}`, +`/group/findGroupInfos`, `/webhook/{set,find}`. Auth: `apikey` header. +canal also calls `/group/findGroupByJid`, which does **not** exist here (see matrix §Open questions). + +## Rules + +1. **Non-breaking by default.** Additive fields are allowed; renaming, removing, retyping a + field, changing a status code, an event name or a URL suffix is breaking. +2. **Breaking change ⇒ all of:** (a) new fork MAJOR/MINOR per `purple-release`, (b) a + `CHANGELOG.md` entry under the `evolution-in-purple` section with a migration note, + (c) a consumer check: grep each consumer listed in the matrix for the affected + route/event/field and record positives/negatives in the task doc. +3. Update `references/consumer-matrix.md` in the same change whenever a consumer starts or + stops using a route/event. +4. Consumers are read-only from this repo. Never edit orbit, canal-whatsapp or stenfar from + here; open a task in the consumer instead. + +## Consumer check (procedure) + +```bash +rg -n '' \ + ../../orbit/orbit-backend/src \ + ../../canal-whatsapp/canal-whatsapp-backend/src \ + ../../stenfar/canal-stenfar/src +``` + +Paths are relative to a checkout under `~/Programacion/evolution/`; adjust if the +layout differs. canal-whatsapp calls REST through `@alien-2-0-llc/sdk-evolution-api`, so also +grep that SDK's method names (`sendText`, `setWebhook`, ...). diff --git a/.claude/skills/purple-contract/references/consumer-matrix.md b/.claude/skills/purple-contract/references/consumer-matrix.md new file mode 100644 index 0000000000..e5f7de09b4 --- /dev/null +++ b/.claude/skills/purple-contract/references/consumer-matrix.md @@ -0,0 +1,74 @@ +# Consumer matrix — evolution-in-purple + +Snapshot taken 2026-10-21 by reading the consumers (read-only). Re-verify before relying on a +row; consumers evolve independently. + +Consumer roots: +- **orbit** — `orbit/orbit-backend/src/channels/evolution-api.service.ts` (REST, axios) and + `orbit/orbit-backend/src/evolution-api-webhook/` (webhook receiver). +- **canal** — `canal-whatsapp/canal-whatsapp-backend/src/evolution-client/` plus + `@alien-2-0-llc/sdk-evolution-api@^0.11.0` (REST) and `src/inbound-webhook/inbound-webhook.controller.ts` + (`POST /evolution/webhook`). +- **stenfar** — `stenfar/canal-stenfar/src/services/whatsapp/evolution.service.js`, + `src/routes/widget.routes.js` (REST, fetch) and `src/routes/whatsapp.routes.js` + (`POST /webhook/whatsapp`). + +## REST routes + +| Route | orbit | canal | stenfar | +|---|---|---|---| +| `POST /instance/create` | yes (`evolution-api.service.ts:38`, with `webhook` body) | yes (`evolution-client.service.ts:139`) | — | +| `GET /instance/fetchInstances` | yes (`:20`) | yes (`evolution-client.service.ts:109`) | yes (`widget.routes.js:29`) | +| `GET /instance/connect/:instance` | yes (`:107`) | yes (`:116`) | yes (`widget.routes.js:74`) | +| `GET /instance/connectionState/:instance` | yes (`:212`) | — | yes (`evolution.service.js:299`) | +| `DELETE /instance/logout/:instance` | yes (`:233`) | yes (`:123`) | — | +| `POST /webhook/set/:instance` | — | yes (`:181`) | — | +| `GET /webhook/find/:instance` | — | yes (`:215`) | — | +| `POST /message/sendText/:instance` | yes (`:183`) | yes (SDK `sendText`) | yes (`evolution.service.js:23`) | +| `POST /message/sendMedia/:instance` | yes (`:248`) | yes (SDK `sendMedia`) | yes (`evolution.service.js:83`) | +| `POST /message/sendWhatsAppAudio/:instance` | — | yes (SDK `sendAudio`) | yes (`evolution.service.js:148`) | +| `POST /message/sendReaction/:instance` | — | yes (SDK `sendReaction`) | — | +| `POST /chat/markMessageAsRead/:instance` | yes (`:135`) | yes (SDK) | — | +| `POST /chat/sendPresence/:instance` | yes (`:155`) | in SDK, no call site seen | — | +| `POST /chat/getBase64FromMediaMessage/:instance` | — | yes (`inbound-worker.service.ts:2981`) | yes (`evolution.service.js:204`) | +| `POST /chat/findMessages/:instance` | — | in SDK, no call site seen | yes (`evolution.service.js:340`) | +| `POST /chat/findContacts/:instance` | — | — | yes (`evolution.service.js:254`) | +| `GET /group/findGroupInfos/:instance` | — | yes (SDK `findGroupInfos`) | — | +| `GET /group/findGroupByJid/:instance` (**not a route in this repo**) | — | yes (`evolution-group.client.ts:82`) | — | + +## Webhook delivery + +| Aspect | orbit | canal | stenfar | +|---|---|---|---| +| Mode | `byEvents: true` (`evolution-api.service.ts:46`) | `byEvents: false` (`evolution-client.service.ts:188`) | single URL (config not set from code — verify) | +| Receiver | `POST /evolution-api-webhook/whatsapp/` | `POST /evolution/webhook` | `POST /webhook/whatsapp` | + +## Webhook events + +| Event | orbit | canal | stenfar | +|---|---|---|---| +| `messages.upsert` / `MESSAGES_UPSERT` | handled (`/messages-upsert`) | subscribed (`webhook-config.ts:31`) | handled (`whatsapp.controller.js:73`) | +| `messages.update` / `MESSAGES_UPDATE` | subscribed | subscribed (`:32`) | handled (acks, `mensaje.service.js:114-132`) | +| `connection.update` / `CONNECTION_UPDATE` | handled (`/connection-update`) | subscribed (`:33`) | handled (`whatsapp.controller.js:67`) | +| `qrcode.updated` / `QRCODE_UPDATED` | handled (`/qrcode-updated`) | — | — | +| `logout.instance` / `LOGOUT_INSTANCE` | — | subscribed (`:34`) | handled (`whatsapp.controller.js:67`) | +| `send.message` / `SEND_MESSAGE` | subscribed | — | referenced | +| Others (`MESSAGES_SET`, `MESSAGES_DELETE`, `CONTACTS_*`, `PRESENCE_UPDATE`, `CHATS_SET`, ...) | subscribed in create body (`evolution-api.service.ts:47+`) | — | — | + +## Payload fields relied on + +| Field | orbit | canal | stenfar | +|---|---|---|---| +| `data.key.remoteJid` / `remoteJidAlt` / `addressingMode` | yes (`evolution-api-webhook.service.ts:227-231`, dto `whatsapp-message-event.dto.ts:14-17`) | yes (inbound worker; exact call sites not audited) | yes (`phoneExtractor.js:35-44`) | +| `messages.update` numeric status | — | yes | yes (`mensaje.service.js:132`: ints in update, strings in upsert) | + +## Open questions + +- canal comments (`webhook-config.ts:8`) state that 2.3.7 `/instance/create` ignores the + `webhook` field, while orbit relies on it and this repo calls + `eventManager.setInstance(...)` from create (`src/api/controllers/instance.controller.ts:88`). + Verify before changing either path. +- canal calls `GET /group/findGroupByJid/:instance` (`evolution-group.client.ts:39`, "ASSUMED RESPONSE SHAPE"), but + `src/api/routes/group.router.ts` only defines `findGroupInfos` (`:79`) and similar. The call + likely 404s today; decide whether to add the route or fix canal. +- stenfar's webhook registration is not visible in code (likely manual / manager UI). diff --git a/.claude/skills/purple-release/SKILL.md b/.claude/skills/purple-release/SKILL.md new file mode 100644 index 0000000000..f5061f6ef3 --- /dev/null +++ b/.claude/skills/purple-release/SKILL.md @@ -0,0 +1,73 @@ +--- +name: purple-release +description: > + Release procedure for evolution-in-purple: fork SemVer independent of upstream (VERSION file, + git tag vX.Y.Z, GHCR image), CHANGELOG.md layout with an evolution-in-purple section above + the upstream history, and the approval gates for push / PR / release / deploy. + Trigger: release, version bump, tag, changelog entry, publishing the Docker image, deploy. +--- + +# Skill: purple-release + +## Versioning + +- The fork has its **own SemVer**, independent of upstream (`package.json` keeps the upstream + `2.3.7` lineage; do not use it as the fork version). +- Source of truth: `VERSION` file at repo root, single line `X.Y.Z`. First release `0.1.0`. +- Git tag: `vX.Y.Z` (annotated), must equal `VERSION`. Upstream tags look like `2.3.7` (no `v`) — + never create or move those. +- Image: `ghcr.io/purple-dice/evolution-in-purple:X.Y.Z` (plus `latest` only for the newest + release). The upstream workflows under `.github/workflows/publish_docker_image*.yml` push to + `evoapicloud/evolution-api`; the fork publish workflow is owned by tooling, not by this skill. + +### Bump rules (pre-1.0) + +| Change | Bump | +|---|---| +| Breaking contract change (see `purple-contract`) | MINOR (`0.x` → `0.(x+1).0`) | +| Feature / backport adding behavior | MINOR | +| Fix only | PATCH | + +After `1.0.0`, standard SemVer (breaking ⇒ MAJOR). + +## CHANGELOG.md layout + +```markdown +# evolution-in-purple + +## X.Y.Z (YYYY-MM-DD) + +### Breaking +- — consumers affected: orbit | canal-whatsapp | stenfar; migration: + +### Features / Fixes / Backports +- (upstream EvolutionAPI/evolution-api#, ) when backported + +--- + +# 2.3.7 (2025-12-05) ← upstream history, untouched below +``` + +The upstream history (currently starting at `CHANGELOG.md:1` with `# 2.3.7 (2025-12-05)`) is +never edited. + +## Procedure + +1. Ensure `main` is clean and green: `npx tsc --noEmit`, `npm run build`, `npm run lint:check`. +2. Decide the next version from the bump rules; list the changes since the last `v*` tag: + `git log --oneline $(git describe --tags --match 'v*' --abbrev=0)..HEAD` + (first release: since `2.3.7`). +3. Update `VERSION` and the `evolution-in-purple` section of `CHANGELOG.md` on a + `release/vX.Y.Z` branch. Any `Breaking` entry requires a completed consumer check. +4. **Ask the user** before: pushing the branch, opening the PR, merging, creating/pushing the + tag, publishing the image, deploying. Each is a separate approval. +5. After merge (approved): `git tag -a vX.Y.Z -m "evolution-in-purple vX.Y.Z"` on the merge + commit, push the tag (approved), let CI publish the image. +6. Verify the image tag exists in GHCR and report it. Consumers switch versions in their own + repos — never edit them from here. + +## Invariants + +- `VERSION` == latest `v*` tag == image tag. +- One release per tag; never re-tag. Fix forward with a PATCH. +- No push, PR, release or deploy without explicit user approval in the current conversation. diff --git a/.claude/skills/purple-typescript/SKILL.md b/.claude/skills/purple-typescript/SKILL.md new file mode 100644 index 0000000000..a0e05290d8 --- /dev/null +++ b/.claude/skills/purple-typescript/SKILL.md @@ -0,0 +1,78 @@ +--- +name: purple-typescript +description: > + Codebase conventions for evolution-in-purple (Evolution API 2.3.7 fork): RouterBroker + routes, thin controllers, services, plain DTOs, JSONSchema7 validation, manual singletons + in server.module.ts, ConfigService env, three Prisma schemas, cache engine, and the + verification commands. Read BEFORE editing anything under src/ or prisma/. + Trigger: new or changed endpoint, controller, service, DTO, schema, env var, Prisma model, or cache usage. +--- + +# Skill: purple-typescript + +TypeScript + Express + Prisma. No DI container, no decorators. Follow the existing shape of +the nearest sibling file; do not introduce new patterns. + +## Request flow + +1. **Router** extends `RouterBroker` (`src/api/abstract/abstract.router.ts:20`). + - `routerPath(path)` appends `/:instanceName` by default (`abstract.router.ts:22-26`). + - `dataValidate({ request, schema, ClassRef, execute })` (`abstract.router.ts:29`) + merges `params` + `query` into an `InstanceDto`, `Object.assign`s the body into a new + `ClassRef`, validates with `jsonschema` and throws `BadRequestException` on failure. + - Routers are mounted in `src/api/routes/index.router.ts:217-230` (`/instance`, `/message`, + `/chat`, `/group`, ... plus `''` for channel/event/chatbot/storage routers). +2. **Controller** — thin, delegates to a service or to `waMonitor.waInstances[instanceName]`. +3. **Service** — business logic; channel-specific behavior lives in + `src/api/integrations/channel/**` (see `purple-baileys`). +4. **DTO** — plain classes in `src/api/dto/` (no class-validator). +5. **Schema** — `JSONSchema7` objects in `src/validate/*.schema.ts`, re-exported by + `src/validate/validate.schema.ts`. + +## Wiring (manual singletons) + +`src/api/server.module.ts` instantiates everything once and exports it: +`cache` (`:55`), `prismaRepository` (`:63`), `waMonitor` (`:65`), controllers (`:90-139`). +A new service/controller is added there with explicit constructor args; routers import the +exported controller. Detail and a checklist: `references/conventions.md`. + +## Config + +`ConfigService.get(key)` (`src/config/env.config.ts:443`) returns typed sections loaded from +env. New env vars: add the type + loader in `env.config.ts` and document in `.env.example`. +Never read `process.env` directly in services. + +## Database + +- Three schemas: `prisma/postgresql-schema.prisma`, `prisma/mysql-schema.prisma`, + `prisma/psql_bouncer-schema.prisma`. A model change must land in **all** of them. +- Migrations per provider: `prisma/postgresql-migrations/`, `prisma/mysql-migrations/`. +- Scripts resolve the provider via `runWithProvider.js` and `DATABASE_PROVIDER` + (`package.json:17-22`). Always scope queries by `instanceId`. + +## Cache + +`CacheEngine` (`src/cache/cacheengine.ts:10`) picks Redis when `CACHE.REDIS.ENABLED` and a +URI are set, else local node-cache (`cacheengine.ts:19-25`). Wrap it in `CacheService` with a +module name, as `server.module.ts:55-56` does. + +## Commands + +| Purpose | Command | +|---|---| +| Typecheck | `npx tsc --noEmit` | +| Build (typecheck + tsup) | `npm run build` (`package.json:8`) | +| Lint check / fix | `npm run lint:check` / `npm run lint` (`package.json:13-14`) | +| Dev server | `npm run dev:server` | +| Prisma client | `npm run db:generate` (needs `DATABASE_PROVIDER`) | + +There is **no test suite** (`package.json:12` points at a non-existent `test/all.test.ts`). +Verification = `tsc --noEmit` + `npm run build` + `npm run lint:check` + manual run. + +## Rules + +- Style: ESLint + Prettier, 2 spaces, single quotes, 120 cols, `simple-import-sort`. +- Path aliases (`@api/*`, `@config/*`, `@utils/*`, `@exceptions`) — never deep relative imports. +- Keep diffs minimal: this is a fork; every unnecessary change costs on upstream cherry-picks + (see `purple-upstream-sync`). +- Any change to a REST route or webhook payload is a contract change: read `purple-contract`. diff --git a/.claude/skills/purple-typescript/references/conventions.md b/.claude/skills/purple-typescript/references/conventions.md new file mode 100644 index 0000000000..e347607150 --- /dev/null +++ b/.claude/skills/purple-typescript/references/conventions.md @@ -0,0 +1,44 @@ +# purple-typescript — conventions detail + +## Adding an endpoint (checklist) + +1. DTO in `src/api/dto/.dto.ts` — plain class, optional fields with `?`. +2. Schema in `src/validate/.schema.ts` — `JSONSchema7` with `$id: v4()`; export it from + `src/validate/validate.schema.ts`. +3. Service method (or channel-service method on `ChannelStartupService` subclasses when it + depends on the WhatsApp provider). +4. Controller method — one line delegating to the service. +5. Router — `this.router.post(this.routerPath('name'), ...guards, async (req, res) => { + const response = await this.dataValidate({ request: req, schema, ClassRef: Dto, + execute: (instance, data) => controller.method(instance, data) }); res.status(201).json(response); })`. +6. If it is a new router, mount it in `src/api/routes/index.router.ts:217-230`. +7. If it is a new service/controller, instantiate it in `src/api/server.module.ts` next to its + siblings and export the controller. +8. Run `npx tsc --noEmit`, `npm run build`, `npm run lint:check`. +9. Update `purple-contract/references/consumer-matrix.md` if a consumer will call it. + +## RouterBroker details + +- `routerPath(path, param = true)` — pass `false` for routes without `:instanceName`. +- `dataValidate` special-cases `/instance/create` by merging the body into the instance DTO + (`src/api/abstract/abstract.router.ts:40-42`). +- `groupNoValidate` / `groupValidate` variants exist for group routes (`abstract.router.ts:65+`). +- Validation error messages come from the schema `description` when present. + +## server.module.ts + +- Order matters: `configService`, caches (`:52-56`), `providerFiles` (`:60`), + `prismaRepository` (`:63`), `waMonitor` (`:65`), then services/controllers. +- Chatbot integrations share `openaiService` (`:119`) as a constructor arg. + +## Prisma + +- Edit the three schema files consistently; types differ by provider (`@db.JsonB` vs `@db.Json`, + `@db.Integer` vs `@db.Int`). +- Generate migrations with `npm run db:migrate:dev` per provider; the script copies + `prisma/migrations` back into the provider folder. +- Destructive migrations require explicit user approval. + +## Logging + +`new Logger('Context')` from `@config/logger.config`; use `verbose` for chatty paths. diff --git a/.claude/skills/purple-upstream-sync/SKILL.md b/.claude/skills/purple-upstream-sync/SKILL.md new file mode 100644 index 0000000000..e5433460bf --- /dev/null +++ b/.claude/skills/purple-upstream-sync/SKILL.md @@ -0,0 +1,66 @@ +--- +name: purple-upstream-sync +description: > + Procedure to backport fixes from upstream Evolution API into evolution-in-purple by + cherry-pick, with provenance, conflict resolution, build verification and a denylist of + upstream changes that must never be imported (license activation, telemetry, EvoHub, + mass dependency bumps). + Trigger: backport, cherry-pick, upstream fix, "sync with upstream", porting an Evolution API PR or commit. +--- + +# Skill: purple-upstream-sync + +Base: tag `2.3.7` (`cd800f29 Merge branch 'release/2.3.7'`). The fork does **not** merge or +rebase onto upstream; it imports selected commits only. + +## Remote + +```bash +git remote -v # upstream must exist +git fetch upstream --tags +``` + +`upstream` currently points to `https://github.com/EvolutionAPI/evolution-api.git` ; the task +spec names `evolution-foundation/evolution-api` as canonical — confirm which URL is current +with the user before changing the remote. Never push to upstream. + +## Procedure (one backport = one branch = one PR) + +1. Identify the source: upstream PR number and commit sha(s). `git log upstream/main --oneline` + or `gh pr view --repo EvolutionAPI/evolution-api`. +2. Check the denylist below. If the commit touches any denylisted area, stop and ask. +3. Branch from `main`: `git switch -c backport/`. +4. Cherry-pick with provenance: + - normal commit: `git cherry-pick -x ` + - merge commit: `git cherry-pick -x -m 1 ` +5. Resolve conflicts preserving fork behavior (contract, `@lid` rules). Prefer the minimal + hunk; drop unrelated upstream refactors. +6. Amend the message body (keep the `(cherry picked from commit ...)` trailer) with: + ``` + Upstream: EvolutionAPI/evolution-api# () + Conflicts: + ``` +7. Verify: `npx tsc --noEmit`, `npm run build`, `npm run lint:check`; manual run if it touches + Baileys (`purple-baileys`). +8. If the change alters routes or payloads, follow `purple-contract` before opening the PR. +9. Add a `CHANGELOG.md` line under the `evolution-in-purple` section (see `purple-release`). +10. Push / open PR only with explicit user approval. + +## Denylist (never import) + +| Upstream change | Reason | +|---|---| +| License activation — PR #2530, commit `3fb04ed6` | Phones home / gates features | +| Telemetry additions or expansions (`src/utils/sendTelemetry.ts` call sites) | Data leaves the deployment | +| EvoHub — PRs #2577, #2636 | Upstream SaaS coupling | +| Mass dependency bump — PR #2575 | Unreviewable; bump deps individually instead | + +A commit that mixes a wanted fix with a denylisted area: port the fix by hand, cite the source +sha in the body as `Adapted-from:` instead of `-x`. + +## Conflict hot spots + +`src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts`, `package.json` / +`package-lock.json` (Baileys pin `package.json:80`), the three `prisma/*-schema.prisma` files +and provider migrations. For lockfile conflicts, take ours and re-run the install only with user +approval. diff --git a/.pi/settings.json b/.pi/settings.json new file mode 100644 index 0000000000..130a365e75 --- /dev/null +++ b/.pi/settings.json @@ -0,0 +1,3 @@ +{ + "skills": ["../.claude/skills"] +} diff --git a/.pi/subagents.json b/.pi/subagents.json new file mode 100644 index 0000000000..2c3d257cbf --- /dev/null +++ b/.pi/subagents.json @@ -0,0 +1,22 @@ +{ + "kind": "gentle-pi.agent-model-config", + "version": 1, + "model_profiles": { + "review-risk": { + "model": "nan/mimo-v2.6-flash", + "effort": "medium" + }, + "review-resilience": { + "model": "nan/glm5.3-flash", + "effort": "low" + }, + "review-readability": { + "model": "nan/glm5.3-flash", + "effort": "low" + }, + "review-reliability": { + "model": "nan/glm5.3-flash", + "effort": "low" + } + } +} \ No newline at end of file From c2aa1dd54e20d0c969ea0619cd869a382028e00e Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 14:17:00 -0300 Subject: [PATCH 05/96] docs(skills): point upstream references to evolution-foundation --- .claude/skills/purple-release/SKILL.md | 2 +- .claude/skills/purple-upstream-sync/SKILL.md | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.claude/skills/purple-release/SKILL.md b/.claude/skills/purple-release/SKILL.md index f5061f6ef3..8b7ea965b1 100644 --- a/.claude/skills/purple-release/SKILL.md +++ b/.claude/skills/purple-release/SKILL.md @@ -41,7 +41,7 @@ After `1.0.0`, standard SemVer (breaking ⇒ MAJOR). - — consumers affected: orbit | canal-whatsapp | stenfar; migration: ### Features / Fixes / Backports -- (upstream EvolutionAPI/evolution-api#, ) when backported +- (upstream evolution-foundation/evolution-api#, ) when backported --- diff --git a/.claude/skills/purple-upstream-sync/SKILL.md b/.claude/skills/purple-upstream-sync/SKILL.md index e5433460bf..49c952911b 100644 --- a/.claude/skills/purple-upstream-sync/SKILL.md +++ b/.claude/skills/purple-upstream-sync/SKILL.md @@ -20,14 +20,13 @@ git remote -v # upstream must exist git fetch upstream --tags ``` -`upstream` currently points to `https://github.com/EvolutionAPI/evolution-api.git` ; the task -spec names `evolution-foundation/evolution-api` as canonical — confirm which URL is current -with the user before changing the remote. Never push to upstream. +`upstream` must point to `https://github.com/evolution-foundation/evolution-api.git` +(`evolution-foundation/evolution-api` is the old org name and only redirects). Never push to upstream. ## Procedure (one backport = one branch = one PR) 1. Identify the source: upstream PR number and commit sha(s). `git log upstream/main --oneline` - or `gh pr view --repo EvolutionAPI/evolution-api`. + or `gh pr view --repo evolution-foundation/evolution-api`. 2. Check the denylist below. If the commit touches any denylisted area, stop and ask. 3. Branch from `main`: `git switch -c backport/`. 4. Cherry-pick with provenance: @@ -37,7 +36,7 @@ with the user before changing the remote. Never push to upstream. hunk; drop unrelated upstream refactors. 6. Amend the message body (keep the `(cherry picked from commit ...)` trailer) with: ``` - Upstream: EvolutionAPI/evolution-api# () + Upstream: evolution-foundation/evolution-api# () Conflicts: ``` 7. Verify: `npx tsc --noEmit`, `npm run build`, `npm run lint:check`; manual run if it touches From 631b7aa9cfc165a7f91708d576b104f0662ebec2 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 14:17:07 -0300 Subject: [PATCH 06/96] docs(odd): record repo-agentic-setup evidence and follow-ups --- odd/tasks/repo-agentic-setup.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/odd/tasks/repo-agentic-setup.md b/odd/tasks/repo-agentic-setup.md index 0ec9a4d61c..041d41d489 100644 --- a/odd/tasks/repo-agentic-setup.md +++ b/odd/tasks/repo-agentic-setup.md @@ -34,10 +34,10 @@ orbit, canal-whatsapp (alien) and stenfar. ## Checklist -- [ ] T1 Core docs -- [ ] T2 Skills -- [ ] T3 Tooling/CI -- [ ] T4 Integrate and verify +- [x] T1 Core docs — 901c00df +- [x] T2 Skills — 453773b2, c2aa1dd5 +- [x] T3 Tooling/CI — f2718917 +- [x] T4 Integrate and verify — merge f81c78e0 ## Acceptance criteria @@ -48,4 +48,13 @@ orbit, canal-whatsapp (alien) and stenfar. ## Evidence -(commit ids recorded per task) +- `bash scripts/check-docs.sh` → `check-docs: OK` after integration. +- `npm run build` / `lint:check` not run: no `src/` changes and `node_modules` not installed. +- Engram mirror `odd/repo-agentic-setup/tasks` failed (server requires upgrade). + +## Follow-ups + +- canal-whatsapp calls `GET /group/findGroupByJid`, which does not exist in 2.3.7 (see purple-contract consumer matrix). +- Confirm whether `/instance/create` honors the `webhook` field (orbit relies on it; canal comment says it does not). +- Locate where stenfar registers its webhook. +- Decide whether to delete disabled upstream publish workflows and `check_code_quality.yml`; update `.husky/README.md`. From a1ccb1d545bc76d9750dce8d77919ceee8e1b8c9 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 14:21:49 -0300 Subject: [PATCH 07/96] docs(odd): record review outcome and follow-ups for repo-agentic-setup --- odd/tasks/repo-agentic-setup.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/odd/tasks/repo-agentic-setup.md b/odd/tasks/repo-agentic-setup.md index 041d41d489..5aced66564 100644 --- a/odd/tasks/repo-agentic-setup.md +++ b/odd/tasks/repo-agentic-setup.md @@ -58,3 +58,13 @@ orbit, canal-whatsapp (alien) and stenfar. - Confirm whether `/instance/create` honors the `webhook` field (orbit relies on it; canal comment says it does not). - Locate where stenfar registers its webhook. - Decide whether to delete disabled upstream publish workflows and `check_code_quality.yml`; update `.husky/README.md`. + +## Review + +Native review `review-38e4968ca23c1f55` (4 lenses, high risk tier): approved and acknowledged. +Non-blocking findings (follow-up work): + +- git-gate.sh: merge/rebase are no-ops; push scope fails open when no base ref; missing node_modules blocks every src commit; `-n`/`--no-verify` regex false positives. +- purple-release: stale workflow and `latest` tag references; CHANGELOG layout drift. +- purple-upstream-sync: upstream remote wording contradiction (lines 23-24). +- AGENTS.md:25: Prisma schema count mismatch. From b79b2430c366a1baa0ee5e35fbcdc19462a3a812 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 14:27:42 -0300 Subject: [PATCH 08/96] fix(tooling): address review findings in git-gate hook and skills git-gate: ignore flags inside quoted messages, check merge/rebase against HEAD, full check on push without base ref, explicit npm ci message when toolchain is missing. Align purple-release with GHCR workflow and CHANGELOG layout, fix upstream remote wording, list the three Prisma schemas. --- .claude/hooks/git-gate.sh | 57 +++++++++++++++----- .claude/skills/purple-release/SKILL.md | 16 +++--- .claude/skills/purple-upstream-sync/SKILL.md | 5 +- AGENTS.md | 2 +- odd/tasks/repo-agentic-setup.md | 1 + 5 files changed, 58 insertions(+), 23 deletions(-) diff --git a/.claude/hooks/git-gate.sh b/.claude/hooks/git-gate.sh index 6631da281e..79f4adb4e4 100755 --- a/.claude/hooks/git-gate.sh +++ b/.claude/hooks/git-gate.sh @@ -3,7 +3,9 @@ # Denies git commit/push/merge/rebase when: # - the command uses --no-verify (bypasses Husky hooks), or # - `npx tsc --noEmit` fails, or -# - ESLint fails on the staged (commit) or unpushed (push) .ts/.js files under src/. +# - ESLint fails on the staged (commit), unpushed (push) or uncommitted (merge/rebase) .ts/.js +# files under src/; push with no upstream/origin/main base checks all of src/, +# - or the toolchain (node_modules/.bin/tsc, eslint) is missing: run `npm ci`. # Emits a PreToolUse deny decision as JSON; otherwise exits 0 silently. set -o pipefail @@ -33,8 +35,17 @@ deny() { exit 0 } -# --no-verify anywhere, or the `git commit -n` short form. -if printf '%s' "$CMD" | grep -qE -- '--no-verify|git[[:space:]]+commit[^|;&]*[[:space:]]-n([[:space:]]|$)'; then +# Quoted strings (commit messages, echo text) are stripped before matching flags, so +# `git commit -m "fix -n parsing --no-verify"` is not mistaken for a bypass. +STRIPPED=$(printf '%s' "$CMD" | sed -E "s/\"([^\"\\\\]|\\\\.)*\"//g; s/'[^']*'//g") +if ! printf '%s' "$STRIPPED" | grep -qE "$GIT_WRITE"; then + exit 0 +fi + +# --no-verify on any git write, or `-n` (alone or in a short-flag cluster) on `git commit` only. +GIT_COMMIT='git([[:space:]]+-[^[:space:]]+)*[[:space:]]+commit' +if printf '%s' "$STRIPPED" | grep -qE -- '(^|[[:space:]])--no-verify([[:space:]=]|$)' \ + || printf '%s' "$STRIPPED" | grep -qE -- "${GIT_COMMIT}[^|;&]*[[:space:]]-[[:alpha:]]*n[[:alpha:]]*([[:space:]]|\$)"; then deny "Git gate: --no-verify is not allowed. Fix the failing Husky hooks instead of bypassing them." fi @@ -42,23 +53,41 @@ ROOT=${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)} [ -n "$ROOT" ] || exit 0 cd "$ROOT" || exit 0 -if printf '%s' "$CMD" | grep -qE 'git([[:space:]]+-[^[:space:]]+)*[[:space:]]+push'; then - UPSTREAM=$(git rev-parse --abbrev-ref '@{u}' 2>/dev/null) - if [ -n "$UPSTREAM" ]; then - FILES=$(git diff --name-only --diff-filter=d "$UPSTREAM"...HEAD 2>/dev/null) +# FULL=1 checks the whole project (tsc + eslint on src/) instead of the changed files only. +FULL=0 +if printf '%s' "$STRIPPED" | grep -qE 'git([[:space:]]+-[^[:space:]]+)*[[:space:]]+push'; then + BASE=$(git rev-parse --abbrev-ref '@{u}' 2>/dev/null) + if [ -z "$BASE" ] && git rev-parse --verify --quiet origin/main >/dev/null; then + BASE=origin/main + fi + if [ -n "$BASE" ]; then + FILES=$(git diff --name-only --diff-filter=d "$BASE"...HEAD 2>/dev/null) else - FILES=$(git diff --name-only --diff-filter=d origin/main...HEAD 2>/dev/null) + # No base ref to diff against: fail closed with a full check instead of passing. + FULL=1 fi -else +elif printf '%s' "$STRIPPED" | grep -qE "${GIT_COMMIT}([[:space:]]|\$)"; then FILES=$(git diff --cached --name-only --diff-filter=d 2>/dev/null) +else + # merge/rebase: the incoming commits already passed their own gates; check that the local + # working tree (staged + unstaged src/ changes vs HEAD) is not broken before rewriting history. + FILES=$(git diff HEAD --name-only --diff-filter=d 2>/dev/null) fi -CODE_FILES=$(printf '%s\n' "$FILES" | grep -E '^src/.*\.(ts|js)$' || true) -[ -n "$CODE_FILES" ] || exit 0 +if [ "$FULL" -eq 1 ]; then + CODE_FILES=src +else + CODE_FILES=$(printf '%s\n' "$FILES" | grep -E '^src/.*\.(ts|js)$' || true) + [ -n "$CODE_FILES" ] || exit 0 +fi + +if [ ! -x node_modules/.bin/tsc ] || [ ! -x node_modules/.bin/eslint ]; then + deny "Git gate: BLOCKED because the local toolchain is missing (node_modules/.bin/tsc or eslint not found). Run \`npm ci\` in ${ROOT} and retry." +fi FAILURES="" -TSC=$(npx --no tsc --noEmit 2>&1) +TSC=$(node_modules/.bin/tsc --noEmit 2>&1) if [ $? -ne 0 ]; then FAILURES="${FAILURES}--- TypeScript (tsc --noEmit) --- ${TSC} @@ -66,9 +95,9 @@ ${TSC} " fi -LINT=$(printf '%s\n' "$CODE_FILES" | xargs npx --no eslint 2>&1) +LINT=$(printf '%s\n' "$CODE_FILES" | xargs node_modules/.bin/eslint 2>&1) if [ $? -ne 0 ]; then - FAILURES="${FAILURES}--- ESLint (changed files) --- + FAILURES="${FAILURES}--- ESLint --- ${LINT} " diff --git a/.claude/skills/purple-release/SKILL.md b/.claude/skills/purple-release/SKILL.md index 8b7ea965b1..57d8ed50fb 100644 --- a/.claude/skills/purple-release/SKILL.md +++ b/.claude/skills/purple-release/SKILL.md @@ -16,9 +16,11 @@ description: > - Source of truth: `VERSION` file at repo root, single line `X.Y.Z`. First release `0.1.0`. - Git tag: `vX.Y.Z` (annotated), must equal `VERSION`. Upstream tags look like `2.3.7` (no `v`) — never create or move those. -- Image: `ghcr.io/purple-dice/evolution-in-purple:X.Y.Z` (plus `latest` only for the newest - release). The upstream workflows under `.github/workflows/publish_docker_image*.yml` push to - `evoapicloud/evolution-api`; the fork publish workflow is owned by tooling, not by this skill. +- Image: `.github/workflows/publish_docker_image.yml` runs on pushed `v*` tags and publishes + `ghcr.io/purple-dice/evolution-in-purple:X.Y.Z` and `:X.Y` (metadata-action semver patterns + `{{version}}` and `{{major}}.{{minor}}`). No `latest` tag is published. + `publish_docker_image_latest.yml` and `publish_docker_image_homolog.yml` are disabled + `workflow_dispatch` no-op stubs (upstream pushed to Docker Hub `evoapicloud/evolution-api`). ### Bump rules (pre-1.0) @@ -40,16 +42,16 @@ After `1.0.0`, standard SemVer (breaking ⇒ MAJOR). ### Breaking - — consumers affected: orbit | canal-whatsapp | stenfar; migration: -### Features / Fixes / Backports +### Features / Fixes / Backports / Chores - (upstream evolution-foundation/evolution-api#, ) when backported ---- +# Upstream Evolution API changelog # 2.3.7 (2025-12-05) ← upstream history, untouched below ``` -The upstream history (currently starting at `CHANGELOG.md:1` with `# 2.3.7 (2025-12-05)`) is -never edited. +New fork releases go at the top of the `# evolution-in-purple` section (newest first). Everything +below the `# Upstream Evolution API changelog` heading is upstream history and is never edited. ## Procedure diff --git a/.claude/skills/purple-upstream-sync/SKILL.md b/.claude/skills/purple-upstream-sync/SKILL.md index 49c952911b..912bc414af 100644 --- a/.claude/skills/purple-upstream-sync/SKILL.md +++ b/.claude/skills/purple-upstream-sync/SKILL.md @@ -17,11 +17,14 @@ rebase onto upstream; it imports selected commits only. ```bash git remote -v # upstream must exist +# if missing or pointing elsewhere (e.g. the old EvolutionAPI org): +git remote add upstream https://github.com/evolution-foundation/evolution-api.git \ + || git remote set-url upstream https://github.com/evolution-foundation/evolution-api.git git fetch upstream --tags ``` `upstream` must point to `https://github.com/evolution-foundation/evolution-api.git` -(`evolution-foundation/evolution-api` is the old org name and only redirects). Never push to upstream. +(`EvolutionAPI/evolution-api` is the old org name and only redirects). Never push to upstream. ## Procedure (one backport = one branch = one PR) diff --git a/AGENTS.md b/AGENTS.md index ae16157031..cf9e933298 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ and **stenfar**. Anything they consume is a public contract. | `src/api/integrations/event/` | Webhook, WebSocket, RabbitMQ, SQS, NATS, Kafka, Pusher | | `src/api/integrations/storage/` | S3 / MinIO | | `src/config/`, `src/cache/`, `src/utils/`, `src/exceptions/` | Env config, Redis/local cache, helpers, HTTP exceptions | -| `prisma/` | `postgresql-schema.prisma` / `mysql-schema.prisma` + provider-specific migration folders | +| `prisma/` | Three schemas (`postgresql-schema.prisma`, `mysql-schema.prisma`, `psql_bouncer-schema.prisma`) + `postgresql-migrations/` and `mysql-migrations/` | | `odd/` | Active feature docs (`odd/tasks/`), closed ones (`odd/archive/`), `odd/agent-protocol.md` | | `.claude/skills/purple-*` | Project skills (shared with Pi via `.pi/settings.json`) | | `VERSION`, `CHANGELOG.md` | Fork versioning, managed by the `purple-release` skill | diff --git a/odd/tasks/repo-agentic-setup.md b/odd/tasks/repo-agentic-setup.md index 5aced66564..7308fa37bc 100644 --- a/odd/tasks/repo-agentic-setup.md +++ b/odd/tasks/repo-agentic-setup.md @@ -38,6 +38,7 @@ orbit, canal-whatsapp (alien) and stenfar. - [x] T2 Skills — 453773b2, c2aa1dd5 - [x] T3 Tooling/CI — f2718917 - [x] T4 Integrate and verify — merge f81c78e0 +- [x] T5 Fix the 10 non-blocking review findings (tsc/eslint gate paths untested: no node_modules) ## Acceptance criteria From 7e53d8fe7aac6cac29b5cdfb1f32236383f7f358 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 14:39:38 -0300 Subject: [PATCH 09/96] fix(tooling): make git-gate fail closed on unparsed input and diff errors Falls back to node when jq is missing and blocks unparseable git writes. A failing git diff escalates to a full tsc + eslint check. Verified against the real toolchain after npm ci and prisma generate. --- .claude/hooks/git-gate.sh | 25 ++++++++++++++++++++----- odd/tasks/repo-agentic-setup.md | 11 +++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.claude/hooks/git-gate.sh b/.claude/hooks/git-gate.sh index 79f4adb4e4..3117f985bc 100755 --- a/.claude/hooks/git-gate.sh +++ b/.claude/hooks/git-gate.sh @@ -6,15 +6,29 @@ # - ESLint fails on the staged (commit), unpushed (push) or uncommitted (merge/rebase) .ts/.js # files under src/; push with no upstream/origin/main base checks all of src/, # - or the toolchain (node_modules/.bin/tsc, eslint) is missing: run `npm ci`. +# Fails closed: unparseable input that looks like a git write, or a failing git diff, is +# blocked or escalated to a full check instead of passing. # Emits a PreToolUse deny decision as JSON; otherwise exits 0 silently. set -o pipefail INPUT=$(cat) +# Parse the tool input with jq, falling back to node (always present in this repo). If neither +# can parse it, fail closed: an unparsed command must never be allowed through silently. if command -v jq >/dev/null 2>&1; then - CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) + CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) || CMD_PARSE_FAILED=1 +elif command -v node >/dev/null 2>&1; then + CMD=$(printf '%s' "$INPUT" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{process.stdout.write(String((JSON.parse(s).tool_input||{}).command||""))})' 2>/dev/null) || CMD_PARSE_FAILED=1 else - CMD=$INPUT + CMD_PARSE_FAILED=1 +fi +if [ -n "$CMD_PARSE_FAILED" ]; then + # Only block unparsed input that looks like a git write; other commands are not gated. + if printf '%s' "$INPUT" | grep -qE 'git[^a-z].*(commit|push|merge|rebase)'; then + printf '%s\n' "Git gate: could not parse the tool input (install jq or node). Git write blocked." >&2 + exit 2 + fi + exit 0 fi GIT_WRITE='(^|[[:space:]&|;(])git([[:space:]]+-[^[:space:]]+)*[[:space:]]+(commit|push|merge|rebase)([[:space:]]|$)' @@ -61,17 +75,18 @@ if printf '%s' "$STRIPPED" | grep -qE 'git([[:space:]]+-[^[:space:]]+)*[[:space: BASE=origin/main fi if [ -n "$BASE" ]; then - FILES=$(git diff --name-only --diff-filter=d "$BASE"...HEAD 2>/dev/null) + # A failing diff (e.g. no merge base) falls back to a full check instead of passing. + FILES=$(git diff --name-only --diff-filter=d "$BASE"...HEAD 2>/dev/null) || FULL=1 else # No base ref to diff against: fail closed with a full check instead of passing. FULL=1 fi elif printf '%s' "$STRIPPED" | grep -qE "${GIT_COMMIT}([[:space:]]|\$)"; then - FILES=$(git diff --cached --name-only --diff-filter=d 2>/dev/null) + FILES=$(git diff --cached --name-only --diff-filter=d 2>/dev/null) || FULL=1 else # merge/rebase: the incoming commits already passed their own gates; check that the local # working tree (staged + unstaged src/ changes vs HEAD) is not broken before rewriting history. - FILES=$(git diff HEAD --name-only --diff-filter=d 2>/dev/null) + FILES=$(git diff HEAD --name-only --diff-filter=d 2>/dev/null) || FULL=1 fi if [ "$FULL" -eq 1 ]; then diff --git a/odd/tasks/repo-agentic-setup.md b/odd/tasks/repo-agentic-setup.md index 7308fa37bc..f9127794ce 100644 --- a/odd/tasks/repo-agentic-setup.md +++ b/odd/tasks/repo-agentic-setup.md @@ -69,3 +69,14 @@ Non-blocking findings (follow-up work): - purple-release: stale workflow and `latest` tag references; CHANGELOG layout drift. - purple-upstream-sync: upstream remote wording contradiction (lines 23-24). - AGENTS.md:25: Prisma schema count mismatch. + +## T6 Close git-gate fail-open paths (second review) + +- [x] No jq: parse with node; unparseable git-write input is blocked (exit 2); other commands pass. +- [x] Failing `git diff` (push/commit/merge) escalates to a full check instead of passing. +- [x] Real toolchain test after `npm ci` + `DATABASE_PROVIDER=postgresql npm run db:generate`: + type error in staged src file → denied (TS2322); clean src file → allowed; `git status` without jq/node → allowed. + +Remaining non-blocking findings from review `review-af4cee1a866a800a` (follow-ups): deny message scope, +FULL comment, full eslint on `src` dir, Husky pre-commit checks worktree not index, alternative bypasses. +Note: `tsc` requires the Prisma client; run `npm run db:generate` after `npm ci` (add to `make install`). From bff07d67fb37ce2b92a37818bfcf8aa0cd46d3a1 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 15:19:24 -0300 Subject: [PATCH 10/96] chore(odd): keep feature task docs local odd/tasks/ and odd/archive/ are gitignored because the repo is public. odd/agent-protocol.md stays tracked. AGENTS.md documents that unfixed security findings never go into commits, PRs or issues. --- .gitignore | 4 ++ AGENTS.md | 4 +- odd/archive/.gitkeep | 0 odd/tasks/repo-agentic-setup.md | 82 --------------------------------- 4 files changed, 7 insertions(+), 83 deletions(-) delete mode 100644 odd/archive/.gitkeep delete mode 100644 odd/tasks/repo-agentic-setup.md diff --git a/.gitignore b/.gitignore index 768d8afa41..17d3d17819 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,7 @@ lerna-debug.log* .tool-versions /prisma/migrations/* + +# ODD feature docs are local working notes (public repo) +odd/tasks/ +odd/archive/ diff --git a/AGENTS.md b/AGENTS.md index cf9e933298..fe23554fa5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,7 +94,9 @@ Read the matching skill BEFORE writing code. Skills live in `.claude/skills/.md` (objective, scope, checklist, acceptance - criteria, evidence with commit ids). Closed features move to `odd/archive/`. + criteria, evidence with commit ids). Closed features move to `odd/archive/`. Both folders are gitignored: + this repo is public, so feature docs stay local and unfixed security findings never go into commits, + PR descriptions or issues. - **Operational rules** for agents and parents: `odd/agent-protocol.md`. - **Contract first:** if a change touches anything a consumer reads, the first task states the contract change (skill `purple-contract`). diff --git a/odd/archive/.gitkeep b/odd/archive/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/odd/tasks/repo-agentic-setup.md b/odd/tasks/repo-agentic-setup.md deleted file mode 100644 index f9127794ce..0000000000 --- a/odd/tasks/repo-agentic-setup.md +++ /dev/null @@ -1,82 +0,0 @@ -# Feature: repo-agentic-setup — agentic development setup for evolution-in-purple - -## Objective - -Organize `purple-dice/evolution-in-purple` (fork of Evolution API 2.3.7) for -agentic development following the orbit conventions, adapted to a single -TypeScript repo. This engine will replace Evolution API in three consumers: -orbit, canal-whatsapp (alien) and stenfar. - -## Scope - -- Canonical `AGENTS.md` (index + hard rules), `CLAUDE.md` as `@AGENTS.md` pointer. -- `odd/agent-protocol.md`, `odd/tasks/`, `odd/archive/`. -- Project skills in `.claude/skills/purple-*`, shared with Pi via `.pi/settings.json`. -- Automated gates: prettier hook, git-gate (relative paths), check-docs, - integrated with the existing Husky hooks (not a second hook system). -- CI: PR checks (tsc, build, lint, check-docs); image publish adapted to the fork. -- Own `VERSION` + `CHANGELOG.md` section, separated from upstream history. - -## Non-goals - -- No source (`src/`) changes. No upstream backports (separate feature). -- No OpenSpec, no gga (can be added later pointing at `AGENTS.md`). -- No removal of telemetry/license yet (separate feature). - -## Parallel plan (isolated worktrees, disjoint surfaces, parent commits) - -| Task | Surface | Worktree branch | -|---|---|---| -| T1 Core docs | `AGENTS.md`, `CLAUDE.md`, `odd/agent-protocol.md`, `odd/archive/.gitkeep` | `agentic/t1-docs` | -| T2 Skills | `.claude/skills/purple-*/**`, `.pi/settings.json`, `.pi/subagents.json` | `agentic/t2-skills` | -| T3 Tooling/CI | `.claude/settings.json`, `.claude/hooks/**`, `.husky/**`, `Makefile`, `scripts/check-docs.sh`, `.github/workflows/**`, `VERSION`, `CHANGELOG.md` | `agentic/t3-tooling` | -| T4 Integrate | merge T1–T3 into `chore/agentic-setup`, cross-check links, run checks | `chore/agentic-setup` | - -## Checklist - -- [x] T1 Core docs — 901c00df -- [x] T2 Skills — 453773b2, c2aa1dd5 -- [x] T3 Tooling/CI — f2718917 -- [x] T4 Integrate and verify — merge f81c78e0 -- [x] T5 Fix the 10 non-blocking review findings (tsc/eslint gate paths untested: no node_modules) - -## Acceptance criteria - -- `AGENTS.md` is the single source; `CLAUDE.md` only points to it. -- Every skill has valid frontmatter with a `Trigger:` line and is listed in `AGENTS.md`. -- `make check-docs` passes; `npm run build` and `npm run lint:check` unaffected. -- Hooks use repo-relative paths only. - -## Evidence - -- `bash scripts/check-docs.sh` → `check-docs: OK` after integration. -- `npm run build` / `lint:check` not run: no `src/` changes and `node_modules` not installed. -- Engram mirror `odd/repo-agentic-setup/tasks` failed (server requires upgrade). - -## Follow-ups - -- canal-whatsapp calls `GET /group/findGroupByJid`, which does not exist in 2.3.7 (see purple-contract consumer matrix). -- Confirm whether `/instance/create` honors the `webhook` field (orbit relies on it; canal comment says it does not). -- Locate where stenfar registers its webhook. -- Decide whether to delete disabled upstream publish workflows and `check_code_quality.yml`; update `.husky/README.md`. - -## Review - -Native review `review-38e4968ca23c1f55` (4 lenses, high risk tier): approved and acknowledged. -Non-blocking findings (follow-up work): - -- git-gate.sh: merge/rebase are no-ops; push scope fails open when no base ref; missing node_modules blocks every src commit; `-n`/`--no-verify` regex false positives. -- purple-release: stale workflow and `latest` tag references; CHANGELOG layout drift. -- purple-upstream-sync: upstream remote wording contradiction (lines 23-24). -- AGENTS.md:25: Prisma schema count mismatch. - -## T6 Close git-gate fail-open paths (second review) - -- [x] No jq: parse with node; unparseable git-write input is blocked (exit 2); other commands pass. -- [x] Failing `git diff` (push/commit/merge) escalates to a full check instead of passing. -- [x] Real toolchain test after `npm ci` + `DATABASE_PROVIDER=postgresql npm run db:generate`: - type error in staged src file → denied (TS2322); clean src file → allowed; `git status` without jq/node → allowed. - -Remaining non-blocking findings from review `review-af4cee1a866a800a` (follow-ups): deny message scope, -FULL comment, full eslint on `src` dir, Husky pre-commit checks worktree not index, alternative bypasses. -Note: `tsc` requires the Prisma client; run `npm run db:generate` after `npm ci` (add to `make install`). From 5065c789208343d724995270038431e249ad2357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Varela?= <114447249+MatiasxVarela@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:29:39 -0300 Subject: [PATCH 11/96] fix(auth): prevent cross-instance override via query on instance routes (#2) The auth guard authorizes req.params.instanceName, but dataValidate merged the query into the same instance object, so ?instanceName=OTHER ran against another tenant's instance (CWE-639). Instance routes now restore the authorized name from the path param and drop instanceId; the create body is merged only on the exact /instance/create route. fetchInstances and instance creation keep their behavior. Adds AGENTS.md hard rule (e): no code comments. Upstream: evolution-foundation/evolution-api#2549 (7a55a2bf), adapted. --- AGENTS.md | 8 +++++++- src/api/abstract/abstract.router.ts | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fe23554fa5..c4d2ffabae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,6 +77,12 @@ Preserve `@lid` addressing end to end. Never strip a JID with `split('@')[0]` (o outbound target — that turns a `@lid` into a bogus phone number and the message goes nowhere or to the wrong user. Normalize only through the existing JID helpers. Detail: skill `purple-baileys`. +### (e) No code comments + +Code must be self-declarative: never add comments to source code. Express intent through names — extract +a well-named function, constant or variable instead of explaining a line. Rationale belongs in the commit +body and the PR description. Existing upstream comments may stay; do not add new ones. + ## Skills per task Read the matching skill BEFORE writing code. Skills live in `.claude/skills//SKILL.md`. @@ -196,7 +202,7 @@ this.router.post(this.routerPath('create'), ...guards, async (req, res) => { - Files `feature.kind.ts` (e.g. `whatsapp.baileys.service.ts`); classes `PascalCase`, functions/variables `camelCase`, constants `UPPER_SNAKE_CASE`. - Logging via the project `Logger` (Pino); never log API keys or tokens. -- Code, comments, API responses and docs in English. +- Code, API responses and docs in English. No code comments (hard rule (e)). ### Environment diff --git a/src/api/abstract/abstract.router.ts b/src/api/abstract/abstract.router.ts index e8449a8c8a..6d556c9b73 100644 --- a/src/api/abstract/abstract.router.ts +++ b/src/api/abstract/abstract.router.ts @@ -17,6 +17,14 @@ type DataValidate = { const logger = new Logger('Validate'); +const isInstanceCreateRoute = (request: Request) => + request.baseUrl.endsWith('/instance') && request.route?.path === '/create'; + +const restoreAuthorizedInstance = (instance: InstanceDto, authorizedInstanceName: string) => { + instance.instanceName = authorizedInstanceName; + delete instance.instanceId; +}; + export abstract class RouterBroker { constructor() {} public routerPath(path: string, param = true) { @@ -32,15 +40,20 @@ export abstract class RouterBroker { const ref = new ClassRef(); const body = request.body; const instance = request.params as unknown as InstanceDto; + const authorizedInstanceName = request.params?.instanceName; if (request?.query && Object.keys(request.query).length > 0) { Object.assign(instance, request.query); } - if (request.originalUrl.includes('/instance/create')) { + if (isInstanceCreateRoute(request)) { Object.assign(instance, body); } + if (authorizedInstanceName) { + restoreAuthorizedInstance(instance, authorizedInstanceName); + } + Object.assign(ref, body); const v = schema ? validate(ref, schema) : { valid: true, errors: [] }; From 1db78d53b76bd1a80f170fd330d2af50c774b4cc Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 15:42:38 -0300 Subject: [PATCH 12/96] fix(deps): patch baileys rc14 to restore WhatsApp device pairing Since 2026-07-28 WhatsApp sends a companion_reg_refresh notification during device registration. Baileys drops it, so the QR keeps advertising a retired adv secret and pairing fails intermittently. Bumps baileys 7.0.0-rc.9 to 7.0.0-rc14 (also fixes CVE-2026-48063, patched in rc12) and vendors WhiskeySockets/Baileys#2765 (companion_reg_refresh handling, skipped on registered sessions) and #2602 (link_code_companion_reg guard) through patch-package. The Dockerfile copies patches/ before npm ci. (cherry picked from commit 5cdf32409a7c10066dd6c5b15af7a3ef7c5aac94) Upstream: evolution-foundation/evolution-api#2759 (5cdf3240), unmerged --- Dockerfile | 1 + package-lock.json | 1036 ++++++++++++++++-------------- package.json | 8 +- patches/baileys+7.0.0-rc14.patch | 220 +++++++ 4 files changed, 777 insertions(+), 488 deletions(-) create mode 100644 patches/baileys+7.0.0-rc14.patch diff --git a/Dockerfile b/Dockerfile index 24c4e3bc7f..df2a043827 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,7 @@ LABEL contact="contato@evolution-api.com" WORKDIR /evolution COPY ./package*.json ./ +COPY ./patches ./patches COPY ./tsconfig.json ./ COPY ./tsup.config.ts ./ diff --git a/package-lock.json b/package-lock.json index c45e8fef38..22b9f87381 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "evolution-api", "version": "2.3.7", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@adiwajshing/keyed-db": "^0.2.4", @@ -21,7 +22,7 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "7.0.0-rc.9", + "baileys": "7.0.0-rc14", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", @@ -94,6 +95,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "husky": "^9.1.7", "lint-staged": "^16.1.6", + "patch-package": "^8.0.1", "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", @@ -829,9 +831,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 +2374,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 +2404,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 +2419,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 +2443,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 +2458,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 +2472,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 +2486,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 +2500,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 +2514,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 +2540,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 +2556,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 +2573,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 +2589,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 +2604,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 +2618,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 +2644,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 +2657,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 +2678,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 +2691,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 +2712,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 +2725,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 +2739,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 +2756,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 +2773,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 +2785,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 +3580,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 +3606,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 +3619,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 +4753,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 +4862,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", @@ -5365,6 +5326,13 @@ "url": "https://github.com/sponsors/eshaz" } }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/@zxing/text-encoding": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", @@ -5814,21 +5782,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-rc14", + "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc14.tgz", + "integrity": "sha512-pewtrljhWx5JTUBvvkXZz1fL3JPiwzjBsnhx/DWf2LWBx1cZNH7J/sF22xDehba5mySWUQU4a1Pwt7lWARUxaA==", "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 +5805,7 @@ }, "peerDependencies": { "audio-decode": "^2.1.3", - "jimp": "^1.6.0", + "jimp": "^1.6.1", "link-preview-js": "^3.0.0", "sharp": "*" }, @@ -5877,6 +5846,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", @@ -6295,6 +6265,22 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -8336,15 +8322,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 +8606,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" @@ -8736,6 +8714,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/findup-sync": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", @@ -9994,6 +9982,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -10337,6 +10341,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -10351,38 +10368,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" @@ -10459,6 +10476,26 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -10492,6 +10529,16 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -10589,6 +10636,16 @@ "@keyv/serialize": "^1.1.1" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -10610,51 +10667,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 +11251,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 +11679,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.16.1", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.16.1.tgz", + "integrity": "sha512-uR/mHK6eyfl8h3jVCobF5b38Mfg1IDJMiBxIVbmm5F+G1OXEFcUpQHveO84tJBAqvY93GCRY2R4kDyx7s06Rug==", "funding": [ { "type": "github", @@ -11671,80 +11694,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.1.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.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "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": { @@ -12126,6 +12101,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openai": { "version": "4.104.0", "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", @@ -12528,6 +12520,137 @@ "node": ">= 0.8" } }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/patch-package/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/patch-package/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/patch-package/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/path-exists": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", @@ -12576,19 +12699,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 +13067,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 +13084,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.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", "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 +13511,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 +14220,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" @@ -14194,6 +14238,16 @@ "url": "https://github.com/sponsors/eshaz" } }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/slice-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", @@ -14640,16 +14694,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 +14967,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", @@ -15722,9 +15776,9 @@ "license": "MIT" }, "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.6.0.tgz", + "integrity": "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==", "license": "MIT", "engines": { "node": ">=18" @@ -15944,6 +15998,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 +16124,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..63c88b78ca 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "db:studio": "node runWithProvider.js \"npx prisma studio --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", "db:migrate:dev": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate dev --schema ./prisma/DATABASE_PROVIDER-schema.prisma && cp -r ./prisma/migrations/* ./prisma/DATABASE_PROVIDER-migrations\"", "db:migrate:dev:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate dev --schema prisma\\DATABASE_PROVIDER-schema.prisma\"", - "prepare": "husky" + "prepare": "husky", + "postinstall": "patch-package" }, "repository": { "type": "git", @@ -77,7 +78,7 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "7.0.0-rc.9", + "baileys": "7.0.0-rc14", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", @@ -153,6 +154,7 @@ "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", - "typescript": "^5.7.2" + "typescript": "^5.7.2", + "patch-package": "^8.0.1" } } diff --git a/patches/baileys+7.0.0-rc14.patch b/patches/baileys+7.0.0-rc14.patch new file mode 100644 index 0000000000..fe1884e625 --- /dev/null +++ b/patches/baileys+7.0.0-rc14.patch @@ -0,0 +1,220 @@ +diff --git a/node_modules/baileys/lib/Socket/messages-recv.js b/node_modules/baileys/lib/Socket/messages-recv.js +index f2003ac..1f72102 100644 +--- a/node_modules/baileys/lib/Socket/messages-recv.js ++++ b/node_modules/baileys/lib/Socket/messages-recv.js +@@ -880,6 +880,7 @@ export const makeMessagesRecvSocket = (config) => { + break; + case 'link_code_companion_reg': + const linkCodeCompanionReg = getBinaryNodeChild(node, 'link_code_companion_reg'); ++ if (!getBinaryNodeChildBuffer(linkCodeCompanionReg, 'primary_identity_pub')) { break; } + const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_ref')); + const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'primary_identity_pub')); + const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_wrapped_primary_ephemeral_pub')); +diff --git a/node_modules/baileys/lib/Socket/socket.js b/node_modules/baileys/lib/Socket/socket.js +index ae34a8f..944188d 100644 +--- a/node_modules/baileys/lib/Socket/socket.js ++++ b/node_modules/baileys/lib/Socket/socket.js +@@ -6,7 +6,7 @@ import { proto } from '../../WAProto/index.js'; + import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, NOISE_WA_HEADER, PROCESSABLE_HISTORY_TYPES, TimeMs, UPLOAD_TIMEOUT } from '../Defaults/index.js'; + import { QueryIds, ReachoutTimelockEnforcementType } from '../Types/index.js'; + import { DisconnectReason, XWAPaths } from '../Types/index.js'; +-import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, buildPairingQRData, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getCompanionPlatformId, getErrorCodeFromStreamError, getNextPreKeysNode, makeEventBuffer, makeNoiseHandler, promiseTimeout, signedKeyPair, xmppSignedPreKey } from '../Utils/index.js'; ++import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, buildPairingQRData, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getCompanionPlatformId, getErrorCodeFromStreamError, getNextPreKeysNode, handleCompanionRegRefresh, makeEventBuffer, makeNoiseHandler, makePairingQRRenderer, promiseTimeout, signedKeyPair, xmppSignedPreKey } from '../Utils/index.js'; + import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js'; + import { BinaryInfo } from '../WAM/BinaryInfo.js'; + import { USyncQuery, USyncUser } from '../WAUSync/index.js'; +@@ -690,6 +690,9 @@ export const makeSocket = (config) => { + ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed }))); + // the server terminated the connection + ws.on('CB:xmlstreamend', () => void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed }))); ++ // Re-render the QR currently on screen. Set while a pairing QR flow is ++ // live on this connection, undefined otherwise. ++ let refreshPairingQR; + // QR gen + ws.on('CB:iq,type:set,pair-device', async (stanza) => { + const iq = { +@@ -705,25 +708,40 @@ export const makeSocket = (config) => { + const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref'); + const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64'); + const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64'); +- const advB64 = creds.advSecretKey; ++ const renderer = makePairingQRRenderer(refNodes.map(refNode => refNode.content.toString('utf-8')), ++ // creds.advSecretKey is read per render rather than captured once: ++ // a companion_reg_refresh rotates it mid-flow. ++ ref => ev.emit('connection.update', { ++ qr: buildPairingQRData(ref, noiseKeyB64, identityKeyB64, creds.advSecretKey, browser) ++ })); ++ refreshPairingQR = () => void renderer.refresh(); + let qrMs = qrTimeout || 60000; // time to let a QR live + const genPairQR = () => { + if (!ws.isOpen) { + return; + } +- const refNode = refNodes.shift(); +- if (!refNode) { ++ if (!renderer.next()) { + void end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut })); + return; + } +- const ref = refNode.content.toString('utf-8'); +- const qr = buildPairingQRData(ref, noiseKeyB64, identityKeyB64, advB64, browser); +- ev.emit('connection.update', { qr }); + qrTimer = setTimeout(genPairQR, qrMs); + qrMs = qrTimeout || 20000; // shorter subsequent qrs + }; + genPairQR(); + }); ++ // the server retiring an unpaired companion's registration material ++ ws.on('CB:notification,type:companion_reg_refresh', (node) => { ++ handleCompanionRegRefresh(node, { ++ creds, ++ emitCredsUpdate: update => ev.emit('creds.update', update), ++ // Deliberately re-renders the ref already on screen and leaves ++ // qrTimer alone: that ref has not expired, only the secret it ++ // advertises changed. Spending a ref here would drain the pool the ++ // server allotted and end the flow with 'QR refs attempts ended'. ++ refreshQR: () => refreshPairingQR?.(), ++ logger ++ }); ++ }); + // device paired for the first time + // if device pairs successfully, the server asks to restart the connection + ws.on('CB:iq,,pair-success', async (stanza) => { +diff --git a/node_modules/baileys/lib/Utils/companion-reg-client-utils.d.ts b/node_modules/baileys/lib/Utils/companion-reg-client-utils.d.ts +index a79a52c..3bc161c 100644 +--- a/node_modules/baileys/lib/Utils/companion-reg-client-utils.d.ts ++++ b/node_modules/baileys/lib/Utils/companion-reg-client-utils.d.ts +@@ -1,4 +1,6 @@ +-import type { WABrowserDescription } from '../Types/index.js'; ++import type { AuthenticationCreds, WABrowserDescription } from '../Types/index.js'; ++import type { BinaryNode } from '../WABinary/index.js'; ++import type { ILogger } from './logger.js'; + export declare enum CompanionWebClientType { + UNKNOWN = 0, + CHROME = 1, +@@ -14,4 +16,40 @@ export declare enum CompanionWebClientType { + export declare const getCompanionWebClientType: ([os, browserName]: WABrowserDescription) => CompanionWebClientType; + export declare const getCompanionPlatformId: (browser: WABrowserDescription) => string; + export declare const buildPairingQRData: (ref: string, noiseKeyB64: string, identityKeyB64: string, advB64: string, browser: WABrowserDescription) => string; ++export type PairingQRRenderer = { ++ /** Render the next ref's QR. False once the server's allotment is spent. */ ++ next(): boolean; ++ /** Re-render the QR on screen. Consumes no ref; false if none is shown yet. */ ++ refresh(): boolean; ++}; ++/** ++ * Holds the ref currently on screen so it can be re-rendered. ++ * ++ * `render` is called with the ref rather than a finished payload so the caller ++ * can read the adv secret at render time: a `companion_reg_refresh` rotates it ++ * mid-flow, and every QR emitted afterwards has to advertise the new value. ++ */ ++export declare const makePairingQRRenderer: (refs: string[], render: (ref: string) => void) => PairingQRRenderer; ++export type CompanionRegRefreshContext = { ++ creds: AuthenticationCreds; ++ emitCredsUpdate: (update: Partial) => void; ++ refreshQR: () => void; ++ logger: ILogger; ++}; ++export type CompanionRegRefreshOutcome = 'rotated' | 'ignored_malformed' | 'ignored_registered'; ++/** ++ * `` - the server retiring an ++ * unpaired companion's registration material. ++ * ++ * WA Web accepts the stanza with either a `companion_reg_refresh` or a ++ * `pair-device-rotate-qr` child, rejects it when neither is present, and ++ * answers by regenerating the adv secret key. That key is a quarter of what ++ * the pairing QR advertises, so a client that only acks keeps offering a QR ++ * built on a secret the server has already retired: the phone scans it, ++ * reports a failed link, and no pair-success ever arrives. ++ * ++ * The ack itself is unchanged - the generic notification path already sends ++ * it - so this only adds the rotation and the re-render. ++ */ ++export declare const handleCompanionRegRefresh: (node: BinaryNode, { creds, emitCredsUpdate, refreshQR, logger }: CompanionRegRefreshContext) => CompanionRegRefreshOutcome; + //# sourceMappingURL=companion-reg-client-utils.d.ts.map +\ No newline at end of file +diff --git a/node_modules/baileys/lib/Utils/companion-reg-client-utils.js b/node_modules/baileys/lib/Utils/companion-reg-client-utils.js +index f5edebd..5021c8f 100644 +--- a/node_modules/baileys/lib/Utils/companion-reg-client-utils.js ++++ b/node_modules/baileys/lib/Utils/companion-reg-client-utils.js +@@ -1,3 +1,5 @@ ++import { randomBytes } from 'crypto'; ++import { getBinaryNodeChild } from '../WABinary/index.js'; + export var CompanionWebClientType; + (function (CompanionWebClientType) { + CompanionWebClientType[CompanionWebClientType["UNKNOWN"] = 0] = "UNKNOWN"; +@@ -32,4 +34,72 @@ export const buildPairingQRData = (ref, noiseKeyB64, identityKeyB64, advB64, bro + return ('https://wa.me/settings/linked_devices#' + + [ref, noiseKeyB64, identityKeyB64, advB64, getCompanionPlatformId(browser)].join(',')); + }; ++/** ++ * Holds the ref currently on screen so it can be re-rendered. ++ * ++ * `render` is called with the ref rather than a finished payload so the caller ++ * can read the adv secret at render time: a `companion_reg_refresh` rotates it ++ * mid-flow, and every QR emitted afterwards has to advertise the new value. ++ */ ++export const makePairingQRRenderer = (refs, render) => { ++ let index = 0; ++ let current; ++ return { ++ next() { ++ const ref = refs[index]; ++ if (ref === undefined) { ++ return false; ++ } ++ index += 1; ++ current = ref; ++ render(ref); ++ return true; ++ }, ++ refresh() { ++ if (current === undefined) { ++ return false; ++ } ++ render(current); ++ return true; ++ } ++ }; ++}; ++/** The two children WA Web's parser accepts on this notification. */ ++const COMPANION_REG_REFRESH_CHILDREN = ['companion_reg_refresh', 'pair-device-rotate-qr']; ++/** ++ * `` - the server retiring an ++ * unpaired companion's registration material. ++ * ++ * WA Web accepts the stanza with either a `companion_reg_refresh` or a ++ * `pair-device-rotate-qr` child, rejects it when neither is present, and ++ * answers by regenerating the adv secret key. That key is a quarter of what ++ * the pairing QR advertises, so a client that only acks keeps offering a QR ++ * built on a secret the server has already retired: the phone scans it, ++ * reports a failed link, and no pair-success ever arrives. ++ * ++ * The ack itself is unchanged - the generic notification path already sends ++ * it - so this only adds the rotation and the re-render. ++ */ ++export const handleCompanionRegRefresh = (node, { creds, emitCredsUpdate, refreshQR, logger }) => { ++ if (!COMPANION_REG_REFRESH_CHILDREN.some(tag => getBinaryNodeChild(node, tag))) { ++ logger.warn({ node }, 'companion_reg_refresh carries neither expected child; ignoring'); ++ return 'ignored_malformed'; ++ } ++ // WA Web rotates unconditionally; a registered session is the one case ++ // where that is wrong here. `creds.me` is set by pair-success and by ++ // requestPairingCode, and in both cases the adv secret is what a completed ++ // or pending pairing is verified against - re-minting it would break the ++ // session rather than refresh a pending registration. ++ if (creds.me) { ++ logger.debug({ id: node.attrs.id }, 'companion_reg_refresh on a registered session; keeping the adv secret'); ++ return 'ignored_registered'; ++ } ++ // Same construction as initAuthCreds and as WA Web's generateADVSecretKey: ++ // 32 CSPRNG bytes, base64. ++ creds.advSecretKey = randomBytes(32).toString('base64'); ++ emitCredsUpdate({ advSecretKey: creds.advSecretKey }); ++ logger.info({ id: node.attrs.id }, 'rotated the adv secret the server asked to retire; re-rendering the pairing QR'); ++ refreshQR(); ++ return 'rotated'; ++}; + //# sourceMappingURL=companion-reg-client-utils.js.map +\ No newline at end of file From 1080622ad50e8602d086742746ef6e1dd3953c37 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 17:19:17 -0300 Subject: [PATCH 13/96] chore(deps): drop comments from the vendored baileys patch The vendored WhiskeySockets/Baileys#2765 and #2602 code now carries no comments, per the no-code-comments rule. Behavior is unchanged: the patch applies on npm ci and the patched modules import cleanly. --- patches/baileys+7.0.0-rc14.patch | 69 ++------------------------------ 1 file changed, 4 insertions(+), 65 deletions(-) diff --git a/patches/baileys+7.0.0-rc14.patch b/patches/baileys+7.0.0-rc14.patch index fe1884e625..480630fa3f 100644 --- a/patches/baileys+7.0.0-rc14.patch +++ b/patches/baileys+7.0.0-rc14.patch @@ -23,24 +23,20 @@ index ae34a8f..944188d 100644 import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js'; import { BinaryInfo } from '../WAM/BinaryInfo.js'; import { USyncQuery, USyncUser } from '../WAUSync/index.js'; -@@ -690,6 +690,9 @@ export const makeSocket = (config) => { +@@ -690,6 +690,7 @@ export const makeSocket = (config) => { ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed }))); // the server terminated the connection ws.on('CB:xmlstreamend', () => void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed }))); -+ // Re-render the QR currently on screen. Set while a pairing QR flow is -+ // live on this connection, undefined otherwise. + let refreshPairingQR; // QR gen ws.on('CB:iq,type:set,pair-device', async (stanza) => { const iq = { -@@ -705,25 +708,40 @@ export const makeSocket = (config) => { +@@ -705,25 +706,33 @@ export const makeSocket = (config) => { const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref'); const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64'); const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64'); - const advB64 = creds.advSecretKey; + const renderer = makePairingQRRenderer(refNodes.map(refNode => refNode.content.toString('utf-8')), -+ // creds.advSecretKey is read per render rather than captured once: -+ // a companion_reg_refresh rotates it mid-flow. + ref => ev.emit('connection.update', { + qr: buildPairingQRData(ref, noiseKeyB64, identityKeyB64, creds.advSecretKey, browser) + })); @@ -64,15 +60,10 @@ index ae34a8f..944188d 100644 }; genPairQR(); }); -+ // the server retiring an unpaired companion's registration material + ws.on('CB:notification,type:companion_reg_refresh', (node) => { + handleCompanionRegRefresh(node, { + creds, + emitCredsUpdate: update => ev.emit('creds.update', update), -+ // Deliberately re-renders the ref already on screen and leaves -+ // qrTimer alone: that ref has not expired, only the secret it -+ // advertises changed. Spending a ref here would drain the pool the -+ // server allotted and end the flow with 'QR refs attempts ended'. + refreshQR: () => refreshPairingQR?.(), + logger + }); @@ -92,23 +83,14 @@ index a79a52c..3bc161c 100644 export declare enum CompanionWebClientType { UNKNOWN = 0, CHROME = 1, -@@ -14,4 +16,40 @@ export declare enum CompanionWebClientType { +@@ -14,4 +16,17 @@ export declare enum CompanionWebClientType { export declare const getCompanionWebClientType: ([os, browserName]: WABrowserDescription) => CompanionWebClientType; export declare const getCompanionPlatformId: (browser: WABrowserDescription) => string; export declare const buildPairingQRData: (ref: string, noiseKeyB64: string, identityKeyB64: string, advB64: string, browser: WABrowserDescription) => string; +export type PairingQRRenderer = { -+ /** Render the next ref's QR. False once the server's allotment is spent. */ + next(): boolean; -+ /** Re-render the QR on screen. Consumes no ref; false if none is shown yet. */ + refresh(): boolean; +}; -+/** -+ * Holds the ref currently on screen so it can be re-rendered. -+ * -+ * `render` is called with the ref rather than a finished payload so the caller -+ * can read the adv secret at render time: a `companion_reg_refresh` rotates it -+ * mid-flow, and every QR emitted afterwards has to advertise the new value. -+ */ +export declare const makePairingQRRenderer: (refs: string[], render: (ref: string) => void) => PairingQRRenderer; +export type CompanionRegRefreshContext = { + creds: AuthenticationCreds; @@ -117,20 +99,6 @@ index a79a52c..3bc161c 100644 + logger: ILogger; +}; +export type CompanionRegRefreshOutcome = 'rotated' | 'ignored_malformed' | 'ignored_registered'; -+/** -+ * `` - the server retiring an -+ * unpaired companion's registration material. -+ * -+ * WA Web accepts the stanza with either a `companion_reg_refresh` or a -+ * `pair-device-rotate-qr` child, rejects it when neither is present, and -+ * answers by regenerating the adv secret key. That key is a quarter of what -+ * the pairing QR advertises, so a client that only acks keeps offering a QR -+ * built on a secret the server has already retired: the phone scans it, -+ * reports a failed link, and no pair-success ever arrives. -+ * -+ * The ack itself is unchanged - the generic notification path already sends -+ * it - so this only adds the rotation and the re-render. -+ */ +export declare const handleCompanionRegRefresh: (node: BinaryNode, { creds, emitCredsUpdate, refreshQR, logger }: CompanionRegRefreshContext) => CompanionRegRefreshOutcome; //# sourceMappingURL=companion-reg-client-utils.d.ts.map \ No newline at end of file @@ -144,17 +112,10 @@ index f5edebd..5021c8f 100644 export var CompanionWebClientType; (function (CompanionWebClientType) { CompanionWebClientType[CompanionWebClientType["UNKNOWN"] = 0] = "UNKNOWN"; -@@ -32,4 +34,72 @@ export const buildPairingQRData = (ref, noiseKeyB64, identityKeyB64, advB64, bro +@@ -32,4 +34,43 @@ export const buildPairingQRData = (ref, noiseKeyB64, identityKeyB64, advB64, bro return ('https://wa.me/settings/linked_devices#' + [ref, noiseKeyB64, identityKeyB64, advB64, getCompanionPlatformId(browser)].join(',')); }; -+/** -+ * Holds the ref currently on screen so it can be re-rendered. -+ * -+ * `render` is called with the ref rather than a finished payload so the caller -+ * can read the adv secret at render time: a `companion_reg_refresh` rotates it -+ * mid-flow, and every QR emitted afterwards has to advertise the new value. -+ */ +export const makePairingQRRenderer = (refs, render) => { + let index = 0; + let current; @@ -178,38 +139,16 @@ index f5edebd..5021c8f 100644 + } + }; +}; -+/** The two children WA Web's parser accepts on this notification. */ +const COMPANION_REG_REFRESH_CHILDREN = ['companion_reg_refresh', 'pair-device-rotate-qr']; -+/** -+ * `` - the server retiring an -+ * unpaired companion's registration material. -+ * -+ * WA Web accepts the stanza with either a `companion_reg_refresh` or a -+ * `pair-device-rotate-qr` child, rejects it when neither is present, and -+ * answers by regenerating the adv secret key. That key is a quarter of what -+ * the pairing QR advertises, so a client that only acks keeps offering a QR -+ * built on a secret the server has already retired: the phone scans it, -+ * reports a failed link, and no pair-success ever arrives. -+ * -+ * The ack itself is unchanged - the generic notification path already sends -+ * it - so this only adds the rotation and the re-render. -+ */ +export const handleCompanionRegRefresh = (node, { creds, emitCredsUpdate, refreshQR, logger }) => { + if (!COMPANION_REG_REFRESH_CHILDREN.some(tag => getBinaryNodeChild(node, tag))) { + logger.warn({ node }, 'companion_reg_refresh carries neither expected child; ignoring'); + return 'ignored_malformed'; + } -+ // WA Web rotates unconditionally; a registered session is the one case -+ // where that is wrong here. `creds.me` is set by pair-success and by -+ // requestPairingCode, and in both cases the adv secret is what a completed -+ // or pending pairing is verified against - re-minting it would break the -+ // session rather than refresh a pending registration. + if (creds.me) { + logger.debug({ id: node.attrs.id }, 'companion_reg_refresh on a registered session; keeping the adv secret'); + return 'ignored_registered'; + } -+ // Same construction as initAuthCreds and as WA Web's generateADVSecretKey: -+ // 32 CSPRNG bytes, base64. + creds.advSecretKey = randomBytes(32).toString('base64'); + emitCredsUpdate({ advSecretKey: creds.advSecretKey }); + logger.info({ id: node.attrs.id }, 'rotated the adv secret the server asked to retire; re-rendering the pairing QR'); From 71a7ff6f46c73119df9e3270ba8b3c08c2453a73 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 17:32:07 -0300 Subject: [PATCH 14/96] fix(deps): install patch-package as a runtime dependency postinstall runs patch-package, so it must be available when only production dependencies are installed; otherwise the baileys patch is silently skipped. --- package-lock.json | 38 +------------------------------------- package.json | 6 +++--- 2 files changed, 4 insertions(+), 40 deletions(-) diff --git a/package-lock.json b/package-lock.json index 22b9f87381..e8129fce2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "node-cache": "^5.1.2", "node-cron": "^3.0.3", "openai": "^4.77.3", + "patch-package": "^8.0.1", "pg": "^8.13.1", "pino": "^9.10.0", "prisma": "^6.1.0", @@ -95,7 +96,6 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "husky": "^9.1.7", "lint-staged": "^16.1.6", - "patch-package": "^8.0.1", "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", @@ -5330,7 +5330,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/@zxing/text-encoding": { @@ -5963,7 +5962,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -6269,7 +6267,6 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, "funding": [ { "type": "github", @@ -6775,7 +6772,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -8627,7 +8623,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -8718,7 +8713,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "micromatch": "^4.0.2" @@ -9308,7 +9302,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -9986,7 +9979,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" @@ -10112,7 +10104,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -10345,7 +10336,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, "license": "MIT", "dependencies": { "is-docker": "^2.0.0" @@ -10358,7 +10348,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -10480,7 +10469,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -10520,7 +10508,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -10533,7 +10520,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", - "dev": true, "license": "Public Domain", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10640,7 +10626,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.1.11" @@ -11431,7 +11416,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -11937,7 +11921,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12105,7 +12088,6 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dev": true, "license": "MIT", "dependencies": { "is-docker": "^2.0.0", @@ -12524,7 +12506,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", - "dev": true, "license": "MIT", "dependencies": { "@yarnpkg/lockfile": "^1.1.0", @@ -12554,7 +12535,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -12570,7 +12550,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -12587,7 +12566,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -12600,14 +12578,12 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/patch-package/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -12622,7 +12598,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12632,7 +12607,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -12645,7 +12619,6 @@ "version": "0.2.7", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14.14" @@ -12675,7 +12648,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12804,7 +12776,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -14115,7 +14086,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -14128,7 +14098,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14242,7 +14211,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -14948,7 +14916,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -15838,7 +15805,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -16018,7 +15984,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -16286,7 +16251,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 63c88b78ca..bed8163028 100644 --- a/package.json +++ b/package.json @@ -88,10 +88,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", @@ -110,6 +110,7 @@ "node-cache": "^5.1.2", "node-cron": "^3.0.3", "openai": "^4.77.3", + "patch-package": "^8.0.1", "pg": "^8.13.1", "pino": "^9.10.0", "prisma": "^6.1.0", @@ -154,7 +155,6 @@ "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", - "typescript": "^5.7.2", - "patch-package": "^8.0.1" + "typescript": "^5.7.2" } } From 1de35d089f60db587d00c695a89d80a4ebcabf1c Mon Sep 17 00:00:00 2001 From: Thiago Di Giorgio Date: Wed, 12 Aug 2026 22:06:05 +0000 Subject: [PATCH 15/96] fix(auth-state): restore AppStateSyncKeyData.fromObject for app-state sync keys The Prisma, Redis and provider-files auth states read app-state-sync-key values with AppStateSyncKeyData.create(), which copies fields without coercion, so the stored base64 keyData never becomes bytes again. Baileys then fails to decrypt app-state mutations (labels, archive, mute, pin, contact names) and those events never fire. fromObject() performs the conversion, matching Baileys' reference auth state. (cherry picked from commit 65b5acf335e7f2831dc4ea1597be0341ece71405) Upstream: evolution-foundation/evolution-api#2685 (65b5acf3), unmerged --- src/utils/use-multi-file-auth-state-prisma.ts | 2 +- src/utils/use-multi-file-auth-state-provider-files.ts | 2 +- src/utils/use-multi-file-auth-state-redis-db.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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; From b6443b1ee8c8c1c4fb6eb309fc604f86b02065fc Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:02:29 -0300 Subject: [PATCH 16/96] test: add Vitest unit test runner Evolution has no tests. Vitest runs TypeScript directly and loads ESM-only Baileys from this CommonJS project without transforming node_modules. Tests live in test/, mirroring src/, outside the tsc and tsup build; /test/ is no longer gitignored. CI runs npm test in purple-checks; make test and AGENTS.md document it. --- .github/workflows/purple-checks.yml | 3 + .gitignore | 1 - AGENTS.md | 8 +- Makefile | 5 +- package-lock.json | 602 +++++++++++++++++++++++++++- package.json | 8 +- test/utils/smoke.test.ts | 8 + vitest.config.ts | 22 + 8 files changed, 648 insertions(+), 9 deletions(-) create mode 100644 test/utils/smoke.test.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/purple-checks.yml b/.github/workflows/purple-checks.yml index bf29412e76..895bf9a578 100644 --- a/.github/workflows/purple-checks.yml +++ b/.github/workflows/purple-checks.yml @@ -37,5 +37,8 @@ jobs: - name: Lint run: npm run lint:check + - name: Unit tests + run: npm test + - name: Check docs run: bash scripts/check-docs.sh diff --git a/.gitignore b/.gitignore index 17d3d17819..cddea7dcd1 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,6 @@ lerna-debug.log* # Project related /instances/* !/instances/.gitkeep -/test/ /src/env.yml /store *.env diff --git a/AGENTS.md b/AGENTS.md index c4d2ffabae..2b98aa4d9e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,8 +31,8 @@ and **stenfar**. Anything they consume is a public contract. ## Commands & danger zones - **Local / safe:** `npm run dev:server` (tsx watch), `npm start`, `npm run build` (`tsc --noEmit && tsup`), - `npm run lint:check`, `npx tsc --noEmit`, `make check-docs`, `npm run db:generate`, - `docker-compose -f docker-compose.dev.yaml up -d`. + `npm test` (Vitest, `test/**/*.test.ts`), `npm run lint:check`, `npx tsc --noEmit`, `make check-docs`, + `npm run db:generate`, `docker-compose -f docker-compose.dev.yaml up -d`. - **Database:** always `export DATABASE_PROVIDER=postgresql` (or `mysql`) first. `npm run db:migrate:dev` only against a local database. Every schema change ships migrations for BOTH providers. - **PROD — never without explicit user approval:** `npm run db:deploy`, image publish workflows, any deploy, @@ -117,9 +117,11 @@ ODD (gentle-ai) is the workflow; its protocol is not repeated here. What this re ## Verification -There is no test suite yet (`npm test` is an upstream placeholder). Before handing off a change, run: +Unit tests use Vitest and live in `test/`, mirroring `src/` (e.g. `test/utils/createJid.test.ts`). They cover +how our code processes data; Baileys is mocked where it touches the socket. Before handing off a change, run: ```bash +npm test npx tsc --noEmit npm run build npm run lint:check diff --git a/Makefile b/Makefile index 44416c9005..55b416f8d8 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ IMAGE ?= evolution-in-purple:dev -.PHONY: help install dev build lint typecheck check-docs hooks-install docker-build _guard-not-prod +.PHONY: help install dev build lint test typecheck check-docs hooks-install docker-build _guard-not-prod help: ## Show available targets @grep -hE '^[a-zA-Z][a-zA-Z_-]*:.*## ' $(MAKEFILE_LIST) | \ @@ -21,6 +21,9 @@ build: ## Typecheck and build to dist/ (npm run build) lint: ## ESLint with auto-fix (npm run lint) npm run lint +test: ## Run the unit tests once (npm test) + npm test + typecheck: ## TypeScript check without emit npx --no tsc --noEmit diff --git a/package-lock.json b/package-lock.json index e8129fce2d..4775b8e3a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,7 +99,8 @@ "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", - "typescript": "^5.7.2" + "typescript": "^5.7.2", + "vitest": "^3.2.7" } }, "node_modules/@adiwajshing/keyed-db": { @@ -4770,6 +4771,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/compression": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", @@ -4809,6 +4821,13 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -5275,6 +5294,121 @@ "dev": true, "license": "ISC" }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@wasm-audio-decoders/common": { "version": "9.0.7", "resolved": "https://registry.npmjs.org/@wasm-audio-decoders/common/-/common-9.0.7.tgz", @@ -5665,6 +5799,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", @@ -6189,6 +6333,23 @@ "node": ">=6" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -6209,6 +6370,16 @@ "dev": true, "license": "MIT" }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/cheerio": { "version": "1.0.0-rc.11", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.11.tgz", @@ -6990,6 +7161,16 @@ "dev": true, "license": "MIT" }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -7554,6 +7735,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -8278,6 +8466,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -8336,6 +8534,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -11208,6 +11416,13 @@ "node": ">=0.10.0" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", @@ -11737,6 +11952,25 @@ "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" } }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/nats": { "version": "2.29.3", "resolved": "https://registry.npmjs.org/nats/-/nats-2.29.3.tgz", @@ -12671,6 +12905,16 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -12893,6 +13137,35 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postcss-load-config": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", @@ -14181,6 +14454,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -14434,6 +14714,16 @@ "node": ">= 12" } }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split-on-first": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", @@ -14452,6 +14742,13 @@ "node": ">= 10.x" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -14461,6 +14758,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -14649,6 +14953,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/strnum": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", @@ -14839,6 +15163,13 @@ "readable-stream": "3" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", @@ -14899,6 +15230,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -15927,6 +16288,228 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -16089,6 +16672,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/win-guid": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz", diff --git a/package.json b/package.json index bed8163028..3496283e9d 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "start": "tsx ./src/main.ts", "start:prod": "node dist/main", "dev:server": "tsx watch ./src/main.ts", - "test": "tsx watch ./test/all.test.ts", + "test": "vitest run", "lint": "eslint --fix --ext .ts src", "lint:check": "eslint --ext .ts src", "commit": "cz", @@ -21,7 +21,8 @@ "db:migrate:dev": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate dev --schema ./prisma/DATABASE_PROVIDER-schema.prisma && cp -r ./prisma/migrations/* ./prisma/DATABASE_PROVIDER-migrations\"", "db:migrate:dev:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate dev --schema prisma\\DATABASE_PROVIDER-schema.prisma\"", "prepare": "husky", - "postinstall": "patch-package" + "postinstall": "patch-package", + "test:watch": "vitest" }, "repository": { "type": "git", @@ -155,6 +156,7 @@ "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", - "typescript": "^5.7.2" + "typescript": "^5.7.2", + "vitest": "^3.2.7" } } diff --git a/test/utils/smoke.test.ts b/test/utils/smoke.test.ts new file mode 100644 index 0000000000..a0234c81f4 --- /dev/null +++ b/test/utils/smoke.test.ts @@ -0,0 +1,8 @@ +import { createJid } from '@utils/createJid'; +import { describe, expect, it } from 'vitest'; + +describe('vitest setup', () => { + it('resolves tsconfig path aliases', () => { + expect(typeof createJid).toBe('function'); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000000..7f85e1d2ce --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,22 @@ +import { resolve } from 'path'; +import { defineConfig } from 'vitest/config'; + +const src = (dir: string) => resolve(__dirname, 'src', dir); + +export default defineConfig({ + resolve: { + alias: { + '@api': src('api'), + '@cache': src('cache'), + '@config': src('config'), + '@exceptions': src('exceptions'), + '@libs': src('libs'), + '@utils': src('utils'), + '@validate': src('validate'), + }, + }, + test: { + include: ['test/**/*.test.ts'], + environment: 'node', + }, +}); From 9d3d1a0058957298e1082748f63ea45b8657fa79 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:06:05 -0300 Subject: [PATCH 17/96] test(utils): characterize createJid and getConversationMessage Pins current behavior: JID passthrough and normalization, group ids, the AR/MX 13-digit and BR 9th-digit rules, and message content extraction per type with S3 on and off. Suspected bugs are asserted as-is, not fixed: AR/MX numbers lose their third digit, ad-only messages duplicate the externalAdReplyBody prefix, and a location without latitude throws. --- test/utils/createJid.test.ts | 64 +++++++++ test/utils/getConversationMessage.test.ts | 160 ++++++++++++++++++++++ test/utils/smoke.test.ts | 8 -- 3 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 test/utils/createJid.test.ts create mode 100644 test/utils/getConversationMessage.test.ts delete mode 100644 test/utils/smoke.test.ts diff --git a/test/utils/createJid.test.ts b/test/utils/createJid.test.ts new file mode 100644 index 0000000000..21fe142bb4 --- /dev/null +++ b/test/utils/createJid.test.ts @@ -0,0 +1,64 @@ +import { createJid } from '@utils/createJid'; +import { describe, expect, it } from 'vitest'; + +describe('createJid', () => { + it('returns group, user, lid and broadcast jids unchanged', () => { + expect(createJid('120363012345678901@g.us')).toBe('120363012345678901@g.us'); + expect(createJid('5511987654321@s.whatsapp.net')).toBe('5511987654321@s.whatsapp.net'); + expect(createJid('123456789@lid')).toBe('123456789@lid'); + expect(createJid('status@broadcast')).toBe('status@broadcast'); + }); + + it('strips the device suffix before passing a jid through', () => { + expect(createJid('5511987654321:12@s.whatsapp.net')).toBe('5511987654321@s.whatsapp.net'); + expect(createJid('123456789:3@lid')).toBe('123456789@lid'); + }); + + it('removes plus signs, spaces and parentheses from plain numbers', () => { + expect(createJid('+1 (555) 123 4567')).toBe('15551234567@s.whatsapp.net'); + }); + + it('removes dashes from short plain numbers', () => { + expect(createJid('+55 (11) 98765-4321')).toBe('5511987654321@s.whatsapp.net'); + }); + + it('treats dashed ids with at least 24 characters as groups', () => { + expect(createJid('5511999999999-1623281429')).toBe('5511999999999-1623281429@g.us'); + }); + + it('treats numbers with at least 18 digits as groups', () => { + expect(createJid('120363012345678901')).toBe('120363012345678901@g.us'); + }); + + it('keeps 17 digit numbers as user jids', () => { + expect(createJid('12036301234567890')).toBe('12036301234567890@s.whatsapp.net'); + }); + + it('removes the third digit from 13 digit Argentine numbers', () => { + expect(createJid('5492616107398')).toBe('542616107398@s.whatsapp.net'); + }); + + it('removes the third digit from 13 digit Mexican numbers', () => { + expect(createJid('5215512345678')).toBe('525512345678@s.whatsapp.net'); + }); + + it('keeps 12 digit Argentine numbers unchanged', () => { + expect(createJid('542616107398')).toBe('542616107398@s.whatsapp.net'); + }); + + it('keeps the Brazilian ninth digit when the area code is below 31', () => { + expect(createJid('5511987654321')).toBe('5511987654321@s.whatsapp.net'); + }); + + it('keeps the Brazilian ninth digit when the subscriber number starts below 7', () => { + expect(createJid('5541961234567')).toBe('5541961234567@s.whatsapp.net'); + }); + + it('drops the Brazilian ninth digit for area codes from 31 with subscriber numbers from 7', () => { + expect(createJid('5541987654321')).toBe('554187654321@s.whatsapp.net'); + }); + + it('leaves non Brazilian 13 digit numbers unchanged', () => { + expect(createJid('4412345678901')).toBe('4412345678901@s.whatsapp.net'); + }); +}); diff --git a/test/utils/getConversationMessage.test.ts b/test/utils/getConversationMessage.test.ts new file mode 100644 index 0000000000..63d5527c49 --- /dev/null +++ b/test/utils/getConversationMessage.test.ts @@ -0,0 +1,160 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const s3 = vi.hoisted(() => ({ ENABLE: false, SAVE_VIDEO: false })); + +vi.mock('@config/env.config', () => ({ + configService: { get: vi.fn(() => s3) }, +})); + +import { getConversationMessage } from '@utils/getConversationMessage'; + +const key = { id: 'MSG1' }; + +describe('getConversationMessage', () => { + beforeEach(() => { + s3.ENABLE = false; + s3.SAVE_VIDEO = false; + }); + + it('returns plain conversation text', () => { + expect(getConversationMessage({ key, message: { conversation: 'hello' } })).toBe('hello'); + }); + + it('returns extended text message text', () => { + expect(getConversationMessage({ key, message: { extendedTextMessage: { text: 'link' } } })).toBe('link'); + }); + + it('formats images with the key id when S3 is disabled', () => { + expect(getConversationMessage({ key, message: { imageMessage: {} } })).toBe('imageMessage|MSG1'); + expect(getConversationMessage({ key, message: { imageMessage: { caption: 'pic' } } })).toBe( + 'imageMessage|MSG1|pic', + ); + }); + + it('formats images with the media url when S3 is enabled', () => { + s3.ENABLE = true; + + expect(getConversationMessage({ key, message: { imageMessage: {}, mediaUrl: 'https://s3/x.jpg' } })).toBe( + 'imageMessage|https://s3/x.jpg', + ); + }); + + it('formats videos with and without caption', () => { + expect(getConversationMessage({ key, message: { videoMessage: {} } })).toBe('videoMessage|MSG1'); + expect(getConversationMessage({ key, message: { videoMessage: { caption: 'clip' } } })).toBe( + 'videoMessage|MSG1|clip', + ); + }); + + it('uses the key id for videos when S3 is enabled but video saving is off', () => { + s3.ENABLE = true; + + expect(getConversationMessage({ key, message: { videoMessage: {}, mediaUrl: 'https://s3/v.mp4' } })).toBe( + 'videoMessage|MSG1', + ); + }); + + it('uses the media url for videos when S3 and video saving are enabled', () => { + s3.ENABLE = true; + s3.SAVE_VIDEO = true; + + expect(getConversationMessage({ key, message: { videoMessage: {}, mediaUrl: 'https://s3/v.mp4' } })).toBe( + 'videoMessage|https://s3/v.mp4', + ); + }); + + it('formats documents with and without caption', () => { + expect(getConversationMessage({ key, message: { documentMessage: {} } })).toBe('documentMessage|MSG1'); + expect(getConversationMessage({ key, message: { documentMessage: { caption: 'doc' } } })).toBe( + 'documentMessage|MSG1|doc', + ); + }); + + it('formats documents wrapped with a caption', () => { + expect( + getConversationMessage({ + key, + message: { documentWithCaptionMessage: { message: { documentMessage: { caption: 'cv' } } } }, + }), + ).toBe('documentWithCaptionMessage|MSG1|cv'); + }); + + it('formats audio with the media id', () => { + expect(getConversationMessage({ key, message: { audioMessage: {} } })).toBe('audioMessage|MSG1'); + }); + + it('prefers speech to text over the audio reference', () => { + expect(getConversationMessage({ key, message: { audioMessage: {}, speechToText: 'transcribed' } })).toBe( + 'transcribed', + ); + }); + + it('returns the latitude for location messages', () => { + expect( + getConversationMessage({ key, message: { locationMessage: { degreesLatitude: -32.89, degreesLongitude: -68 } } }), + ).toBe('-32.89'); + }); + + it('throws for location messages without latitude', () => { + expect(() => getConversationMessage({ key, message: { locationMessage: {} } })).toThrow(TypeError); + }); + + it('returns the contact display name', () => { + expect(getConversationMessage({ key, message: { contactMessage: { displayName: 'Ana' } } })).toBe('Ana'); + }); + + it('returns the list response title', () => { + expect( + getConversationMessage({ + key, + message: { listResponseMessage: { title: 'Option A', singleSelectReply: { selectedRowId: 'row-1' } } }, + }), + ).toBe('Option A'); + }); + + it('returns the selected row id when the list response has no title', () => { + expect( + getConversationMessage({ + key, + message: { listResponseMessage: { singleSelectReply: { selectedRowId: 'row-1' } } }, + }), + ).toBe('row-1'); + }); + + it('returns the selected id of template and buttons responses', () => { + expect(getConversationMessage({ key, message: { templateButtonReplyMessage: { selectedId: 'tpl' } } })).toBe('tpl'); + expect(getConversationMessage({ key, message: { buttonsResponseMessage: { selectedButtonId: 'btn' } } })).toBe( + 'btn', + ); + }); + + it('returns view once media urls', () => { + expect( + getConversationMessage({ key, message: { viewOnceMessageV2: { message: { imageMessage: { url: 'u1' } } } } }), + ).toBe('u1'); + }); + + it('appends the external ad reply body on a new line', () => { + expect( + getConversationMessage({ + key, + message: { conversation: 'hi' }, + contextInfo: { externalAdReply: { body: 'Ad text' } }, + }), + ).toBe('hi\nexternalAdReplyBody|Ad text'); + }); + + it('prefixes the message type name when the external ad reply body is the only content', () => { + expect(getConversationMessage({ key, message: {}, contextInfo: { externalAdReply: { body: 'Ad' } } })).toBe( + 'externalAdReplyBody\nexternalAdReplyBody|Ad', + ); + }); + + it('returns the first matching type when several are present', () => { + expect(getConversationMessage({ key, message: { conversation: 'text', imageMessage: {} } })).toBe('text'); + }); + + it('returns the message type name for unknown messages', () => { + expect(getConversationMessage({ key, message: { reactionMessage: { text: '👍' } } })).toBe('unknown'); + }); +}); diff --git a/test/utils/smoke.test.ts b/test/utils/smoke.test.ts deleted file mode 100644 index a0234c81f4..0000000000 --- a/test/utils/smoke.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createJid } from '@utils/createJid'; -import { describe, expect, it } from 'vitest'; - -describe('vitest setup', () => { - it('resolves tsconfig path aliases', () => { - expect(typeof createJid).toBe('function'); - }); -}); From 668871d06af6b6e1385f9a8e279be8d73834b59f Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:06:06 -0300 Subject: [PATCH 18/96] test(utils): characterize findBotByTrigger, advancedOperatorsSearch and onWhatsappCache Covers each trigger type and operator and the cache hit, miss, update-skip and expiry paths with mocked Prisma and config. Suspected bugs are asserted as-is, not fixed: non BR/MX/AR numbers get the domain appended twice in onWhatsappCache, a jid without a domain becomes @undefined, an invalid stored regex rejects the whole bot lookup, and search values with spaces never match. --- test/utils/advancedOperatorsSearch.test.ts | 56 +++++ test/utils/findBotByTrigger.test.ts | 138 +++++++++++++ test/utils/onWhatsappCache.test.ts | 228 +++++++++++++++++++++ 3 files changed, 422 insertions(+) create mode 100644 test/utils/advancedOperatorsSearch.test.ts create mode 100644 test/utils/findBotByTrigger.test.ts create mode 100644 test/utils/onWhatsappCache.test.ts diff --git a/test/utils/advancedOperatorsSearch.test.ts b/test/utils/advancedOperatorsSearch.test.ts new file mode 100644 index 0000000000..603f52bc4c --- /dev/null +++ b/test/utils/advancedOperatorsSearch.test.ts @@ -0,0 +1,56 @@ +import { advancedOperatorsSearch } from '@utils/advancedOperatorsSearch'; +import { describe, expect, it } from 'vitest'; + +describe('advancedOperatorsSearch', () => { + it('matches contains ignoring case and accents', () => { + expect(advancedOperatorsSearch('Olá Mundo', 'contains:ola')).toBe(true); + expect(advancedOperatorsSearch('Hello', 'contains:bye')).toBe(false); + }); + + it('matches notcontains only when the value is absent', () => { + expect(advancedOperatorsSearch('hello world', 'notcontains:bye')).toBe(true); + expect(advancedOperatorsSearch('hello world', 'notcontains:world')).toBe(false); + }); + + it('matches startswith and endswith', () => { + expect(advancedOperatorsSearch('Hello world', 'startswith:hello')).toBe(true); + expect(advancedOperatorsSearch('Hello world', 'startswith:world')).toBe(false); + expect(advancedOperatorsSearch('Hello world', 'endswith:WORLD')).toBe(true); + expect(advancedOperatorsSearch('Hello world', 'endswith:hello')).toBe(false); + }); + + it('matches exact only on the whole normalized text', () => { + expect(advancedOperatorsSearch('Ação', 'exact:acao')).toBe(true); + expect(advancedOperatorsSearch('Ação rápida', 'exact:acao')).toBe(false); + }); + + it('accepts operator names in any case', () => { + expect(advancedOperatorsSearch('hello', 'CONTAINS:ell')).toBe(true); + }); + + it('returns false for unknown operators', () => { + expect(advancedOperatorsSearch('hello', 'matches:hello')).toBe(false); + }); + + it('requires every comma separated value to match', () => { + expect(advancedOperatorsSearch('buy red shoes', 'contains:red,shoes')).toBe(true); + expect(advancedOperatorsSearch('buy red shoes', 'contains:red,hat')).toBe(false); + }); + + it('accepts any repeated value for the same operator', () => { + expect(advancedOperatorsSearch('buy shoes', 'contains:hat contains:shoes')).toBe(true); + }); + + it('requires every distinct operator to match', () => { + expect(advancedOperatorsSearch('buy shoes', 'startswith:buy notcontains:hat')).toBe(true); + expect(advancedOperatorsSearch('buy shoes', 'startswith:buy notcontains:shoes')).toBe(false); + }); + + it('keeps colons inside the value', () => { + expect(advancedOperatorsSearch('time 10:30', 'endswith:10:30')).toBe(true); + }); + + it('splits multi word values into separate filters', () => { + expect(advancedOperatorsSearch('good morning', 'exact:good morning')).toBe(false); + }); +}); diff --git a/test/utils/findBotByTrigger.test.ts b/test/utils/findBotByTrigger.test.ts new file mode 100644 index 0000000000..6b9d43b355 --- /dev/null +++ b/test/utils/findBotByTrigger.test.ts @@ -0,0 +1,138 @@ +import { findBotByTrigger } from '@utils/findBotByTrigger'; +import { describe, expect, it, vi } from 'vitest'; + +type Bot = { id: string; triggerType: string; triggerOperator?: string; triggerValue?: string }; + +const createRepository = (bots: Bot[]) => ({ + findFirst: vi.fn(async ({ where }) => { + if (where.triggerType?.in) return bots.find((bot) => where.triggerType.in.includes(bot.triggerType)) ?? null; + return ( + bots.find( + (bot) => + bot.triggerType === where.triggerType && + bot.triggerOperator === where.triggerOperator && + bot.triggerValue === where.triggerValue, + ) ?? null + ); + }), + findMany: vi.fn(async ({ where }) => + bots.filter( + (bot) => + bot.triggerType === where.triggerType && + (where.triggerOperator === undefined || bot.triggerOperator === where.triggerOperator), + ), + ), +}); + +describe('findBotByTrigger', () => { + it('returns a bot with trigger type all before any other bot', async () => { + const repository = createRepository([ + { id: 'kw', triggerType: 'keyword', triggerOperator: 'equals', triggerValue: 'hi' }, + { id: 'all', triggerType: 'all' }, + ]); + + expect(await findBotByTrigger(repository, 'hi', 'inst')).toEqual({ id: 'all', triggerType: 'all' }); + }); + + it('returns a bot with trigger type none', async () => { + const repository = createRepository([{ id: 'none', triggerType: 'none' }]); + + expect((await findBotByTrigger(repository, 'anything', 'inst'))?.id).toBe('none'); + }); + + it('filters every query by enabled and instance id', async () => { + const repository = createRepository([]); + + await findBotByTrigger(repository, 'hi', 'inst-1'); + + for (const call of [...repository.findFirst.mock.calls, ...repository.findMany.mock.calls]) { + expect(call[0].where).toMatchObject({ enabled: true, instanceId: 'inst-1' }); + } + }); + + it('returns the first advanced bot whose query matches', async () => { + const repository = createRepository([ + { id: 'a1', triggerType: 'advanced', triggerValue: 'contains:bye' }, + { id: 'a2', triggerType: 'advanced', triggerValue: 'startswith:hello' }, + ]); + + expect((await findBotByTrigger(repository, 'Hello there', 'inst'))?.id).toBe('a2'); + }); + + it('prefers advanced bots over keyword bots', async () => { + const repository = createRepository([ + { id: 'eq', triggerType: 'keyword', triggerOperator: 'equals', triggerValue: 'hello' }, + { id: 'adv', triggerType: 'advanced', triggerValue: 'exact:hello' }, + ]); + + expect((await findBotByTrigger(repository, 'hello', 'inst'))?.id).toBe('adv'); + }); + + it('returns a keyword bot on exact equality', async () => { + const repository = createRepository([ + { id: 'eq', triggerType: 'keyword', triggerOperator: 'equals', triggerValue: 'menu' }, + ]); + + expect((await findBotByTrigger(repository, 'menu', 'inst'))?.id).toBe('eq'); + expect(await findBotByTrigger(repository, 'Menu', 'inst')).toBeNull(); + }); + + it('returns a keyword bot whose regex matches', async () => { + const repository = createRepository([ + { id: 're', triggerType: 'keyword', triggerOperator: 'regex', triggerValue: '^order\\s\\d+$' }, + ]); + + expect((await findBotByTrigger(repository, 'order 42', 'inst'))?.id).toBe('re'); + expect(await findBotByTrigger(repository, 'order x', 'inst')).toBeNull(); + }); + + it('returns a keyword bot whose value starts the content', async () => { + const repository = createRepository([ + { id: 'sw', triggerType: 'keyword', triggerOperator: 'startsWith', triggerValue: '/start' }, + ]); + + expect((await findBotByTrigger(repository, '/start now', 'inst'))?.id).toBe('sw'); + expect(await findBotByTrigger(repository, 'now /start', 'inst')).toBeNull(); + }); + + it('returns a keyword bot whose value ends the content', async () => { + const repository = createRepository([ + { id: 'ew', triggerType: 'keyword', triggerOperator: 'endsWith', triggerValue: '?' }, + ]); + + expect((await findBotByTrigger(repository, 'help?', 'inst'))?.id).toBe('ew'); + expect(await findBotByTrigger(repository, '?help', 'inst')).toBeNull(); + }); + + it('returns a keyword bot whose value is contained in the content', async () => { + const repository = createRepository([ + { id: 'ct', triggerType: 'keyword', triggerOperator: 'contains', triggerValue: 'price' }, + ]); + + expect((await findBotByTrigger(repository, 'what is the price today', 'inst'))?.id).toBe('ct'); + expect(await findBotByTrigger(repository, 'hello', 'inst')).toBeNull(); + }); + + it('checks keyword operators in the order equals, regex, startsWith, endsWith, contains', async () => { + const repository = createRepository([ + { id: 'ct', triggerType: 'keyword', triggerOperator: 'contains', triggerValue: 'a' }, + { id: 'ew', triggerType: 'keyword', triggerOperator: 'endsWith', triggerValue: 'a' }, + { id: 'sw', triggerType: 'keyword', triggerOperator: 'startsWith', triggerValue: 'a' }, + { id: 're', triggerType: 'keyword', triggerOperator: 'regex', triggerValue: 'a' }, + ]); + + expect((await findBotByTrigger(repository, 'aba', 'inst'))?.id).toBe('re'); + }); + + it('throws when a stored regex is invalid', async () => { + const repository = createRepository([ + { id: 're', triggerType: 'keyword', triggerOperator: 'regex', triggerValue: '(' }, + ]); + + await expect(findBotByTrigger(repository, 'x', 'inst')).rejects.toThrow(SyntaxError); + }); + + it('returns null when nothing matches', async () => { + expect(await findBotByTrigger(createRepository([]), 'hi', 'inst')).toBeNull(); + }); +}); diff --git a/test/utils/onWhatsappCache.test.ts b/test/utils/onWhatsappCache.test.ts new file mode 100644 index 0000000000..42d5f466f2 --- /dev/null +++ b/test/utils/onWhatsappCache.test.ts @@ -0,0 +1,228 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + database: { SAVE_DATA: { IS_ON_WHATSAPP: true, IS_ON_WHATSAPP_DAYS: 7 } }, + isOnWhatsapp: { findFirst: vi.fn(), findMany: vi.fn(), create: vi.fn(), update: vi.fn() }, +})); + +vi.mock('@api/server.module', () => ({ prismaRepository: { isOnWhatsapp: mocks.isOnWhatsapp } })); +vi.mock('@config/env.config', () => ({ configService: { get: vi.fn(() => mocks.database) } })); +vi.mock('@config/logger.config', () => ({ + Logger: class { + verbose = vi.fn(); + warn = vi.fn(); + error = vi.fn(); + }, +})); + +import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; + +const { isOnWhatsapp } = mocks; + +const queriedJids = () => + isOnWhatsapp.findMany.mock.calls[0][0].where.OR.map((clause: any) => clause.jidOptions.contains); + +describe('getOnWhatsappCache', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-01-15T12:00:00.000Z')); + vi.clearAllMocks(); + mocks.database.SAVE_DATA.IS_ON_WHATSAPP = true; + mocks.database.SAVE_DATA.IS_ON_WHATSAPP_DAYS = 7; + isOnWhatsapp.findMany.mockResolvedValue([]); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('returns an empty list without querying when the cache is disabled', async () => { + mocks.database.SAVE_DATA.IS_ON_WHATSAPP = false; + + expect(await getOnWhatsappCache(['5511987654321@s.whatsapp.net'])).toEqual([]); + expect(isOnWhatsapp.findMany).not.toHaveBeenCalled(); + }); + + it('only returns records updated within the configured number of days', async () => { + await getOnWhatsappCache(['15551234567@s.whatsapp.net']); + + expect(isOnWhatsapp.findMany.mock.calls[0][0].where.updatedAt.gte).toEqual(new Date('2026-01-08T12:00:00.000Z')); + }); + + it('maps cache hits to remote jid, number, jid options and lid', async () => { + isOnWhatsapp.findMany.mockResolvedValue([ + { remoteJid: '5511987654321@s.whatsapp.net', jidOptions: 'a@s.whatsapp.net,b@lid', lid: 'lid' }, + ]); + + expect(await getOnWhatsappCache(['5511987654321@s.whatsapp.net'])).toEqual([ + { + remoteJid: '5511987654321@s.whatsapp.net', + number: '5511987654321', + jidOptions: ['a@s.whatsapp.net', 'b@lid'], + lid: 'lid', + }, + ]); + }); + + it('searches Brazilian numbers with and without the ninth digit', async () => { + await getOnWhatsappCache(['5511987654321@s.whatsapp.net', '551187654321@s.whatsapp.net']); + + expect(queriedJids()).toEqual([ + '5511987654321@s.whatsapp.net', + '551187654321@s.whatsapp.net', + '5511987654321@s.whatsapp.net', + '551187654321@s.whatsapp.net', + ]); + }); + + it('searches Argentine numbers with and without the 9 prefix', async () => { + await getOnWhatsappCache(['5492616107398@s.whatsapp.net', '542616107398@s.whatsapp.net']); + + expect(queriedJids()).toEqual([ + '5492616107398@s.whatsapp.net', + '542616107398@s.whatsapp.net', + '5492616107398@s.whatsapp.net', + '542616107398@s.whatsapp.net', + ]); + }); + + it('searches Mexican numbers with and without the 1 prefix', async () => { + await getOnWhatsappCache(['525512345678@s.whatsapp.net']); + + expect(queriedJids()).toEqual(['5215512345678@s.whatsapp.net', '525512345678@s.whatsapp.net']); + }); + + it('searches other numbers without the plus sign but with the domain appended twice', async () => { + await getOnWhatsappCache(['+15551234567@s.whatsapp.net']); + + expect(queriedJids()).toEqual(['15551234567@s.whatsapp.net@s.whatsapp.net']); + }); + + it('searches lid and group jids as given', async () => { + await getOnWhatsappCache(['123456@lid', '5511999999999-1623281429@g.us']); + + expect(queriedJids()).toEqual(['123456@lid', '5511999999999-1623281429@g.us']); + }); + + it('appends an undefined domain to numbers without a domain', async () => { + await getOnWhatsappCache(['15551234567']); + + expect(queriedJids()).toEqual(['15551234567@undefined']); + }); +}); + +describe('saveOnWhatsappCache', () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.database.SAVE_DATA.IS_ON_WHATSAPP = true; + isOnWhatsapp.findFirst.mockResolvedValue(null); + }); + + it('does nothing when the cache is disabled', async () => { + mocks.database.SAVE_DATA.IS_ON_WHATSAPP = false; + + await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); + + expect(isOnWhatsapp.findFirst).not.toHaveBeenCalled(); + }); + + it('skips items without a remote jid', async () => { + await saveOnWhatsappCache([{ remoteJid: '' }]); + + expect(isOnWhatsapp.findFirst).not.toHaveBeenCalled(); + }); + + it('creates a record with sorted jid options when none exists', async () => { + await saveOnWhatsappCache([{ remoteJid: '+5511987654321@s.whatsapp.net' }]); + + expect(isOnWhatsapp.create).toHaveBeenCalledWith({ + data: { + remoteJid: '5511987654321@s.whatsapp.net', + jidOptions: '551187654321@s.whatsapp.net,5511987654321@s.whatsapp.net', + lid: null, + }, + }); + }); + + it('looks up existing records by jid options and remote jid', async () => { + await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); + + expect(isOnWhatsapp.findFirst).toHaveBeenCalledWith({ + where: { + OR: [ + { jidOptions: { contains: '15551234567@s.whatsapp.net@s.whatsapp.net' } }, + { remoteJid: '15551234567@s.whatsapp.net' }, + ], + }, + }); + }); + + it('adds a lid alternate jid and marks the record as lid', async () => { + await saveOnWhatsappCache([{ remoteJid: '5511987654321@s.whatsapp.net', remoteJidAlt: '999@lid', lid: 'lid' }]); + + expect(isOnWhatsapp.create).toHaveBeenCalledWith({ + data: { + remoteJid: '5511987654321@s.whatsapp.net', + jidOptions: '551187654321@s.whatsapp.net,5511987654321@s.whatsapp.net,999@lid', + lid: 'lid', + }, + }); + }); + + it('ignores alternate jids that are not lids', async () => { + await saveOnWhatsappCache([{ remoteJid: '999@lid', remoteJidAlt: '15550000000@s.whatsapp.net' }]); + + expect(isOnWhatsapp.create.mock.calls[0][0].data.jidOptions).toBe('999@lid'); + }); + + it('marks lid remote jids as lid', async () => { + await saveOnWhatsappCache([{ remoteJid: '999@lid' }]); + + expect(isOnWhatsapp.create).toHaveBeenCalledWith({ + data: { remoteJid: '999@lid', jidOptions: '999@lid', lid: 'lid' }, + }); + }); + + it('skips the update when the existing record is identical', async () => { + isOnWhatsapp.findFirst.mockResolvedValue({ + id: 'r1', + remoteJid: '5511987654321@s.whatsapp.net', + jidOptions: '5511987654321@s.whatsapp.net,551187654321@s.whatsapp.net', + lid: null, + }); + + await saveOnWhatsappCache([{ remoteJid: '5511987654321@s.whatsapp.net' }]); + + expect(isOnWhatsapp.update).not.toHaveBeenCalled(); + expect(isOnWhatsapp.create).not.toHaveBeenCalled(); + }); + + it('merges existing jid options into an update when data differs', async () => { + isOnWhatsapp.findFirst.mockResolvedValue({ + id: 'r1', + remoteJid: '15551234567@s.whatsapp.net', + jidOptions: '888@lid', + lid: null, + }); + + await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); + + expect(isOnWhatsapp.update).toHaveBeenCalledWith({ + where: { id: 'r1' }, + data: { + remoteJid: '15551234567@s.whatsapp.net', + jidOptions: '15551234567@s.whatsapp.net@s.whatsapp.net,888@lid', + lid: null, + }, + }); + }); + + it('keeps processing other items when one fails', async () => { + isOnWhatsapp.findFirst.mockRejectedValueOnce(new Error('db down')).mockResolvedValueOnce(null); + + await expect( + saveOnWhatsappCache([{ remoteJid: '111@s.whatsapp.net' }, { remoteJid: '222@s.whatsapp.net' }]), + ).resolves.toBeUndefined(); + expect(isOnWhatsapp.create).toHaveBeenCalledTimes(1); + }); +}); From 4120eb2578627e78389ec567f8e84e7eadd5705b Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:09:43 -0300 Subject: [PATCH 19/96] test(utils): characterize the Prisma, Redis and provider-files auth states Round-trips creds, Signal keys and app-state-sync-key values through fakes that reproduce the real BufferJSON serialization, so binary fields must survive storage. Covers the base64 keyData path that fromObject converts back to bytes. Suspected bugs are asserted as-is, not fixed: Prisma removeCreds keeps the Session row when Redis is enabled and leaves local key files on disk, fs.writeFile errors reject keys.set, and provider-files errors are swallowed without logs. --- .../use-multi-file-auth-state-prisma.test.ts | 266 ++++++++++++++++++ ...lti-file-auth-state-provider-files.test.ts | 184 ++++++++++++ ...use-multi-file-auth-state-redis-db.test.ts | 173 ++++++++++++ 3 files changed, 623 insertions(+) create mode 100644 test/utils/use-multi-file-auth-state-prisma.test.ts create mode 100644 test/utils/use-multi-file-auth-state-provider-files.test.ts create mode 100644 test/utils/use-multi-file-auth-state-redis-db.test.ts diff --git a/test/utils/use-multi-file-auth-state-prisma.test.ts b/test/utils/use-multi-file-auth-state-prisma.test.ts new file mode 100644 index 0000000000..4ddbb3bf29 --- /dev/null +++ b/test/utils/use-multi-file-auth-state-prisma.test.ts @@ -0,0 +1,266 @@ +import { BufferJSON, proto } from 'baileys'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + loggers: [] as any[], + cacheConf: { REDIS: { ENABLED: false } }, + sessions: new Map(), + files: new Map(), +})); + +vi.mock('@config/logger.config', () => ({ + Logger: class { + info = vi.fn(); + warn = vi.fn(); + error = vi.fn(); + constructor() { + mocks.loggers.push(this); + } + }, +})); +vi.mock('@config/env.config', () => ({ configService: { get: vi.fn(() => mocks.cacheConf) } })); +vi.mock('@config/path.config', () => ({ INSTANCE_DIR: '/instances' })); +vi.mock('@api/services/cache.service', () => ({ CacheService: class {} })); +vi.mock('@api/server.module', () => ({ + prismaRepository: { + session: { + findUnique: vi.fn(async ({ where }) => + mocks.sessions.has(where.sessionId) + ? { sessionId: where.sessionId, creds: mocks.sessions.get(where.sessionId) } + : null, + ), + create: vi.fn(async ({ data }) => mocks.sessions.set(data.sessionId, data.creds)), + update: vi.fn(async ({ where, data }) => mocks.sessions.set(where.sessionId, data.creds)), + delete: vi.fn(async ({ where }) => mocks.sessions.delete(where.sessionId)), + }, + }, +})); +vi.mock('fs/promises', () => { + const enoent = () => Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); + const fs = { + mkdir: vi.fn(async () => undefined), + stat: vi.fn(async (file: string) => { + if (!mocks.files.has(file)) throw enoent(); + return { isFile: () => true }; + }), + readFile: vi.fn(async (file: string) => { + if (!mocks.files.has(file)) throw enoent(); + return mocks.files.get(file); + }), + writeFile: vi.fn(async (file: string, data: string) => { + mocks.files.set(file, data); + }), + unlink: vi.fn(async (file: string) => { + if (!mocks.files.delete(file)) throw enoent(); + }), + }; + return { default: fs, ...fs }; +}); + +import { prismaRepository } from '@api/server.module'; +import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma'; +import fs from 'fs/promises'; + +const createCache = () => { + const hashes = new Map>(); + const hash = (key: string) => { + if (!hashes.has(key)) hashes.set(key, new Map()); + return hashes.get(key); + }; + return { + hashes, + hSet: vi.fn(async (key: string, field: string, value: any) => { + hash(key).set(field, JSON.stringify(value, BufferJSON.replacer)); + }), + hGet: vi.fn(async (key: string, field: string) => { + const data = hash(key).get(field); + return data ? JSON.parse(data, BufferJSON.reviver) : null; + }), + hDelete: vi.fn(async (key: string, field: string) => { + hash(key).delete(field); + return true; + }), + delete: vi.fn(async (key: string) => { + hashes.delete(key); + }), + }; +}; + +const lastLogger = () => mocks.loggers[mocks.loggers.length - 1]; + +describe('useMultiFileAuthStatePrisma', () => { + let cache: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + mocks.sessions.clear(); + mocks.files.clear(); + mocks.cacheConf.REDIS.ENABLED = false; + cache = createCache(); + }); + + const open = () => useMultiFileAuthStatePrisma('session', cache as any); + + it('creates and persists fresh creds when nothing is stored', async () => { + const { state } = await open(); + + expect(state.creds.registered).toBe(false); + expect(state.creds.noiseKey.private).toHaveLength(32); + expect(prismaRepository.session.create).toHaveBeenCalledTimes(1); + expect(typeof JSON.parse(mocks.sessions.get('session'))).toBe('string'); + }); + + it('restores saved creds with binary fields intact after reopening', async () => { + const first = await open(); + first.state.creds.registered = true; + await first.saveCreds(); + + expect(prismaRepository.session.update).toHaveBeenCalledTimes(1); + + const second = await open(); + + expect(second.state.creds).toEqual(first.state.creds); + expect(second.state.creds.registered).toBe(true); + expect(Buffer.isBuffer(second.state.creds.noiseKey.private)).toBe(true); + expect(Buffer.from(second.state.creds.signedIdentityKey.public)).toEqual( + Buffer.from(first.state.creds.signedIdentityKey.public), + ); + }); + + describe.each([ + ['local files', false], + ['redis', true], + ])('keys stored in %s', (_, redisEnabled) => { + beforeEach(() => { + mocks.cacheConf.REDIS.ENABLED = redisEnabled; + }); + + const storedKeys = () => + redisEnabled ? [...(cache.hashes.get('session')?.keys() ?? [])] : [...mocks.files.keys()]; + + it('round trips pre-keys and sessions through serialized storage', async () => { + const { state } = await open(); + const preKey = { public: Buffer.from([1, 2, 3]), private: new Uint8Array([4, 5, 6]) }; + const session = Buffer.from('session-bytes'); + + await state.keys.set({ 'pre-key': { '1': preKey }, session: { '123@s.whatsapp.net:1': session } }); + + const preKeys = await state.keys.get('pre-key', ['1', '2']); + const sessions = await state.keys.get('session', ['123@s.whatsapp.net:1']); + + expect(Buffer.isBuffer(preKeys['1'].public)).toBe(true); + expect(Buffer.from(preKeys['1'].private)).toEqual(Buffer.from([4, 5, 6])); + expect(preKeys['2']).toBeNull(); + expect(Buffer.from(sessions['123@s.whatsapp.net:1'] as any)).toEqual(session); + if (!redisEnabled) expect(storedKeys()).toContain('/instances/session/session-123@s.whatsapp.net-1.json'); + }); + + it('restores app state sync keys as protobuf messages with byte key data', async () => { + const { state } = await open(); + const keyData = new Uint8Array([9, 8, 7, 6]); + const value = proto.Message.AppStateSyncKeyData.create({ keyData, timestamp: 1700 }); + + await state.keys.set({ 'app-state-sync-key': { AAA: value } }); + + const result = await state.keys.get('app-state-sync-key', ['AAA']); + + expect(result.AAA).toBeInstanceOf(proto.Message.AppStateSyncKeyData); + expect(result.AAA.keyData).toBeInstanceOf(Uint8Array); + expect(Buffer.from(result.AAA.keyData)).toEqual(Buffer.from(keyData)); + }); + + it('decodes app state sync key data persisted as a base64 string', async () => { + const raw = JSON.stringify({ keyData: 'AQID' }); + if (redisEnabled) cache.hashes.set('session', new Map([['app-state-sync-key-BBB', raw]])); + else mocks.files.set('/instances/session/app-state-sync-key-BBB.json', raw); + const { state } = await open(); + + const result = await state.keys.get('app-state-sync-key', ['BBB']); + + expect(result.BBB.keyData).toBeInstanceOf(Uint8Array); + expect(Buffer.from(result.BBB.keyData)).toEqual(Buffer.from([1, 2, 3])); + }); + + it('removes keys set to null or undefined', async () => { + const { state } = await open(); + await state.keys.set({ 'pre-key': { '1': { public: Buffer.from([1]), private: Buffer.from([2]) } } }); + await state.keys.set({ session: { a: Buffer.from([1]) } }); + + await state.keys.set({ 'pre-key': { '1': null }, session: { a: undefined } }); + + expect(storedKeys()).toEqual([]); + expect(await state.keys.get('pre-key', ['1'])).toEqual({ '1': null }); + }); + }); + + it('removes only the database creds on removeCreds without redis and leaves key files behind', async () => { + const { state, removeCreds } = await open(); + await state.keys.set({ session: { a: Buffer.from([1]) } }); + + await removeCreds(); + + expect(mocks.sessions.has('session')).toBe(false); + expect(mocks.files.size).toBe(1); + expect(lastLogger().info).toHaveBeenCalledWith({ action: 'auth.key.delete', sessionId: 'session' }); + }); + + it('clears the redis hash but keeps the database creds on removeCreds with redis', async () => { + mocks.cacheConf.REDIS.ENABLED = true; + const { state, removeCreds } = await open(); + await state.keys.set({ session: { a: Buffer.from([1]) } }); + + await removeCreds(); + + expect(cache.hashes.has('session')).toBe(false); + expect(mocks.sessions.has('session')).toBe(true); + expect(lastLogger().info).toHaveBeenCalledWith({ action: 'redis.delete', sessionId: 'session' }); + }); + + it('logs the redis failure and falls back to deleting database creds', async () => { + mocks.cacheConf.REDIS.ENABLED = true; + const error = new Error('redis down'); + cache.delete.mockRejectedValue(error); + const { removeCreds } = await open(); + + await removeCreds(); + + expect(lastLogger().warn).toHaveBeenCalledWith({ action: 'redis.delete', sessionId: 'session', err: error }); + expect(mocks.sessions.has('session')).toBe(false); + }); + + it('swallows database and file read errors without logging them', async () => { + vi.mocked(prismaRepository.session.findUnique).mockRejectedValueOnce(new Error('db down')); + vi.mocked(fs.readFile).mockRejectedValueOnce(new Error('disk error')); + mocks.files.set('/instances/session/session-a.json', '{}'); + + const { state } = await open(); + const result = await state.keys.get('session', ['a']); + + expect(state.creds.registered).toBe(false); + expect(result).toEqual({ a: null }); + expect(lastLogger().error).not.toHaveBeenCalled(); + }); + + it('swallows database write errors and leaves creds unsaved', async () => { + vi.mocked(prismaRepository.session.create).mockRejectedValue(new Error('db down')); + + const { saveCreds } = await open(); + + await expect(saveCreds()).resolves.toBeUndefined(); + expect(mocks.sessions.has('session')).toBe(false); + expect(lastLogger().error).not.toHaveBeenCalled(); + }); + + it('propagates local file write errors from keys.set', async () => { + const { state } = await open(); + vi.mocked(fs.writeFile).mockRejectedValueOnce(new Error('disk full')); + + await expect(state.keys.set({ session: { a: Buffer.from([1]) } })).rejects.toThrow('disk full'); + }); + + it('swallows unlink errors when removing a missing key file', async () => { + const { state } = await open(); + + await expect(state.keys.set({ session: { missing: null } })).resolves.toBeUndefined(); + }); +}); diff --git a/test/utils/use-multi-file-auth-state-provider-files.test.ts b/test/utils/use-multi-file-auth-state-provider-files.test.ts new file mode 100644 index 0000000000..60141c5fce --- /dev/null +++ b/test/utils/use-multi-file-auth-state-provider-files.test.ts @@ -0,0 +1,184 @@ +import { proto } from 'baileys'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ loggers: [] as any[] })); + +vi.mock('@config/logger.config', () => ({ + Logger: class { + info = vi.fn(); + warn = vi.fn(); + error = vi.fn(); + constructor() { + mocks.loggers.push(this); + } + }, +})); + +import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; + +const createProviderFiles = () => { + const files = new Map(); + const path = (instance: string, key: string) => `${instance}/${key}`; + return { + files, + parseResponses: true, + create: vi.fn(async (): Promise<[any, any?]> => [{ status: 201 }]), + write: vi.fn(async (instance: string, key: string, body: { data: string }): Promise<[any, any?]> => { + files.set(path(instance, key), body.data); + return [{ status: 201, data: 'ok' }]; + }), + read: vi.fn(async function (instance: string, key: string): Promise<[any, any?]> { + const stored = files.get(path(instance, key)); + if (stored === undefined) return [{ status: 404 }, new Error('not found')]; + return [{ status: 200, data: this.parseResponses ? JSON.parse(stored) : stored }]; + }), + delete: vi.fn(async (instance: string, key: string): Promise<[any, any?]> => { + files.delete(path(instance, key)); + return [{ status: 200 }]; + }), + removeSession: vi.fn(async (instance: string): Promise<[any, any?]> => { + for (const key of [...files.keys()]) if (key.startsWith(`${instance}/`)) files.delete(key); + return [{ status: 200, data: 'removed' }]; + }), + }; +}; + +describe('AuthStateProvider', () => { + let providerFiles: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + providerFiles = createProviderFiles(); + }); + + const open = () => new AuthStateProvider(providerFiles as any).authStateProvider('instance'); + + it('starts with fresh creds when nothing is stored', async () => { + const { state } = await open(); + + expect(providerFiles.create).toHaveBeenCalledWith('instance'); + expect(state.creds.registered).toBe(false); + expect(state.creds.noiseKey.private).toHaveLength(32); + }); + + it('restores saved creds with binary fields intact after reopening', async () => { + const first = await open(); + await first.saveCreds(); + + expect(typeof providerFiles.files.get('instance/creds')).toBe('string'); + + const second = await open(); + + expect(second.state.creds).toEqual(first.state.creds); + expect(Buffer.isBuffer(second.state.creds.noiseKey.private)).toBe(true); + expect(Buffer.from(second.state.creds.signedIdentityKey.public)).toEqual( + Buffer.from(first.state.creds.signedIdentityKey.public), + ); + }); + + it('returns creds as an unparsed string when the provider responds with raw text', async () => { + const first = await open(); + await first.saveCreds(); + providerFiles.parseResponses = false; + + const second = await open(); + + expect(typeof second.state.creds).toBe('string'); + }); + + it('round trips pre-keys and sessions through serialized storage', async () => { + const { state } = await open(); + const preKey = { public: Buffer.from([1, 2, 3]), private: new Uint8Array([4, 5, 6]) }; + const session = Buffer.from('session-bytes'); + + await state.keys.set({ 'pre-key': { '1': preKey }, session: { 'jid.0': session } }); + + const preKeys = await state.keys.get('pre-key', ['1', '2']); + const sessions = await state.keys.get('session', ['jid.0']); + + expect(Buffer.isBuffer(preKeys['1'].public)).toBe(true); + expect(Buffer.from(preKeys['1'].private)).toEqual(Buffer.from([4, 5, 6])); + expect(preKeys['2']).toBeUndefined(); + expect(Buffer.from(sessions['jid.0'] as any)).toEqual(session); + }); + + it('restores app state sync keys as protobuf messages with byte key data', async () => { + const { state } = await open(); + const keyData = new Uint8Array([9, 8, 7, 6]); + const value = proto.Message.AppStateSyncKeyData.create({ keyData, timestamp: 1700 }); + + await state.keys.set({ 'app-state-sync-key': { AAA: value } }); + + expect(JSON.parse(providerFiles.files.get('instance/app-state-sync-key-AAA')).keyData).toBe('CQgHBg=='); + + const result = await state.keys.get('app-state-sync-key', ['AAA']); + + expect(result.AAA).toBeInstanceOf(proto.Message.AppStateSyncKeyData); + expect(result.AAA.keyData).toBeInstanceOf(Uint8Array); + expect(Buffer.from(result.AAA.keyData)).toEqual(Buffer.from(keyData)); + }); + + it('decodes app state sync key data persisted as a base64 string', async () => { + providerFiles.files.set('instance/app-state-sync-key-BBB', JSON.stringify({ keyData: 'AQID' })); + const { state } = await open(); + + const result = await state.keys.get('app-state-sync-key', ['BBB']); + + expect(result.BBB.keyData).toBeInstanceOf(Uint8Array); + expect(Buffer.from(result.BBB.keyData)).toEqual(Buffer.from([1, 2, 3])); + }); + + it('removes keys set to null or undefined', async () => { + const { state } = await open(); + await state.keys.set({ 'pre-key': { '1': { public: Buffer.from([1]), private: Buffer.from([2]) } } }); + await state.keys.set({ session: { a: Buffer.from([1]) } }); + + await state.keys.set({ 'pre-key': { '1': null }, session: { a: undefined } }); + + expect(providerFiles.delete).toHaveBeenCalledWith('instance', 'pre-key-1'); + expect(providerFiles.delete).toHaveBeenCalledWith('instance', 'session-a'); + expect(providerFiles.files.size).toBe(0); + }); + + it('removes the whole session on removeCreds', async () => { + const { saveCreds, removeCreds } = await open(); + await saveCreds(); + + await removeCreds(); + + expect(providerFiles.removeSession).toHaveBeenCalledWith('instance'); + expect(providerFiles.files.size).toBe(0); + expect(mocks.loggers.some((logger) => logger.info.mock.calls.length > 0)).toBe(true); + }); + + it('returns undefined and logs when the session folder cannot be created', async () => { + const error = new Error('provider offline'); + providerFiles.create.mockResolvedValue([{ status: 500 }, error]); + + const result = await open(); + + expect(result).toBeUndefined(); + expect(mocks.loggers.some((logger) => logger.error.mock.calls.length > 0)).toBe(true); + }); + + it('swallows read, write and delete errors without logging them', async () => { + const error = new Error('provider down'); + providerFiles.read.mockResolvedValue([{ status: 500 }, error]); + providerFiles.write.mockResolvedValue([{ status: 500 }, error]); + providerFiles.delete.mockResolvedValue([{ status: 500 }, error]); + providerFiles.removeSession.mockResolvedValue([{ status: 500 }, error]); + + const { state, saveCreds, removeCreds } = await open(); + + expect(state.creds.registered).toBe(false); + await expect(saveCreds()).resolves.toBeUndefined(); + await expect(state.keys.set({ session: { a: Buffer.from([1]), b: null } })).resolves.toBeUndefined(); + await expect(state.keys.get('session', ['a'])).resolves.toEqual({ a: undefined }); + await expect(removeCreds()).resolves.toBeUndefined(); + + for (const logger of mocks.loggers) { + expect(logger.error).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); + } + }); +}); diff --git a/test/utils/use-multi-file-auth-state-redis-db.test.ts b/test/utils/use-multi-file-auth-state-redis-db.test.ts new file mode 100644 index 0000000000..8928427780 --- /dev/null +++ b/test/utils/use-multi-file-auth-state-redis-db.test.ts @@ -0,0 +1,173 @@ +import { BufferJSON, initAuthCreds, proto } from 'baileys'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ loggers: [] as any[] })); + +vi.mock('@config/logger.config', () => ({ + Logger: class { + info = vi.fn(); + warn = vi.fn(); + error = vi.fn(); + constructor() { + mocks.loggers.push(this); + } + }, +})); + +import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; + +const createCache = () => { + const hashes = new Map>(); + const hash = (key: string) => { + if (!hashes.has(key)) hashes.set(key, new Map()); + return hashes.get(key); + }; + return { + hashes, + hSet: vi.fn(async (key: string, field: string, value: any) => { + hash(key).set(field, JSON.stringify(value, BufferJSON.replacer)); + }), + hGet: vi.fn(async (key: string, field: string) => { + const data = hash(key).get(field); + return data ? JSON.parse(data, BufferJSON.reviver) : null; + }), + hDelete: vi.fn(async (key: string, field: string) => { + hash(key).delete(field); + return true; + }), + delete: vi.fn(async (key: string) => { + hashes.delete(key); + }), + }; +}; + +const lastLogger = () => mocks.loggers[mocks.loggers.length - 1]; + +describe('useMultiFileAuthStateRedisDb', () => { + let cache: ReturnType; + + beforeEach(() => { + mocks.loggers.length = 0; + cache = createCache(); + }); + + const open = () => useMultiFileAuthStateRedisDb('instance', cache as any); + + it('starts with fresh creds when nothing is stored', async () => { + const { state } = await open(); + + expect(state.creds.registered).toBe(false); + expect(state.creds.noiseKey.private).toHaveLength(32); + expect(cache.hashes.get('instance')?.has('creds')).toBe(false); + }); + + it('restores saved creds with binary fields intact after reopening', async () => { + const first = await open(); + await first.saveCreds(); + + const stored = cache.hashes.get('instance').get('creds'); + expect(typeof stored).toBe('string'); + + const second = await open(); + + expect(second.state.creds).toEqual(first.state.creds); + expect(Buffer.isBuffer(second.state.creds.noiseKey.private)).toBe(true); + expect(Buffer.from(second.state.creds.signedIdentityKey.public)).toEqual( + Buffer.from(first.state.creds.signedIdentityKey.public), + ); + }); + + it('round trips pre-keys and sessions through serialized storage', async () => { + const { state } = await open(); + const preKey = { public: Buffer.from([1, 2, 3]), private: new Uint8Array([4, 5, 6]) }; + const session = Buffer.from('session-bytes'); + + await state.keys.set({ 'pre-key': { '1': preKey }, session: { 'jid.0': session } }); + + const preKeys = await state.keys.get('pre-key', ['1', '2']); + const sessions = await state.keys.get('session', ['jid.0']); + + expect(Buffer.isBuffer(preKeys['1'].public)).toBe(true); + expect(Buffer.from(preKeys['1'].private)).toEqual(Buffer.from([4, 5, 6])); + expect(preKeys['2']).toBeNull(); + expect(Buffer.from(sessions['jid.0'] as any)).toEqual(session); + }); + + it('restores app state sync keys as protobuf messages with byte key data', async () => { + const { state } = await open(); + const keyData = new Uint8Array([9, 8, 7, 6]); + const value = proto.Message.AppStateSyncKeyData.create({ keyData, timestamp: 1700 }); + + await state.keys.set({ 'app-state-sync-key': { AAA: value } }); + + expect(JSON.parse(cache.hashes.get('instance').get('app-state-sync-key-AAA')).keyData).toBe('CQgHBg=='); + + const result = await state.keys.get('app-state-sync-key', ['AAA']); + + expect(result.AAA).toBeInstanceOf(proto.Message.AppStateSyncKeyData); + expect(result.AAA.keyData).toBeInstanceOf(Uint8Array); + expect(Buffer.from(result.AAA.keyData)).toEqual(Buffer.from(keyData)); + }); + + it('decodes app state sync key data persisted as a base64 string', async () => { + cache.hashes.set('instance', new Map([['app-state-sync-key-BBB', JSON.stringify({ keyData: 'AQID' })]])); + const { state } = await open(); + + const result = await state.keys.get('app-state-sync-key', ['BBB']); + + expect(result.BBB.keyData).toBeInstanceOf(Uint8Array); + expect(Buffer.from(result.BBB.keyData)).toEqual(Buffer.from([1, 2, 3])); + }); + + it('removes keys set to null or undefined', async () => { + const { state } = await open(); + await state.keys.set({ 'pre-key': { '1': { public: Buffer.from([1]), private: Buffer.from([2]) } } }); + await state.keys.set({ session: { a: Buffer.from([1]) } }); + + await state.keys.set({ 'pre-key': { '1': null }, session: { a: undefined } }); + + expect(cache.hDelete).toHaveBeenCalledWith('instance', 'pre-key-1'); + expect(cache.hDelete).toHaveBeenCalledWith('instance', 'session-a'); + expect(await state.keys.get('pre-key', ['1'])).toEqual({ '1': null }); + expect(await state.keys.get('session', ['a'])).toEqual({ a: null }); + }); + + it('clears the whole instance hash on removeCreds', async () => { + const { saveCreds, removeCreds } = await open(); + await saveCreds(); + + await removeCreds(); + + expect(cache.delete).toHaveBeenCalledWith('instance'); + expect(cache.hashes.has('instance')).toBe(false); + expect(lastLogger().warn).toHaveBeenCalledWith({ action: 'redis.delete', instanceName: 'instance' }); + }); + + it('logs and swallows read, write and delete errors', async () => { + const error = new Error('redis down'); + cache.hGet.mockRejectedValue(error); + cache.hSet.mockRejectedValue(error); + cache.hDelete.mockRejectedValue(error); + cache.delete.mockRejectedValue(error); + + const { state, saveCreds, removeCreds } = await open(); + const logger = lastLogger(); + + expect(state.creds.registered).toBe(false); + await expect(saveCreds()).resolves.toBeUndefined(); + await expect(state.keys.set({ session: { a: Buffer.from([1]), b: null } })).resolves.toBeUndefined(); + await expect(state.keys.get('session', ['a'])).resolves.toEqual({ a: undefined }); + await expect(removeCreds()).resolves.toBeUndefined(); + + expect(logger.error).toHaveBeenCalledWith({ localError: 'readData', error }); + expect(logger.error).toHaveBeenCalledWith({ localError: 'writeData', error }); + expect(logger.error).toHaveBeenCalledWith({ readData: 'removeData', error }); + }); + + it('keeps the initial creds object when a fresh instance has nothing stored', async () => { + const fresh = initAuthCreds(); + const { state } = await open(); + + expect(Object.keys(state.creds).sort()).toEqual(Object.keys(fresh).sort()); + }); +}); From 74b5121a85d576c06a8a029576449680b8fb9760 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:10:53 -0300 Subject: [PATCH 20/96] refactor(baileys): extract the @lid to phone JID resolution for MESSAGES_UPSERT Moves the remoteJid -> remoteJidAlt replacement applied before the MESSAGES_UPSERT webhook into resolvePhoneRemoteJid, so the rule the consumer products depend on is unit-tested. Behavior is unchanged: remoteJid is replaced only when it is a @lid and remoteJidAlt is present. --- .../whatsapp/whatsapp.baileys.service.ts | 6 ++-- src/utils/resolvePhoneRemoteJid.ts | 7 ++++ test/utils/resolvePhoneRemoteJid.test.ts | 34 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/utils/resolvePhoneRemoteJid.ts create mode 100644 test/utils/resolvePhoneRemoteJid.test.ts diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc1..c13891593b 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -85,6 +85,7 @@ import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent'; import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; import { status } from '@utils/renderStatus'; +import { resolvePhoneRemoteJid } from '@utils/resolvePhoneRemoteJid'; import { sendTelemetry } from '@utils/sendTelemetry'; import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma'; import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; @@ -1475,8 +1476,9 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.verbose(messageRaw); sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`); - if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) { - messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt; + const phoneRemoteJid = resolvePhoneRemoteJid(messageRaw.key); + if (phoneRemoteJid !== messageRaw.key.remoteJid) { + messageRaw.key.remoteJid = phoneRemoteJid; } console.log(messageRaw); diff --git a/src/utils/resolvePhoneRemoteJid.ts b/src/utils/resolvePhoneRemoteJid.ts new file mode 100644 index 0000000000..fce8595ce4 --- /dev/null +++ b/src/utils/resolvePhoneRemoteJid.ts @@ -0,0 +1,7 @@ +export function resolvePhoneRemoteJid(key: { remoteJid?: string; remoteJidAlt?: string }): string | undefined { + if (key.remoteJid?.includes('@lid') && key.remoteJidAlt) { + return key.remoteJidAlt; + } + + return key.remoteJid; +} diff --git a/test/utils/resolvePhoneRemoteJid.test.ts b/test/utils/resolvePhoneRemoteJid.test.ts new file mode 100644 index 0000000000..9be24a42ab --- /dev/null +++ b/test/utils/resolvePhoneRemoteJid.test.ts @@ -0,0 +1,34 @@ +import { resolvePhoneRemoteJid } from '@utils/resolvePhoneRemoteJid'; +import { describe, expect, it } from 'vitest'; + +describe('resolvePhoneRemoteJid', () => { + it('replaces a @lid remoteJid with the phone JID from remoteJidAlt', () => { + expect( + resolvePhoneRemoteJid({ remoteJid: '14714994171909@lid', remoteJidAlt: '5492616841080@s.whatsapp.net' }), + ).toBe('5492616841080@s.whatsapp.net'); + }); + + it('keeps a @lid remoteJid when remoteJidAlt is missing', () => { + expect(resolvePhoneRemoteJid({ remoteJid: '14714994171909@lid' })).toBe('14714994171909@lid'); + }); + + it('keeps a @lid remoteJid when remoteJidAlt is empty', () => { + expect(resolvePhoneRemoteJid({ remoteJid: '14714994171909@lid', remoteJidAlt: '' })).toBe('14714994171909@lid'); + }); + + it('keeps a phone remoteJid even when remoteJidAlt is present', () => { + expect( + resolvePhoneRemoteJid({ remoteJid: '5492616841080@s.whatsapp.net', remoteJidAlt: '14714994171909@lid' }), + ).toBe('5492616841080@s.whatsapp.net'); + }); + + it('keeps a group remoteJid', () => { + expect(resolvePhoneRemoteJid({ remoteJid: '120363204608479348@g.us', remoteJidAlt: undefined })).toBe( + '120363204608479348@g.us', + ); + }); + + it('returns undefined when there is no remoteJid', () => { + expect(resolvePhoneRemoteJid({ remoteJidAlt: '5492616841080@s.whatsapp.net' })).toBeUndefined(); + }); +}); From d7913779c76bcb957d72ef1565d53cee4bc3c78d Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:17:54 -0300 Subject: [PATCH 21/96] chore(deps): upgrade lint-staged to 17 and run CI on Node 24 lint-staged 17 replaces micromatch with picomatch, removing one dependency path to braces@3.0.3 (GHSA-vfj7-8cjw-p6xm, no patched release). It requires Node >= 22.22.1, so CI moves from the end-of-life Node 20 to 24, matching the Dockerfile. braces is still pulled in by patch-package via find-yarn-workspace-root. --- .github/workflows/check_code_quality.yml | 2 +- .github/workflows/purple-checks.yml | 2 +- package-lock.json | 449 ++--------------------- package.json | 2 +- 4 files changed, 26 insertions(+), 429 deletions(-) diff --git a/.github/workflows/check_code_quality.yml b/.github/workflows/check_code_quality.yml index df156f2117..ef6315a6fd 100644 --- a/.github/workflows/check_code_quality.yml +++ b/.github/workflows/check_code_quality.yml @@ -19,7 +19,7 @@ jobs: - name: Install Node uses: actions/setup-node@v5 with: - node-version: 20.x + node-version: 24.x - name: Cache node modules uses: actions/cache@v4 diff --git a/.github/workflows/purple-checks.yml b/.github/workflows/purple-checks.yml index 895bf9a578..11c2886546 100644 --- a/.github/workflows/purple-checks.yml +++ b/.github/workflows/purple-checks.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/setup-node@v5 with: - node-version: 20.x + node-version: 24.x cache: npm - name: Install packages diff --git a/package-lock.json b/package-lock.json index 4775b8e3a2..89888f6e6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,7 +95,7 @@ "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-simple-import-sort": "^12.1.1", "husky": "^9.1.7", - "lint-staged": "^16.1.6", + "lint-staged": "^17.6.0", "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", @@ -6501,69 +6501,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-truncate": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", - "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/cli-width": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", @@ -6630,13 +6567,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -6649,16 +6579,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/commitizen": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.3.1.tgz", @@ -7625,19 +7545,6 @@ "node": ">=6" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -10224,22 +10131,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -10901,111 +10792,40 @@ } }, "node_modules/lint-staged": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", - "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.6.0.tgz", + "integrity": "sha512-nOhBfYkZEwkFtNzD5pK9s9LkcWoxJ+whLzEnhqi9nl7zTB5n7tg8+9iYgMGx2Wm3VlLu8cbNj0RACzY+nWi5iQ==", "dev": true, "license": "MIT", "dependencies": { - "commander": "^14.0.2", - "listr2": "^9.0.5", - "micromatch": "^4.0.8", - "nano-spawn": "^2.0.0", - "pidtree": "^0.6.0", + "picomatch": "^4.0.7", "string-argv": "^0.3.2", - "yaml": "^2.8.1" + "tinyexec": "^1.3.1" }, "bin": { "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": ">=20.17" + "node": ">=22.22.1" }, "funding": { "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "yaml": "^2.9.1" } }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/lint-staged/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/load-tsconfig": { @@ -11237,169 +11057,6 @@ "node": ">=8" } }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", - "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -11695,19 +11352,6 @@ "node": ">=6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -11939,19 +11583,6 @@ "thenify-all": "^1.0.0" } }, - "node_modules/nano-spawn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", - "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" - } - }, "node_modules/nanoid": { "version": "3.3.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", @@ -13028,19 +12659,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/pino": { "version": "9.14.0", "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", @@ -13961,13 +13579,6 @@ "node": ">=0.10.0" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -14496,23 +14107,6 @@ "node": ">=6" } }, - "node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -15177,9 +14771,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", "license": "MIT", "engines": { "node": ">=18" @@ -16848,15 +16442,18 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index 3496283e9d..40f41c136b 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,7 @@ "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-simple-import-sort": "^12.1.1", "husky": "^9.1.7", - "lint-staged": "^16.1.6", + "lint-staged": "^17.6.0", "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", From f513827d3e699c24cb09469e1da941892c2c343f Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:36:09 -0300 Subject: [PATCH 22/96] test: make the unit suite order-independent and shuffle every run A persistent mockRejectedValue on the Prisma session fake leaked into later tests, so the suite only passed in default order. restoreMocks resets every vi.fn to its original implementation before each test, unstubEnvs and unstubGlobals undo stubs, and sequence.shuffle randomizes order locally and in CI so hidden coupling fails fast. Manual vi.clearAllMocks calls are now redundant. --- test/utils/onWhatsappCache.test.ts | 2 -- test/utils/use-multi-file-auth-state-prisma.test.ts | 1 - test/utils/use-multi-file-auth-state-provider-files.test.ts | 1 - vitest.config.ts | 4 ++++ 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/utils/onWhatsappCache.test.ts b/test/utils/onWhatsappCache.test.ts index 42d5f466f2..06edc5daf4 100644 --- a/test/utils/onWhatsappCache.test.ts +++ b/test/utils/onWhatsappCache.test.ts @@ -26,7 +26,6 @@ describe('getOnWhatsappCache', () => { beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-01-15T12:00:00.000Z')); - vi.clearAllMocks(); mocks.database.SAVE_DATA.IS_ON_WHATSAPP = true; mocks.database.SAVE_DATA.IS_ON_WHATSAPP_DAYS = 7; isOnWhatsapp.findMany.mockResolvedValue([]); @@ -113,7 +112,6 @@ describe('getOnWhatsappCache', () => { describe('saveOnWhatsappCache', () => { beforeEach(() => { - vi.clearAllMocks(); mocks.database.SAVE_DATA.IS_ON_WHATSAPP = true; isOnWhatsapp.findFirst.mockResolvedValue(null); }); diff --git a/test/utils/use-multi-file-auth-state-prisma.test.ts b/test/utils/use-multi-file-auth-state-prisma.test.ts index 4ddbb3bf29..2c06c2cda7 100644 --- a/test/utils/use-multi-file-auth-state-prisma.test.ts +++ b/test/utils/use-multi-file-auth-state-prisma.test.ts @@ -92,7 +92,6 @@ describe('useMultiFileAuthStatePrisma', () => { let cache: ReturnType; beforeEach(() => { - vi.clearAllMocks(); mocks.sessions.clear(); mocks.files.clear(); mocks.cacheConf.REDIS.ENABLED = false; diff --git a/test/utils/use-multi-file-auth-state-provider-files.test.ts b/test/utils/use-multi-file-auth-state-provider-files.test.ts index 60141c5fce..b119374467 100644 --- a/test/utils/use-multi-file-auth-state-provider-files.test.ts +++ b/test/utils/use-multi-file-auth-state-provider-files.test.ts @@ -47,7 +47,6 @@ describe('AuthStateProvider', () => { let providerFiles: ReturnType; beforeEach(() => { - vi.clearAllMocks(); providerFiles = createProviderFiles(); }); diff --git a/vitest.config.ts b/vitest.config.ts index 7f85e1d2ce..e50f86fed4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -18,5 +18,9 @@ export default defineConfig({ test: { include: ['test/**/*.test.ts'], environment: 'node', + restoreMocks: true, + unstubEnvs: true, + unstubGlobals: true, + sequence: { shuffle: true }, }, }); From 2aed552397fd35670a33891c5fac8e8245ed56ca Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:39:16 -0300 Subject: [PATCH 23/96] test: mark pinned bugs as KNOWN BUG with it.fails contracts Characterization tests asserted buggy behavior as if it were the contract. Each pinned bug is now named KNOWN BUG (current behavior) and paired with an it.fails asserting the correct behavior, so fixing the bug flips the it.fails and forces both into one normal test. createJid AR/MX keeps only the KNOWN BUG name because the correct rule is unclear. AGENTS.md documents the convention. --- AGENTS.md | 5 ++ test/utils/advancedOperatorsSearch.test.ts | 6 ++- test/utils/createJid.test.ts | 4 +- test/utils/findBotByTrigger.test.ts | 11 +++- test/utils/getConversationMessage.test.ts | 14 +++++- test/utils/onWhatsappCache.test.ts | 50 +++++++++++++++++-- .../use-multi-file-auth-state-prisma.test.ts | 34 +++++++++++-- ...lti-file-auth-state-provider-files.test.ts | 30 ++++++++++- ...use-multi-file-auth-state-redis-db.test.ts | 21 +++++++- 9 files changed, 159 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2b98aa4d9e..e9bfafe105 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -131,6 +131,11 @@ make check-docs # docs drift guard (pointers, skill frontmatter) Behavior that needs a real WhatsApp account (pairing, sending, receiving) requires a human with a phone; report it as unverified rather than faking it. When tests are added, they ship with the behavior. +Known bugs in tests: a test that pins buggy behavior is named `it('KNOWN BUG: (current behavior)')` +and sits next to an `it.fails('')` asserting the intended behavior. When you fix the bug, the +`it.fails` starts failing; replace both with one normal test for the correct contract. Skip the `it.fails` only when +the correct contract is unclear (e.g. Argentine/Mexican numbers in `createJid`). + ## Documentation conventions 1. This file is an index: hard rules + pointers. Implementation detail goes to skills. diff --git a/test/utils/advancedOperatorsSearch.test.ts b/test/utils/advancedOperatorsSearch.test.ts index 603f52bc4c..8d9ed376fc 100644 --- a/test/utils/advancedOperatorsSearch.test.ts +++ b/test/utils/advancedOperatorsSearch.test.ts @@ -50,7 +50,11 @@ describe('advancedOperatorsSearch', () => { expect(advancedOperatorsSearch('time 10:30', 'endswith:10:30')).toBe(true); }); - it('splits multi word values into separate filters', () => { + it('KNOWN BUG: never matches values that contain spaces (current behavior)', () => { expect(advancedOperatorsSearch('good morning', 'exact:good morning')).toBe(false); }); + + it.fails('matches values that contain spaces', () => { + expect(advancedOperatorsSearch('good morning', 'exact:good morning')).toBe(true); + }); }); diff --git a/test/utils/createJid.test.ts b/test/utils/createJid.test.ts index 21fe142bb4..4a0065c2e4 100644 --- a/test/utils/createJid.test.ts +++ b/test/utils/createJid.test.ts @@ -34,11 +34,11 @@ describe('createJid', () => { expect(createJid('12036301234567890')).toBe('12036301234567890@s.whatsapp.net'); }); - it('removes the third digit from 13 digit Argentine numbers', () => { + it('KNOWN BUG: removes the third digit from 13 digit Argentine numbers (current behavior)', () => { expect(createJid('5492616107398')).toBe('542616107398@s.whatsapp.net'); }); - it('removes the third digit from 13 digit Mexican numbers', () => { + it('KNOWN BUG: removes the third digit from 13 digit Mexican numbers (current behavior)', () => { expect(createJid('5215512345678')).toBe('525512345678@s.whatsapp.net'); }); diff --git a/test/utils/findBotByTrigger.test.ts b/test/utils/findBotByTrigger.test.ts index 6b9d43b355..73d6a6376e 100644 --- a/test/utils/findBotByTrigger.test.ts +++ b/test/utils/findBotByTrigger.test.ts @@ -124,7 +124,7 @@ describe('findBotByTrigger', () => { expect((await findBotByTrigger(repository, 'aba', 'inst'))?.id).toBe('re'); }); - it('throws when a stored regex is invalid', async () => { + it('KNOWN BUG: rejects the whole lookup when a stored regex is invalid (current behavior)', async () => { const repository = createRepository([ { id: 're', triggerType: 'keyword', triggerOperator: 'regex', triggerValue: '(' }, ]); @@ -132,6 +132,15 @@ describe('findBotByTrigger', () => { await expect(findBotByTrigger(repository, 'x', 'inst')).rejects.toThrow(SyntaxError); }); + it.fails('skips a bot with an invalid stored regex and keeps searching', async () => { + const repository = createRepository([ + { id: 're', triggerType: 'keyword', triggerOperator: 'regex', triggerValue: '(' }, + { id: 'ct', triggerType: 'keyword', triggerOperator: 'contains', triggerValue: 'x' }, + ]); + + expect((await findBotByTrigger(repository, 'x', 'inst'))?.id).toBe('ct'); + }); + it('returns null when nothing matches', async () => { expect(await findBotByTrigger(createRepository([]), 'hi', 'inst')).toBeNull(); }); diff --git a/test/utils/getConversationMessage.test.ts b/test/utils/getConversationMessage.test.ts index 63d5527c49..5cc1d154ed 100644 --- a/test/utils/getConversationMessage.test.ts +++ b/test/utils/getConversationMessage.test.ts @@ -95,10 +95,14 @@ describe('getConversationMessage', () => { ).toBe('-32.89'); }); - it('throws for location messages without latitude', () => { + it('KNOWN BUG: throws for location messages without latitude (current behavior)', () => { expect(() => getConversationMessage({ key, message: { locationMessage: {} } })).toThrow(TypeError); }); + it.fails('does not throw for location messages without latitude', () => { + expect(() => getConversationMessage({ key, message: { locationMessage: {} } })).not.toThrow(); + }); + it('returns the contact display name', () => { expect(getConversationMessage({ key, message: { contactMessage: { displayName: 'Ana' } } })).toBe('Ana'); }); @@ -144,12 +148,18 @@ describe('getConversationMessage', () => { ).toBe('hi\nexternalAdReplyBody|Ad text'); }); - it('prefixes the message type name when the external ad reply body is the only content', () => { + it('KNOWN BUG: duplicates the external ad reply body when it is the only content (current behavior)', () => { expect(getConversationMessage({ key, message: {}, contextInfo: { externalAdReply: { body: 'Ad' } } })).toBe( 'externalAdReplyBody\nexternalAdReplyBody|Ad', ); }); + it.fails('returns only the external ad reply body when it is the only content', () => { + expect(getConversationMessage({ key, message: {}, contextInfo: { externalAdReply: { body: 'Ad' } } })).toBe( + 'externalAdReplyBody|Ad', + ); + }); + it('returns the first matching type when several are present', () => { expect(getConversationMessage({ key, message: { conversation: 'text', imageMessage: {} } })).toBe('text'); }); diff --git a/test/utils/onWhatsappCache.test.ts b/test/utils/onWhatsappCache.test.ts index 06edc5daf4..bfd6083753 100644 --- a/test/utils/onWhatsappCache.test.ts +++ b/test/utils/onWhatsappCache.test.ts @@ -91,23 +91,35 @@ describe('getOnWhatsappCache', () => { expect(queriedJids()).toEqual(['5215512345678@s.whatsapp.net', '525512345678@s.whatsapp.net']); }); - it('searches other numbers without the plus sign but with the domain appended twice', async () => { + it('KNOWN BUG: appends the domain twice to numbers outside Brazil, Mexico and Argentina (current behavior)', async () => { await getOnWhatsappCache(['+15551234567@s.whatsapp.net']); expect(queriedJids()).toEqual(['15551234567@s.whatsapp.net@s.whatsapp.net']); }); + it.fails('searches other numbers without the plus sign and with a single domain', async () => { + await getOnWhatsappCache(['+15551234567@s.whatsapp.net']); + + expect(queriedJids()).toEqual(['15551234567@s.whatsapp.net']); + }); + it('searches lid and group jids as given', async () => { await getOnWhatsappCache(['123456@lid', '5511999999999-1623281429@g.us']); expect(queriedJids()).toEqual(['123456@lid', '5511999999999-1623281429@g.us']); }); - it('appends an undefined domain to numbers without a domain', async () => { + it('KNOWN BUG: appends an undefined domain to numbers without a domain (current behavior)', async () => { await getOnWhatsappCache(['15551234567']); expect(queriedJids()).toEqual(['15551234567@undefined']); }); + + it.fails('searches numbers without a domain as user jids', async () => { + await getOnWhatsappCache(['15551234567']); + + expect(queriedJids()).toEqual(['15551234567@s.whatsapp.net']); + }); }); describe('saveOnWhatsappCache', () => { @@ -142,7 +154,7 @@ describe('saveOnWhatsappCache', () => { }); }); - it('looks up existing records by jid options and remote jid', async () => { + it('KNOWN BUG: looks up existing records by a jid option with the domain appended twice (current behavior)', async () => { await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); expect(isOnWhatsapp.findFirst).toHaveBeenCalledWith({ @@ -155,6 +167,16 @@ describe('saveOnWhatsappCache', () => { }); }); + it.fails('looks up existing records by jid options and remote jid', async () => { + await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); + + expect(isOnWhatsapp.findFirst).toHaveBeenCalledWith({ + where: { + OR: [{ jidOptions: { contains: '15551234567@s.whatsapp.net' } }, { remoteJid: '15551234567@s.whatsapp.net' }], + }, + }); + }); + it('adds a lid alternate jid and marks the record as lid', async () => { await saveOnWhatsappCache([{ remoteJid: '5511987654321@s.whatsapp.net', remoteJidAlt: '999@lid', lid: 'lid' }]); @@ -195,7 +217,7 @@ describe('saveOnWhatsappCache', () => { expect(isOnWhatsapp.create).not.toHaveBeenCalled(); }); - it('merges existing jid options into an update when data differs', async () => { + it('KNOWN BUG: merges a jid option with the domain appended twice into the update (current behavior)', async () => { isOnWhatsapp.findFirst.mockResolvedValue({ id: 'r1', remoteJid: '15551234567@s.whatsapp.net', @@ -215,6 +237,26 @@ describe('saveOnWhatsappCache', () => { }); }); + it.fails('merges existing jid options into an update when data differs', async () => { + isOnWhatsapp.findFirst.mockResolvedValue({ + id: 'r1', + remoteJid: '15551234567@s.whatsapp.net', + jidOptions: '888@lid', + lid: null, + }); + + await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); + + expect(isOnWhatsapp.update).toHaveBeenCalledWith({ + where: { id: 'r1' }, + data: { + remoteJid: '15551234567@s.whatsapp.net', + jidOptions: '15551234567@s.whatsapp.net,888@lid', + lid: null, + }, + }); + }); + it('keeps processing other items when one fails', async () => { isOnWhatsapp.findFirst.mockRejectedValueOnce(new Error('db down')).mockResolvedValueOnce(null); diff --git a/test/utils/use-multi-file-auth-state-prisma.test.ts b/test/utils/use-multi-file-auth-state-prisma.test.ts index 2c06c2cda7..8a2b29cdc2 100644 --- a/test/utils/use-multi-file-auth-state-prisma.test.ts +++ b/test/utils/use-multi-file-auth-state-prisma.test.ts @@ -192,7 +192,7 @@ describe('useMultiFileAuthStatePrisma', () => { }); }); - it('removes only the database creds on removeCreds without redis and leaves key files behind', async () => { + it('KNOWN BUG: leaves local key files behind on removeCreds without redis (current behavior)', async () => { const { state, removeCreds } = await open(); await state.keys.set({ session: { a: Buffer.from([1]) } }); @@ -203,7 +203,17 @@ describe('useMultiFileAuthStatePrisma', () => { expect(lastLogger().info).toHaveBeenCalledWith({ action: 'auth.key.delete', sessionId: 'session' }); }); - it('clears the redis hash but keeps the database creds on removeCreds with redis', async () => { + it.fails('removes the database creds and local key files on removeCreds without redis', async () => { + const { state, removeCreds } = await open(); + await state.keys.set({ session: { a: Buffer.from([1]) } }); + + await removeCreds(); + + expect(mocks.sessions.has('session')).toBe(false); + expect(mocks.files.size).toBe(0); + }); + + it('KNOWN BUG: keeps the database creds on removeCreds with redis (current behavior)', async () => { mocks.cacheConf.REDIS.ENABLED = true; const { state, removeCreds } = await open(); await state.keys.set({ session: { a: Buffer.from([1]) } }); @@ -215,6 +225,17 @@ describe('useMultiFileAuthStatePrisma', () => { expect(lastLogger().info).toHaveBeenCalledWith({ action: 'redis.delete', sessionId: 'session' }); }); + it.fails('clears the redis hash and the database creds on removeCreds with redis', async () => { + mocks.cacheConf.REDIS.ENABLED = true; + const { state, removeCreds } = await open(); + await state.keys.set({ session: { a: Buffer.from([1]) } }); + + await removeCreds(); + + expect(cache.hashes.has('session')).toBe(false); + expect(mocks.sessions.has('session')).toBe(false); + }); + it('logs the redis failure and falls back to deleting database creds', async () => { mocks.cacheConf.REDIS.ENABLED = true; const error = new Error('redis down'); @@ -250,13 +271,20 @@ describe('useMultiFileAuthStatePrisma', () => { expect(lastLogger().error).not.toHaveBeenCalled(); }); - it('propagates local file write errors from keys.set', async () => { + it('KNOWN BUG: rejects keys.set when a local file write fails (current behavior)', async () => { const { state } = await open(); vi.mocked(fs.writeFile).mockRejectedValueOnce(new Error('disk full')); await expect(state.keys.set({ session: { a: Buffer.from([1]) } })).rejects.toThrow('disk full'); }); + it.fails('swallows local file write errors in keys.set like other storage errors', async () => { + const { state } = await open(); + vi.mocked(fs.writeFile).mockRejectedValueOnce(new Error('disk full')); + + await expect(state.keys.set({ session: { a: Buffer.from([1]) } })).resolves.toBeUndefined(); + }); + it('swallows unlink errors when removing a missing key file', async () => { const { state } = await open(); diff --git a/test/utils/use-multi-file-auth-state-provider-files.test.ts b/test/utils/use-multi-file-auth-state-provider-files.test.ts index b119374467..0fec65a7dd 100644 --- a/test/utils/use-multi-file-auth-state-provider-files.test.ts +++ b/test/utils/use-multi-file-auth-state-provider-files.test.ts @@ -75,7 +75,7 @@ describe('AuthStateProvider', () => { ); }); - it('returns creds as an unparsed string when the provider responds with raw text', async () => { + it('KNOWN BUG: returns creds as an unparsed string when the provider responds with raw text (current behavior)', async () => { const first = await open(); await first.saveCreds(); providerFiles.parseResponses = false; @@ -85,6 +85,16 @@ describe('AuthStateProvider', () => { expect(typeof second.state.creds).toBe('string'); }); + it.fails('parses creds when the provider responds with raw text', async () => { + const first = await open(); + await first.saveCreds(); + providerFiles.parseResponses = false; + + const second = await open(); + + expect(second.state.creds).toEqual(first.state.creds); + }); + it('round trips pre-keys and sessions through serialized storage', async () => { const { state } = await open(); const preKey = { public: Buffer.from([1, 2, 3]), private: new Uint8Array([4, 5, 6]) }; @@ -160,7 +170,7 @@ describe('AuthStateProvider', () => { expect(mocks.loggers.some((logger) => logger.error.mock.calls.length > 0)).toBe(true); }); - it('swallows read, write and delete errors without logging them', async () => { + it('KNOWN BUG: swallows read, write and delete errors without logging them (current behavior)', async () => { const error = new Error('provider down'); providerFiles.read.mockResolvedValue([{ status: 500 }, error]); providerFiles.write.mockResolvedValue([{ status: 500 }, error]); @@ -180,4 +190,20 @@ describe('AuthStateProvider', () => { expect(logger.info).not.toHaveBeenCalled(); } }); + + it.fails('logs read, write and delete errors', async () => { + const error = new Error('provider down'); + providerFiles.read.mockResolvedValue([{ status: 500 }, error]); + providerFiles.write.mockResolvedValue([{ status: 500 }, error]); + providerFiles.delete.mockResolvedValue([{ status: 500 }, error]); + providerFiles.removeSession.mockResolvedValue([{ status: 500 }, error]); + + const { state, saveCreds, removeCreds } = await open(); + await saveCreds(); + await state.keys.set({ session: { a: Buffer.from([1]), b: null } }); + await state.keys.get('session', ['a']); + await removeCreds(); + + expect(mocks.loggers.some((logger) => logger.error.mock.calls.length > 0)).toBe(true); + }); }); diff --git a/test/utils/use-multi-file-auth-state-redis-db.test.ts b/test/utils/use-multi-file-auth-state-redis-db.test.ts index 8928427780..0aa56ef896 100644 --- a/test/utils/use-multi-file-auth-state-redis-db.test.ts +++ b/test/utils/use-multi-file-auth-state-redis-db.test.ts @@ -161,7 +161,26 @@ describe('useMultiFileAuthStateRedisDb', () => { expect(logger.error).toHaveBeenCalledWith({ localError: 'readData', error }); expect(logger.error).toHaveBeenCalledWith({ localError: 'writeData', error }); - expect(logger.error).toHaveBeenCalledWith({ readData: 'removeData', error }); + }); + + it('KNOWN BUG: logs removeData errors under a readData key (current behavior)', async () => { + const error = new Error('redis down'); + cache.hDelete.mockRejectedValue(error); + const { state } = await open(); + + await state.keys.set({ session: { b: null } }); + + expect(lastLogger().error).toHaveBeenCalledWith({ readData: 'removeData', error }); + }); + + it.fails('logs removeData errors under the localError key', async () => { + const error = new Error('redis down'); + cache.hDelete.mockRejectedValue(error); + const { state } = await open(); + + await state.keys.set({ session: { b: null } }); + + expect(lastLogger().error).toHaveBeenCalledWith({ localError: 'removeData', error }); }); it('keeps the initial creds object when a fresh instance has nothing stored', async () => { From ba26cf962dd1de794760d2bf6ddf0a68e2ac4fd0 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:42:52 -0300 Subject: [PATCH 24/96] test(auth-state): run cache tests through the real CacheService The cache fake encoded once and rejected on errors, while production double-encodes (CacheService + RedisCache) and CacheService swallows inner errors. test/helpers now provides an in-memory ICache with RedisCache's serialization and a down mode behind the real CacheService, plus shared Logger and fs/promises fakes. With Redis down, saveCreds resolves, nothing persists and reopening yields fresh creds. The removeData log-key KNOWN BUG is dropped: CacheService.hDelete catches its own errors, so that path is unreachable. --- test/helpers/cache.ts | 90 +++++++++++++ test/helpers/fs-promises.ts | 25 ++++ test/helpers/logger.ts | 19 +++ .../use-multi-file-auth-state-prisma.test.ts | 124 ++++++------------ ...lti-file-auth-state-provider-files.test.ts | 23 +--- ...use-multi-file-auth-state-redis-db.test.ts | 115 +++++----------- 6 files changed, 217 insertions(+), 179 deletions(-) create mode 100644 test/helpers/cache.ts create mode 100644 test/helpers/fs-promises.ts create mode 100644 test/helpers/logger.ts diff --git a/test/helpers/cache.ts b/test/helpers/cache.ts new file mode 100644 index 0000000000..145e8b08be --- /dev/null +++ b/test/helpers/cache.ts @@ -0,0 +1,90 @@ +import { ICache } from '@api/abstract/abstract.cache'; +import { CacheService } from '@api/services/cache.service'; +import { BufferJSON } from 'baileys'; +import { vi } from 'vitest'; + +export class InMemoryRedisCache implements ICache { + readonly hashes = new Map>(); + readonly values = new Map(); + down = false; + readonly error = new Error('redis down'); + + private hash(key: string) { + if (!this.hashes.has(key)) this.hashes.set(key, new Map()); + return this.hashes.get(key); + } + + private ensureUp() { + if (this.down) throw this.error; + } + + async get(key: string) { + this.ensureUp(); + return JSON.parse(this.values.get(key) ?? 'null'); + } + + async hGet(key: string, field: string) { + this.ensureUp(); + const data = this.hashes.get(key)?.get(field); + if (data) { + return JSON.parse(data, BufferJSON.reviver); + } + return null; + } + + async set(key: string, value: any) { + this.ensureUp(); + this.values.set(key, JSON.stringify(value)); + } + + async hSet(key: string, field: string, value: any) { + this.ensureUp(); + this.hash(key).set(field, JSON.stringify(value, BufferJSON.replacer)); + } + + async has(key: string) { + this.ensureUp(); + return this.values.has(key) || this.hashes.has(key); + } + + async delete(key: string) { + this.ensureUp(); + const existed = this.values.delete(key) || this.hashes.delete(key); + return existed ? 1 : 0; + } + + async hDelete(key: string, field: string) { + this.ensureUp(); + return this.hashes.get(key)?.delete(field) ? 1 : 0; + } + + async keys() { + this.ensureUp(); + return [...new Set([...this.values.keys(), ...this.hashes.keys()])]; + } + + async deleteAll() { + this.ensureUp(); + const count = this.values.size + this.hashes.size; + this.values.clear(); + this.hashes.clear(); + return count; + } + + readRaw(key: string, field: string) { + const data = this.hashes.get(key)?.get(field); + return data === undefined ? undefined : JSON.parse(data, BufferJSON.reviver); + } + + writeRaw(key: string, field: string, raw: string) { + this.hash(key).set(field, JSON.stringify(raw, BufferJSON.replacer)); + } +} + +export const createCacheService = () => { + const inner = new InMemoryRedisCache(); + const service = new CacheService(inner); + vi.spyOn(service, 'hDelete'); + vi.spyOn(service, 'delete'); + return { inner, service }; +}; diff --git a/test/helpers/fs-promises.ts b/test/helpers/fs-promises.ts new file mode 100644 index 0000000000..ce0a5273e9 --- /dev/null +++ b/test/helpers/fs-promises.ts @@ -0,0 +1,25 @@ +import { vi } from 'vitest'; + +export const files = new Map(); + +const enoent = () => Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); + +const fs = { + mkdir: vi.fn(async () => undefined), + stat: vi.fn(async (file: string) => { + if (!files.has(file)) throw enoent(); + return { isFile: () => true }; + }), + readFile: vi.fn(async (file: string) => { + if (!files.has(file)) throw enoent(); + return files.get(file); + }), + writeFile: vi.fn(async (file: string, data: string) => { + files.set(file, data); + }), + unlink: vi.fn(async (file: string) => { + if (!files.delete(file)) throw enoent(); + }), +}; + +export const fsPromisesModule = { default: fs, ...fs }; diff --git a/test/helpers/logger.ts b/test/helpers/logger.ts new file mode 100644 index 0000000000..96c46127a5 --- /dev/null +++ b/test/helpers/logger.ts @@ -0,0 +1,19 @@ +import { vi } from 'vitest'; + +export const loggers: any[] = []; + +export const lastLogger = () => loggers[loggers.length - 1]; + +export const loggersNamed = (name: string) => loggers.filter((logger) => logger.context === name); + +export class Logger { + verbose = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + error = vi.fn(); + constructor(public context?: string) { + loggers.push(this); + } +} + +export const loggerModule = { Logger }; diff --git a/test/utils/use-multi-file-auth-state-prisma.test.ts b/test/utils/use-multi-file-auth-state-prisma.test.ts index 8a2b29cdc2..05a812dcf2 100644 --- a/test/utils/use-multi-file-auth-state-prisma.test.ts +++ b/test/utils/use-multi-file-auth-state-prisma.test.ts @@ -1,26 +1,18 @@ -import { BufferJSON, proto } from 'baileys'; +import { proto } from 'baileys'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createCacheService, InMemoryRedisCache } from '../helpers/cache'; +import { files } from '../helpers/fs-promises'; +import { loggersNamed } from '../helpers/logger'; + const mocks = vi.hoisted(() => ({ - loggers: [] as any[], cacheConf: { REDIS: { ENABLED: false } }, sessions: new Map(), - files: new Map(), })); -vi.mock('@config/logger.config', () => ({ - Logger: class { - info = vi.fn(); - warn = vi.fn(); - error = vi.fn(); - constructor() { - mocks.loggers.push(this); - } - }, -})); +vi.mock('@config/logger.config', async () => (await import('../helpers/logger')).loggerModule); vi.mock('@config/env.config', () => ({ configService: { get: vi.fn(() => mocks.cacheConf) } })); vi.mock('@config/path.config', () => ({ INSTANCE_DIR: '/instances' })); -vi.mock('@api/services/cache.service', () => ({ CacheService: class {} })); vi.mock('@api/server.module', () => ({ prismaRepository: { session: { @@ -35,70 +27,26 @@ vi.mock('@api/server.module', () => ({ }, }, })); -vi.mock('fs/promises', () => { - const enoent = () => Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); - const fs = { - mkdir: vi.fn(async () => undefined), - stat: vi.fn(async (file: string) => { - if (!mocks.files.has(file)) throw enoent(); - return { isFile: () => true }; - }), - readFile: vi.fn(async (file: string) => { - if (!mocks.files.has(file)) throw enoent(); - return mocks.files.get(file); - }), - writeFile: vi.fn(async (file: string, data: string) => { - mocks.files.set(file, data); - }), - unlink: vi.fn(async (file: string) => { - if (!mocks.files.delete(file)) throw enoent(); - }), - }; - return { default: fs, ...fs }; -}); +vi.mock('fs/promises', async () => (await import('../helpers/fs-promises')).fsPromisesModule); import { prismaRepository } from '@api/server.module'; +import { CacheService } from '@api/services/cache.service'; import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma'; import fs from 'fs/promises'; -const createCache = () => { - const hashes = new Map>(); - const hash = (key: string) => { - if (!hashes.has(key)) hashes.set(key, new Map()); - return hashes.get(key); - }; - return { - hashes, - hSet: vi.fn(async (key: string, field: string, value: any) => { - hash(key).set(field, JSON.stringify(value, BufferJSON.replacer)); - }), - hGet: vi.fn(async (key: string, field: string) => { - const data = hash(key).get(field); - return data ? JSON.parse(data, BufferJSON.reviver) : null; - }), - hDelete: vi.fn(async (key: string, field: string) => { - hash(key).delete(field); - return true; - }), - delete: vi.fn(async (key: string) => { - hashes.delete(key); - }), - }; -}; - -const lastLogger = () => mocks.loggers[mocks.loggers.length - 1]; - describe('useMultiFileAuthStatePrisma', () => { - let cache: ReturnType; + let inner: InMemoryRedisCache; + let cache: CacheService; beforeEach(() => { mocks.sessions.clear(); - mocks.files.clear(); + files.clear(); mocks.cacheConf.REDIS.ENABLED = false; - cache = createCache(); + ({ inner, service: cache } = createCacheService()); }); - const open = () => useMultiFileAuthStatePrisma('session', cache as any); + const open = () => useMultiFileAuthStatePrisma('session', cache); + const authLogger = () => loggersNamed('useMultiFileAuthStatePrisma')[0]; it('creates and persists fresh creds when nothing is stored', async () => { const { state } = await open(); @@ -134,8 +82,7 @@ describe('useMultiFileAuthStatePrisma', () => { mocks.cacheConf.REDIS.ENABLED = redisEnabled; }); - const storedKeys = () => - redisEnabled ? [...(cache.hashes.get('session')?.keys() ?? [])] : [...mocks.files.keys()]; + const storedKeys = () => (redisEnabled ? [...(inner.hashes.get('session')?.keys() ?? [])] : [...files.keys()]); it('round trips pre-keys and sessions through serialized storage', async () => { const { state } = await open(); @@ -170,8 +117,8 @@ describe('useMultiFileAuthStatePrisma', () => { it('decodes app state sync key data persisted as a base64 string', async () => { const raw = JSON.stringify({ keyData: 'AQID' }); - if (redisEnabled) cache.hashes.set('session', new Map([['app-state-sync-key-BBB', raw]])); - else mocks.files.set('/instances/session/app-state-sync-key-BBB.json', raw); + if (redisEnabled) inner.writeRaw('session', 'app-state-sync-key-BBB', raw); + else files.set('/instances/session/app-state-sync-key-BBB.json', raw); const { state } = await open(); const result = await state.keys.get('app-state-sync-key', ['BBB']); @@ -199,8 +146,8 @@ describe('useMultiFileAuthStatePrisma', () => { await removeCreds(); expect(mocks.sessions.has('session')).toBe(false); - expect(mocks.files.size).toBe(1); - expect(lastLogger().info).toHaveBeenCalledWith({ action: 'auth.key.delete', sessionId: 'session' }); + expect(files.size).toBe(1); + expect(authLogger().info).toHaveBeenCalledWith({ action: 'auth.key.delete', sessionId: 'session' }); }); it.fails('removes the database creds and local key files on removeCreds without redis', async () => { @@ -210,7 +157,7 @@ describe('useMultiFileAuthStatePrisma', () => { await removeCreds(); expect(mocks.sessions.has('session')).toBe(false); - expect(mocks.files.size).toBe(0); + expect(files.size).toBe(0); }); it('KNOWN BUG: keeps the database creds on removeCreds with redis (current behavior)', async () => { @@ -220,9 +167,9 @@ describe('useMultiFileAuthStatePrisma', () => { await removeCreds(); - expect(cache.hashes.has('session')).toBe(false); + expect(inner.hashes.has('session')).toBe(false); expect(mocks.sessions.has('session')).toBe(true); - expect(lastLogger().info).toHaveBeenCalledWith({ action: 'redis.delete', sessionId: 'session' }); + expect(authLogger().info).toHaveBeenCalledWith({ action: 'redis.delete', sessionId: 'session' }); }); it.fails('clears the redis hash and the database creds on removeCreds with redis', async () => { @@ -232,33 +179,46 @@ describe('useMultiFileAuthStatePrisma', () => { await removeCreds(); - expect(cache.hashes.has('session')).toBe(false); + expect(inner.hashes.has('session')).toBe(false); expect(mocks.sessions.has('session')).toBe(false); }); it('logs the redis failure and falls back to deleting database creds', async () => { mocks.cacheConf.REDIS.ENABLED = true; - const error = new Error('redis down'); - cache.delete.mockRejectedValue(error); const { removeCreds } = await open(); + inner.down = true; await removeCreds(); - expect(lastLogger().warn).toHaveBeenCalledWith({ action: 'redis.delete', sessionId: 'session', err: error }); + expect(authLogger().warn).toHaveBeenCalledWith({ action: 'redis.delete', sessionId: 'session', err: inner.error }); expect(mocks.sessions.has('session')).toBe(false); }); + it('drops redis key writes and reads without throwing when redis is down', async () => { + mocks.cacheConf.REDIS.ENABLED = true; + const { state } = await open(); + inner.down = true; + + await expect(state.keys.set({ session: { a: Buffer.from([1]), b: null } })).resolves.toBeUndefined(); + await expect(state.keys.get('session', ['a'])).resolves.toEqual({ a: null }); + inner.down = false; + + expect(inner.hashes.get('session')?.has('session-a')).toBeFalsy(); + expect(await state.keys.get('session', ['a'])).toEqual({ a: null }); + expect(loggersNamed('CacheService').at(-1).error).toHaveBeenCalledWith(inner.error); + }); + it('swallows database and file read errors without logging them', async () => { vi.mocked(prismaRepository.session.findUnique).mockRejectedValueOnce(new Error('db down')); vi.mocked(fs.readFile).mockRejectedValueOnce(new Error('disk error')); - mocks.files.set('/instances/session/session-a.json', '{}'); + files.set('/instances/session/session-a.json', '{}'); const { state } = await open(); const result = await state.keys.get('session', ['a']); expect(state.creds.registered).toBe(false); expect(result).toEqual({ a: null }); - expect(lastLogger().error).not.toHaveBeenCalled(); + expect(authLogger().error).not.toHaveBeenCalled(); }); it('swallows database write errors and leaves creds unsaved', async () => { @@ -268,7 +228,7 @@ describe('useMultiFileAuthStatePrisma', () => { await expect(saveCreds()).resolves.toBeUndefined(); expect(mocks.sessions.has('session')).toBe(false); - expect(lastLogger().error).not.toHaveBeenCalled(); + expect(authLogger().error).not.toHaveBeenCalled(); }); it('KNOWN BUG: rejects keys.set when a local file write fails (current behavior)', async () => { diff --git a/test/utils/use-multi-file-auth-state-provider-files.test.ts b/test/utils/use-multi-file-auth-state-provider-files.test.ts index 0fec65a7dd..39f539f7fc 100644 --- a/test/utils/use-multi-file-auth-state-provider-files.test.ts +++ b/test/utils/use-multi-file-auth-state-provider-files.test.ts @@ -1,18 +1,9 @@ import { proto } from 'baileys'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -const mocks = vi.hoisted(() => ({ loggers: [] as any[] })); - -vi.mock('@config/logger.config', () => ({ - Logger: class { - info = vi.fn(); - warn = vi.fn(); - error = vi.fn(); - constructor() { - mocks.loggers.push(this); - } - }, -})); +import { loggers } from '../helpers/logger'; + +vi.mock('@config/logger.config', async () => (await import('../helpers/logger')).loggerModule); import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; @@ -157,7 +148,7 @@ describe('AuthStateProvider', () => { expect(providerFiles.removeSession).toHaveBeenCalledWith('instance'); expect(providerFiles.files.size).toBe(0); - expect(mocks.loggers.some((logger) => logger.info.mock.calls.length > 0)).toBe(true); + expect(loggers.some((logger) => logger.info.mock.calls.length > 0)).toBe(true); }); it('returns undefined and logs when the session folder cannot be created', async () => { @@ -167,7 +158,7 @@ describe('AuthStateProvider', () => { const result = await open(); expect(result).toBeUndefined(); - expect(mocks.loggers.some((logger) => logger.error.mock.calls.length > 0)).toBe(true); + expect(loggers.some((logger) => logger.error.mock.calls.length > 0)).toBe(true); }); it('KNOWN BUG: swallows read, write and delete errors without logging them (current behavior)', async () => { @@ -185,7 +176,7 @@ describe('AuthStateProvider', () => { await expect(state.keys.get('session', ['a'])).resolves.toEqual({ a: undefined }); await expect(removeCreds()).resolves.toBeUndefined(); - for (const logger of mocks.loggers) { + for (const logger of loggers) { expect(logger.error).not.toHaveBeenCalled(); expect(logger.info).not.toHaveBeenCalled(); } @@ -204,6 +195,6 @@ describe('AuthStateProvider', () => { await state.keys.get('session', ['a']); await removeCreds(); - expect(mocks.loggers.some((logger) => logger.error.mock.calls.length > 0)).toBe(true); + expect(loggers.some((logger) => logger.error.mock.calls.length > 0)).toBe(true); }); }); diff --git a/test/utils/use-multi-file-auth-state-redis-db.test.ts b/test/utils/use-multi-file-auth-state-redis-db.test.ts index 0aa56ef896..35f436bb12 100644 --- a/test/utils/use-multi-file-auth-state-redis-db.test.ts +++ b/test/utils/use-multi-file-auth-state-redis-db.test.ts @@ -1,72 +1,38 @@ -import { BufferJSON, initAuthCreds, proto } from 'baileys'; +import { initAuthCreds, proto } from 'baileys'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -const mocks = vi.hoisted(() => ({ loggers: [] as any[] })); +import { createCacheService, InMemoryRedisCache } from '../helpers/cache'; +import { lastLogger, loggers, loggersNamed } from '../helpers/logger'; -vi.mock('@config/logger.config', () => ({ - Logger: class { - info = vi.fn(); - warn = vi.fn(); - error = vi.fn(); - constructor() { - mocks.loggers.push(this); - } - }, -})); +vi.mock('@config/logger.config', async () => (await import('../helpers/logger')).loggerModule); +import { CacheService } from '@api/services/cache.service'; import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; -const createCache = () => { - const hashes = new Map>(); - const hash = (key: string) => { - if (!hashes.has(key)) hashes.set(key, new Map()); - return hashes.get(key); - }; - return { - hashes, - hSet: vi.fn(async (key: string, field: string, value: any) => { - hash(key).set(field, JSON.stringify(value, BufferJSON.replacer)); - }), - hGet: vi.fn(async (key: string, field: string) => { - const data = hash(key).get(field); - return data ? JSON.parse(data, BufferJSON.reviver) : null; - }), - hDelete: vi.fn(async (key: string, field: string) => { - hash(key).delete(field); - return true; - }), - delete: vi.fn(async (key: string) => { - hashes.delete(key); - }), - }; -}; - -const lastLogger = () => mocks.loggers[mocks.loggers.length - 1]; - describe('useMultiFileAuthStateRedisDb', () => { - let cache: ReturnType; + let inner: InMemoryRedisCache; + let cache: CacheService; beforeEach(() => { - mocks.loggers.length = 0; - cache = createCache(); + loggers.length = 0; + ({ inner, service: cache } = createCacheService()); }); - const open = () => useMultiFileAuthStateRedisDb('instance', cache as any); + const open = () => useMultiFileAuthStateRedisDb('instance', cache); it('starts with fresh creds when nothing is stored', async () => { const { state } = await open(); expect(state.creds.registered).toBe(false); expect(state.creds.noiseKey.private).toHaveLength(32); - expect(cache.hashes.get('instance')?.has('creds')).toBe(false); + expect(inner.hashes.get('instance')?.has('creds')).toBeFalsy(); }); it('restores saved creds with binary fields intact after reopening', async () => { const first = await open(); await first.saveCreds(); - const stored = cache.hashes.get('instance').get('creds'); - expect(typeof stored).toBe('string'); + expect(typeof inner.readRaw('instance', 'creds')).toBe('string'); const second = await open(); @@ -100,7 +66,7 @@ describe('useMultiFileAuthStateRedisDb', () => { await state.keys.set({ 'app-state-sync-key': { AAA: value } }); - expect(JSON.parse(cache.hashes.get('instance').get('app-state-sync-key-AAA')).keyData).toBe('CQgHBg=='); + expect(JSON.parse(inner.readRaw('instance', 'app-state-sync-key-AAA')).keyData).toBe('CQgHBg=='); const result = await state.keys.get('app-state-sync-key', ['AAA']); @@ -110,7 +76,7 @@ describe('useMultiFileAuthStateRedisDb', () => { }); it('decodes app state sync key data persisted as a base64 string', async () => { - cache.hashes.set('instance', new Map([['app-state-sync-key-BBB', JSON.stringify({ keyData: 'AQID' })]])); + inner.writeRaw('instance', 'app-state-sync-key-BBB', JSON.stringify({ keyData: 'AQID' })); const { state } = await open(); const result = await state.keys.get('app-state-sync-key', ['BBB']); @@ -139,48 +105,35 @@ describe('useMultiFileAuthStateRedisDb', () => { await removeCreds(); expect(cache.delete).toHaveBeenCalledWith('instance'); - expect(cache.hashes.has('instance')).toBe(false); + expect(inner.hashes.has('instance')).toBe(false); expect(lastLogger().warn).toHaveBeenCalledWith({ action: 'redis.delete', instanceName: 'instance' }); }); - it('logs and swallows read, write and delete errors', async () => { - const error = new Error('redis down'); - cache.hGet.mockRejectedValue(error); - cache.hSet.mockRejectedValue(error); - cache.hDelete.mockRejectedValue(error); - cache.delete.mockRejectedValue(error); - - const { state, saveCreds, removeCreds } = await open(); - const logger = lastLogger(); - - expect(state.creds.registered).toBe(false); - await expect(saveCreds()).resolves.toBeUndefined(); - await expect(state.keys.set({ session: { a: Buffer.from([1]), b: null } })).resolves.toBeUndefined(); - await expect(state.keys.get('session', ['a'])).resolves.toEqual({ a: undefined }); - await expect(removeCreds()).resolves.toBeUndefined(); - - expect(logger.error).toHaveBeenCalledWith({ localError: 'readData', error }); - expect(logger.error).toHaveBeenCalledWith({ localError: 'writeData', error }); - }); - - it('KNOWN BUG: logs removeData errors under a readData key (current behavior)', async () => { - const error = new Error('redis down'); - cache.hDelete.mockRejectedValue(error); - const { state } = await open(); + it('silently drops saved creds when redis is down so reopening yields fresh creds', async () => { + inner.down = true; + const first = await open(); + first.state.creds.registered = true; - await state.keys.set({ session: { b: null } }); + await expect(first.saveCreds()).resolves.toBeUndefined(); + inner.down = false; + const second = await open(); - expect(lastLogger().error).toHaveBeenCalledWith({ readData: 'removeData', error }); + expect(inner.hashes.get('instance')?.has('creds')).toBeFalsy(); + expect(second.state.creds.registered).toBe(false); + expect(second.state.creds.noiseKey.private).not.toEqual(first.state.creds.noiseKey.private); }); - it.fails('logs removeData errors under the localError key', async () => { - const error = new Error('redis down'); - cache.hDelete.mockRejectedValue(error); - const { state } = await open(); + it('swallows read, write and delete errors inside CacheService when redis is down', async () => { + inner.down = true; + const { state, removeCreds } = await open(); - await state.keys.set({ session: { b: null } }); + expect(state.creds.registered).toBe(false); + await expect(state.keys.set({ session: { a: Buffer.from([1]), b: null } })).resolves.toBeUndefined(); + await expect(state.keys.get('session', ['a'])).resolves.toEqual({ a: null }); + await expect(removeCreds()).resolves.toBeUndefined(); - expect(lastLogger().error).toHaveBeenCalledWith({ localError: 'removeData', error }); + expect(loggersNamed('CacheService')[0].error).toHaveBeenCalledWith(inner.error); + expect(loggersNamed('useMultiFileAuthStateRedisDb')[0].error).not.toHaveBeenCalled(); }); it('keeps the initial creds object when a fresh instance has nothing stored', async () => { From 4680bd10e0ea24d5f74f6270afbad551413b5ebd Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:46:21 -0300 Subject: [PATCH 25/96] test(utils): cover the branches reviewers found unprotected Adds cases that fail when their target line is mutated: onWhatsappCache isDataSame and null jidOptions updates, getConversationMessage view-once and list fallbacks, query-side accent stripping and empty queries, createJid unknown domains, short dashed ids and device-suffixed broadcasts, fixFileName slashes, no creds persisted on Redis open, exact provider-files log payloads, and findBotByTrigger enabled/instanceId filtering. A @lid substring that is not a suffix is pinned as a KNOWN BUG in resolvePhoneRemoteJid. --- test/utils/advancedOperatorsSearch.test.ts | 8 +++ test/utils/createJid.test.ts | 9 +++ test/utils/findBotByTrigger.test.ts | 67 +++++++++++++------ test/utils/getConversationMessage.test.ts | 24 +++++++ test/utils/onWhatsappCache.test.ts | 60 +++++++++++++++++ test/utils/resolvePhoneRemoteJid.test.ts | 12 ++++ .../use-multi-file-auth-state-prisma.test.ts | 8 +++ ...lti-file-auth-state-provider-files.test.ts | 14 +++- ...use-multi-file-auth-state-redis-db.test.ts | 11 +-- 9 files changed, 185 insertions(+), 28 deletions(-) diff --git a/test/utils/advancedOperatorsSearch.test.ts b/test/utils/advancedOperatorsSearch.test.ts index 8d9ed376fc..1814107682 100644 --- a/test/utils/advancedOperatorsSearch.test.ts +++ b/test/utils/advancedOperatorsSearch.test.ts @@ -24,6 +24,14 @@ describe('advancedOperatorsSearch', () => { expect(advancedOperatorsSearch('Ação rápida', 'exact:acao')).toBe(false); }); + it('strips accents from the query value', () => { + expect(advancedOperatorsSearch('acao', 'contains:Ação')).toBe(true); + }); + + it('returns false for an empty query', () => { + expect(advancedOperatorsSearch('hello', '')).toBe(false); + }); + it('accepts operator names in any case', () => { expect(advancedOperatorsSearch('hello', 'CONTAINS:ell')).toBe(true); }); diff --git a/test/utils/createJid.test.ts b/test/utils/createJid.test.ts index 4a0065c2e4..1a56a44dd1 100644 --- a/test/utils/createJid.test.ts +++ b/test/utils/createJid.test.ts @@ -12,6 +12,15 @@ describe('createJid', () => { it('strips the device suffix before passing a jid through', () => { expect(createJid('5511987654321:12@s.whatsapp.net')).toBe('5511987654321@s.whatsapp.net'); expect(createJid('123456789:3@lid')).toBe('123456789@lid'); + expect(createJid('status:1@broadcast')).toBe('status@broadcast'); + }); + + it('drops unknown domains and builds a user jid', () => { + expect(createJid('15551234567@x1.net')).toBe('15551234567@s.whatsapp.net'); + }); + + it('treats dashed ids shorter than 24 characters as numbers', () => { + expect(createJid('123-456')).toBe('123456@s.whatsapp.net'); }); it('removes plus signs, spaces and parentheses from plain numbers', () => { diff --git a/test/utils/findBotByTrigger.test.ts b/test/utils/findBotByTrigger.test.ts index 73d6a6376e..a80f301e49 100644 --- a/test/utils/findBotByTrigger.test.ts +++ b/test/utils/findBotByTrigger.test.ts @@ -1,28 +1,41 @@ import { findBotByTrigger } from '@utils/findBotByTrigger'; import { describe, expect, it, vi } from 'vitest'; -type Bot = { id: string; triggerType: string; triggerOperator?: string; triggerValue?: string }; - -const createRepository = (bots: Bot[]) => ({ - findFirst: vi.fn(async ({ where }) => { - if (where.triggerType?.in) return bots.find((bot) => where.triggerType.in.includes(bot.triggerType)) ?? null; - return ( - bots.find( +type Bot = { + id: string; + triggerType: string; + triggerOperator?: string; + triggerValue?: string; + enabled?: boolean; + instanceId?: string; +}; + +const createRepository = (seed: Bot[]) => { + const bots = seed.map((bot) => ({ enabled: true, instanceId: 'inst', ...bot })); + const scoped = (where: any) => + bots.filter((bot) => bot.enabled === where.enabled && bot.instanceId === where.instanceId); + return { + findFirst: vi.fn(async ({ where }) => { + if (where.triggerType?.in) + return scoped(where).find((bot) => where.triggerType.in.includes(bot.triggerType)) ?? null; + return ( + scoped(where).find( + (bot) => + bot.triggerType === where.triggerType && + bot.triggerOperator === where.triggerOperator && + bot.triggerValue === where.triggerValue, + ) ?? null + ); + }), + findMany: vi.fn(async ({ where }) => + scoped(where).filter( (bot) => bot.triggerType === where.triggerType && - bot.triggerOperator === where.triggerOperator && - bot.triggerValue === where.triggerValue, - ) ?? null - ); - }), - findMany: vi.fn(async ({ where }) => - bots.filter( - (bot) => - bot.triggerType === where.triggerType && - (where.triggerOperator === undefined || bot.triggerOperator === where.triggerOperator), + (where.triggerOperator === undefined || bot.triggerOperator === where.triggerOperator), + ), ), - ), -}); + }; +}; describe('findBotByTrigger', () => { it('returns a bot with trigger type all before any other bot', async () => { @@ -31,7 +44,21 @@ describe('findBotByTrigger', () => { { id: 'all', triggerType: 'all' }, ]); - expect(await findBotByTrigger(repository, 'hi', 'inst')).toEqual({ id: 'all', triggerType: 'all' }); + expect(await findBotByTrigger(repository, 'hi', 'inst')).toMatchObject({ id: 'all', triggerType: 'all' }); + }); + + it('ignores disabled bots and bots from other instances', async () => { + const repository = createRepository([ + { id: 'off', triggerType: 'all', enabled: false }, + { id: 'other', triggerType: 'all', instanceId: 'inst-2' }, + { id: 'adv-off', triggerType: 'advanced', triggerValue: 'contains:hi', enabled: false }, + { id: 'eq-other', triggerType: 'keyword', triggerOperator: 'equals', triggerValue: 'hi', instanceId: 'inst-2' }, + { id: 'ct-off', triggerType: 'keyword', triggerOperator: 'contains', triggerValue: 'h', enabled: false }, + { id: 'ct', triggerType: 'keyword', triggerOperator: 'contains', triggerValue: 'i' }, + ]); + + expect((await findBotByTrigger(repository, 'hi', 'inst'))?.id).toBe('ct'); + expect(await findBotByTrigger(repository, 'hi', 'inst-3')).toBeNull(); }); it('returns a bot with trigger type none', async () => { diff --git a/test/utils/getConversationMessage.test.ts b/test/utils/getConversationMessage.test.ts index 5cc1d154ed..cfa7645495 100644 --- a/test/utils/getConversationMessage.test.ts +++ b/test/utils/getConversationMessage.test.ts @@ -138,6 +138,30 @@ describe('getConversationMessage', () => { ).toBe('u1'); }); + it('falls back to view once video and audio urls', () => { + expect( + getConversationMessage({ key, message: { viewOnceMessageV2: { message: { videoMessage: { url: 'v1' } } } } }), + ).toBe('v1'); + expect( + getConversationMessage({ key, message: { viewOnceMessageV2: { message: { audioMessage: { url: 'a1' } } } } }), + ).toBe('a1'); + }); + + it('falls back to a top level list response title', () => { + expect(getConversationMessage({ key, message: {}, listResponseMessage: { title: 'Top' } })).toBe('Top'); + }); + + it('uses the key id when S3 is enabled and a view once video is present without video saving', () => { + s3.ENABLE = true; + + expect( + getConversationMessage({ + key, + message: { viewOnceMessageV2: { message: { videoMessage: {} } }, audioMessage: {}, mediaUrl: 'https://s3/a' }, + }), + ).toBe('audioMessage|MSG1'); + }); + it('appends the external ad reply body on a new line', () => { expect( getConversationMessage({ diff --git a/test/utils/onWhatsappCache.test.ts b/test/utils/onWhatsappCache.test.ts index bfd6083753..eb22d508d4 100644 --- a/test/utils/onWhatsappCache.test.ts +++ b/test/utils/onWhatsappCache.test.ts @@ -217,6 +217,66 @@ describe('saveOnWhatsappCache', () => { expect(isOnWhatsapp.create).not.toHaveBeenCalled(); }); + it('updates when only the lid flag changes', async () => { + isOnWhatsapp.findFirst.mockResolvedValue({ + id: 'r1', + remoteJid: '5511987654321@s.whatsapp.net', + jidOptions: '5511987654321@s.whatsapp.net,551187654321@s.whatsapp.net', + lid: null, + }); + + await saveOnWhatsappCache([{ remoteJid: '5511987654321@s.whatsapp.net', lid: 'lid' }]); + + expect(isOnWhatsapp.update).toHaveBeenCalledWith({ + where: { id: 'r1' }, + data: { + remoteJid: '5511987654321@s.whatsapp.net', + jidOptions: '551187654321@s.whatsapp.net,5511987654321@s.whatsapp.net', + lid: 'lid', + }, + }); + }); + + it('updates when only the remote jid changes', async () => { + isOnWhatsapp.findFirst.mockResolvedValue({ + id: 'r1', + remoteJid: '551187654321@s.whatsapp.net', + jidOptions: '5511987654321@s.whatsapp.net,551187654321@s.whatsapp.net', + lid: null, + }); + + await saveOnWhatsappCache([{ remoteJid: '5511987654321@s.whatsapp.net' }]); + + expect(isOnWhatsapp.update).toHaveBeenCalledWith({ + where: { id: 'r1' }, + data: { + remoteJid: '5511987654321@s.whatsapp.net', + jidOptions: '551187654321@s.whatsapp.net,5511987654321@s.whatsapp.net', + lid: null, + }, + }); + }); + + it('updates an existing record without jid options with the expanded options', async () => { + isOnWhatsapp.findFirst.mockResolvedValue({ + id: 'r1', + remoteJid: '5511987654321@s.whatsapp.net', + jidOptions: null, + lid: null, + }); + + await saveOnWhatsappCache([{ remoteJid: '5511987654321@s.whatsapp.net' }]); + + expect(isOnWhatsapp.update).toHaveBeenCalledWith({ + where: { id: 'r1' }, + data: { + remoteJid: '5511987654321@s.whatsapp.net', + jidOptions: '551187654321@s.whatsapp.net,5511987654321@s.whatsapp.net', + lid: null, + }, + }); + }); + it('KNOWN BUG: merges a jid option with the domain appended twice into the update (current behavior)', async () => { isOnWhatsapp.findFirst.mockResolvedValue({ id: 'r1', diff --git a/test/utils/resolvePhoneRemoteJid.test.ts b/test/utils/resolvePhoneRemoteJid.test.ts index 9be24a42ab..148bee0c3a 100644 --- a/test/utils/resolvePhoneRemoteJid.test.ts +++ b/test/utils/resolvePhoneRemoteJid.test.ts @@ -28,6 +28,18 @@ describe('resolvePhoneRemoteJid', () => { ); }); + it('KNOWN BUG: replaces a remoteJid that contains @lid outside the suffix (current behavior)', () => { + expect(resolvePhoneRemoteJid({ remoteJid: 'x@lid.example', remoteJidAlt: '5492616841080@s.whatsapp.net' })).toBe( + '5492616841080@s.whatsapp.net', + ); + }); + + it.fails('keeps a remoteJid that contains @lid outside the suffix', () => { + expect(resolvePhoneRemoteJid({ remoteJid: 'x@lid.example', remoteJidAlt: '5492616841080@s.whatsapp.net' })).toBe( + 'x@lid.example', + ); + }); + it('returns undefined when there is no remoteJid', () => { expect(resolvePhoneRemoteJid({ remoteJidAlt: '5492616841080@s.whatsapp.net' })).toBeUndefined(); }); diff --git a/test/utils/use-multi-file-auth-state-prisma.test.ts b/test/utils/use-multi-file-auth-state-prisma.test.ts index 05a812dcf2..26d1d5c838 100644 --- a/test/utils/use-multi-file-auth-state-prisma.test.ts +++ b/test/utils/use-multi-file-auth-state-prisma.test.ts @@ -139,6 +139,14 @@ describe('useMultiFileAuthStatePrisma', () => { }); }); + it('maps slashes in key names to double underscores in local file paths', async () => { + const { state } = await open(); + + await state.keys.set({ session: { 'a/b': Buffer.from([1]) } }); + + expect([...files.keys()]).toContain('/instances/session/session-a__b.json'); + }); + it('KNOWN BUG: leaves local key files behind on removeCreds without redis (current behavior)', async () => { const { state, removeCreds } = await open(); await state.keys.set({ session: { a: Buffer.from([1]) } }); diff --git a/test/utils/use-multi-file-auth-state-provider-files.test.ts b/test/utils/use-multi-file-auth-state-provider-files.test.ts index 39f539f7fc..01fa7d3680 100644 --- a/test/utils/use-multi-file-auth-state-provider-files.test.ts +++ b/test/utils/use-multi-file-auth-state-provider-files.test.ts @@ -1,7 +1,7 @@ import { proto } from 'baileys'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { loggers } from '../helpers/logger'; +import { loggers, loggersNamed } from '../helpers/logger'; vi.mock('@config/logger.config', async () => (await import('../helpers/logger')).loggerModule); @@ -148,7 +148,11 @@ describe('AuthStateProvider', () => { expect(providerFiles.removeSession).toHaveBeenCalledWith('instance'); expect(providerFiles.files.size).toBe(0); - expect(loggers.some((logger) => logger.info.mock.calls.length > 0)).toBe(true); + expect(loggersNamed('useMultiFileAuthStatePrisma')[0].info).toHaveBeenCalledWith({ + action: 'remove.session', + instance: 'instance', + response: { status: 200, data: 'removed' }, + }); }); it('returns undefined and logs when the session folder cannot be created', async () => { @@ -158,7 +162,11 @@ describe('AuthStateProvider', () => { const result = await open(); expect(result).toBeUndefined(); - expect(loggers.some((logger) => logger.error.mock.calls.length > 0)).toBe(true); + expect(loggersNamed('AuthStateProvider').at(-1).error).toHaveBeenCalledWith([ + 'Failed to create folder on file server', + 'provider offline', + error.stack, + ]); }); it('KNOWN BUG: swallows read, write and delete errors without logging them (current behavior)', async () => { diff --git a/test/utils/use-multi-file-auth-state-redis-db.test.ts b/test/utils/use-multi-file-auth-state-redis-db.test.ts index 35f436bb12..18bdd3a279 100644 --- a/test/utils/use-multi-file-auth-state-redis-db.test.ts +++ b/test/utils/use-multi-file-auth-state-redis-db.test.ts @@ -1,4 +1,4 @@ -import { initAuthCreds, proto } from 'baileys'; +import { proto } from 'baileys'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createCacheService, InMemoryRedisCache } from '../helpers/cache'; @@ -136,10 +136,11 @@ describe('useMultiFileAuthStateRedisDb', () => { expect(loggersNamed('useMultiFileAuthStateRedisDb')[0].error).not.toHaveBeenCalled(); }); - it('keeps the initial creds object when a fresh instance has nothing stored', async () => { - const fresh = initAuthCreds(); - const { state } = await open(); + it('does not persist fresh creds on open, unlike the prisma store', async () => { + const hSet = vi.spyOn(cache, 'hSet'); + + await open(); - expect(Object.keys(state.creds).sort()).toEqual(Object.keys(fresh).sort()); + expect(hSet).not.toHaveBeenCalled(); }); }); From d8e3aac12737363c6a7d8be004b6d21f30e2718f Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:48:32 -0300 Subject: [PATCH 26/96] test: lint and typecheck the test suite test/ was excluded from tsconfig.json and lint:check covered only src, so type and lint errors in tests went unnoticed. tsconfig.test.json extends the base config to include src, test and vitest.config.ts without changing what the build compiles; eslint reads both projects; lint, lint:check and lint-staged cover test; typecheck:test runs in CI and make typecheck. Fixes the surfaced import order and es2020 .at() errors. --- .eslintrc.js | 2 +- .github/workflows/purple-checks.yml | 3 +++ AGENTS.md | 4 +++- Makefile | 3 ++- package.json | 8 ++++++-- test/utils/getConversationMessage.test.ts | 3 +-- test/utils/onWhatsappCache.test.ts | 3 +-- test/utils/use-multi-file-auth-state-prisma.test.ts | 11 +++++------ .../use-multi-file-auth-state-provider-files.test.ts | 5 ++--- test/utils/use-multi-file-auth-state-redis-db.test.ts | 5 ++--- tsconfig.test.json | 9 +++++++++ 11 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 tsconfig.test.json diff --git a/.eslintrc.js b/.eslintrc.js index 8f54a77661..f869514184 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,7 @@ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { - project: 'tsconfig.json', + project: ['tsconfig.json', 'tsconfig.test.json'], tsconfigRootDir: __dirname, sourceType: 'module', warnOnUnsupportedTypeScriptVersion: false, diff --git a/.github/workflows/purple-checks.yml b/.github/workflows/purple-checks.yml index 11c2886546..cba83272b7 100644 --- a/.github/workflows/purple-checks.yml +++ b/.github/workflows/purple-checks.yml @@ -31,6 +31,9 @@ jobs: - name: Typecheck run: npx tsc --noEmit + - name: Typecheck tests + run: npm run typecheck:test + - name: Build run: npm run build diff --git a/AGENTS.md b/AGENTS.md index e9bfafe105..4e288657b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,8 @@ and **stenfar**. Anything they consume is a public contract. ## Commands & danger zones - **Local / safe:** `npm run dev:server` (tsx watch), `npm start`, `npm run build` (`tsc --noEmit && tsup`), - `npm test` (Vitest, `test/**/*.test.ts`), `npm run lint:check`, `npx tsc --noEmit`, `make check-docs`, + `npm test` (Vitest, `test/**/*.test.ts`), `npm run lint:check` (src + test), `npx tsc --noEmit`, + `npm run typecheck:test` (src + test + `vitest.config.ts`), `make check-docs`, `npm run db:generate`, `docker-compose -f docker-compose.dev.yaml up -d`. - **Database:** always `export DATABASE_PROVIDER=postgresql` (or `mysql`) first. `npm run db:migrate:dev` only against a local database. Every schema change ships migrations for BOTH providers. @@ -123,6 +124,7 @@ how our code processes data; Baileys is mocked where it touches the socket. Befo ```bash npm test npx tsc --noEmit +npm run typecheck:test npm run build npm run lint:check make check-docs # docs drift guard (pointers, skill frontmatter) diff --git a/Makefile b/Makefile index 55b416f8d8..0a1de5455b 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,9 @@ lint: ## ESLint with auto-fix (npm run lint) test: ## Run the unit tests once (npm test) npm test -typecheck: ## TypeScript check without emit +typecheck: ## TypeScript check without emit (src, then src + test) npx --no tsc --noEmit + npm run typecheck:test check-docs: ## Validate agent docs (AGENTS.md, CLAUDE.md, skills, links) bash scripts/check-docs.sh diff --git a/package.json b/package.json index 40f41c136b..3aa052f968 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,9 @@ "start:prod": "node dist/main", "dev:server": "tsx watch ./src/main.ts", "test": "vitest run", - "lint": "eslint --fix --ext .ts src", - "lint:check": "eslint --ext .ts src", + "lint": "eslint --fix --ext .ts src test", + "lint:check": "eslint --ext .ts src test", + "typecheck:test": "tsc -p tsconfig.test.json --noEmit", "commit": "cz", "commitlint": "commitlint --edit", "db:generate": "node runWithProvider.js \"npx prisma generate --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", @@ -59,6 +60,9 @@ ], "src/**/*.ts": [ "sh -c 'tsc --noEmit'" + ], + "test/**/*.ts": [ + "eslint --fix" ] }, "config": { diff --git a/test/utils/getConversationMessage.test.ts b/test/utils/getConversationMessage.test.ts index cfa7645495..e9ca0ca307 100644 --- a/test/utils/getConversationMessage.test.ts +++ b/test/utils/getConversationMessage.test.ts @@ -1,3 +1,4 @@ +import { getConversationMessage } from '@utils/getConversationMessage'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const s3 = vi.hoisted(() => ({ ENABLE: false, SAVE_VIDEO: false })); @@ -6,8 +7,6 @@ vi.mock('@config/env.config', () => ({ configService: { get: vi.fn(() => s3) }, })); -import { getConversationMessage } from '@utils/getConversationMessage'; - const key = { id: 'MSG1' }; describe('getConversationMessage', () => { diff --git a/test/utils/onWhatsappCache.test.ts b/test/utils/onWhatsappCache.test.ts index eb22d508d4..cb5a7ed9f1 100644 --- a/test/utils/onWhatsappCache.test.ts +++ b/test/utils/onWhatsappCache.test.ts @@ -1,3 +1,4 @@ +import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const mocks = vi.hoisted(() => ({ @@ -15,8 +16,6 @@ vi.mock('@config/logger.config', () => ({ }, })); -import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; - const { isOnWhatsapp } = mocks; const queriedJids = () => diff --git a/test/utils/use-multi-file-auth-state-prisma.test.ts b/test/utils/use-multi-file-auth-state-prisma.test.ts index 26d1d5c838..a0db09a637 100644 --- a/test/utils/use-multi-file-auth-state-prisma.test.ts +++ b/test/utils/use-multi-file-auth-state-prisma.test.ts @@ -1,4 +1,8 @@ +import { prismaRepository } from '@api/server.module'; +import { CacheService } from '@api/services/cache.service'; +import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma'; import { proto } from 'baileys'; +import fs from 'fs/promises'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createCacheService, InMemoryRedisCache } from '../helpers/cache'; @@ -29,11 +33,6 @@ vi.mock('@api/server.module', () => ({ })); vi.mock('fs/promises', async () => (await import('../helpers/fs-promises')).fsPromisesModule); -import { prismaRepository } from '@api/server.module'; -import { CacheService } from '@api/services/cache.service'; -import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma'; -import fs from 'fs/promises'; - describe('useMultiFileAuthStatePrisma', () => { let inner: InMemoryRedisCache; let cache: CacheService; @@ -213,7 +212,7 @@ describe('useMultiFileAuthStatePrisma', () => { expect(inner.hashes.get('session')?.has('session-a')).toBeFalsy(); expect(await state.keys.get('session', ['a'])).toEqual({ a: null }); - expect(loggersNamed('CacheService').at(-1).error).toHaveBeenCalledWith(inner.error); + expect(loggersNamed('CacheService').slice(-1)[0].error).toHaveBeenCalledWith(inner.error); }); it('swallows database and file read errors without logging them', async () => { diff --git a/test/utils/use-multi-file-auth-state-provider-files.test.ts b/test/utils/use-multi-file-auth-state-provider-files.test.ts index 01fa7d3680..55bd33ed8c 100644 --- a/test/utils/use-multi-file-auth-state-provider-files.test.ts +++ b/test/utils/use-multi-file-auth-state-provider-files.test.ts @@ -1,3 +1,4 @@ +import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; import { proto } from 'baileys'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -5,8 +6,6 @@ import { loggers, loggersNamed } from '../helpers/logger'; vi.mock('@config/logger.config', async () => (await import('../helpers/logger')).loggerModule); -import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; - const createProviderFiles = () => { const files = new Map(); const path = (instance: string, key: string) => `${instance}/${key}`; @@ -162,7 +161,7 @@ describe('AuthStateProvider', () => { const result = await open(); expect(result).toBeUndefined(); - expect(loggersNamed('AuthStateProvider').at(-1).error).toHaveBeenCalledWith([ + expect(loggersNamed('AuthStateProvider').slice(-1)[0].error).toHaveBeenCalledWith([ 'Failed to create folder on file server', 'provider offline', error.stack, diff --git a/test/utils/use-multi-file-auth-state-redis-db.test.ts b/test/utils/use-multi-file-auth-state-redis-db.test.ts index 18bdd3a279..93a096ce39 100644 --- a/test/utils/use-multi-file-auth-state-redis-db.test.ts +++ b/test/utils/use-multi-file-auth-state-redis-db.test.ts @@ -1,3 +1,5 @@ +import { CacheService } from '@api/services/cache.service'; +import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; import { proto } from 'baileys'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -6,9 +8,6 @@ import { lastLogger, loggers, loggersNamed } from '../helpers/logger'; vi.mock('@config/logger.config', async () => (await import('../helpers/logger')).loggerModule); -import { CacheService } from '@api/services/cache.service'; -import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; - describe('useMultiFileAuthStateRedisDb', () => { let inner: InMemoryRedisCache; let cache: CacheService; diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000000..29fffa09ab --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "incremental": false + }, + "include": ["src/**/*", "src/**/*.json", "test/**/*", "vitest.config.ts"], + "exclude": ["node_modules", "./dist", "./prisma"] +} From c10d3e22e214647f3fa9c9cecb922f0b60205dc4 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:56:05 -0300 Subject: [PATCH 27/96] test(helpers): fail loudly when a logger lookup is ambiguous Tests picked the first logger matching a context name, so a second logger with the same name would silently be asserted instead. onlyLoggerNamed throws unless exactly one logger has that name. --- test/helpers/logger.ts | 8 ++++++++ test/utils/use-multi-file-auth-state-prisma.test.ts | 4 ++-- .../use-multi-file-auth-state-provider-files.test.ts | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/test/helpers/logger.ts b/test/helpers/logger.ts index 96c46127a5..a28f8be9f0 100644 --- a/test/helpers/logger.ts +++ b/test/helpers/logger.ts @@ -6,6 +6,14 @@ export const lastLogger = () => loggers[loggers.length - 1]; export const loggersNamed = (name: string) => loggers.filter((logger) => logger.context === name); +export const onlyLoggerNamed = (name: string) => { + const found = loggersNamed(name); + if (found.length !== 1) { + throw new Error(`Expected exactly one logger named ${name}, found ${found.length}`); + } + return found[0]; +}; + export class Logger { verbose = vi.fn(); info = vi.fn(); diff --git a/test/utils/use-multi-file-auth-state-prisma.test.ts b/test/utils/use-multi-file-auth-state-prisma.test.ts index a0db09a637..dda722afcc 100644 --- a/test/utils/use-multi-file-auth-state-prisma.test.ts +++ b/test/utils/use-multi-file-auth-state-prisma.test.ts @@ -7,7 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createCacheService, InMemoryRedisCache } from '../helpers/cache'; import { files } from '../helpers/fs-promises'; -import { loggersNamed } from '../helpers/logger'; +import { loggersNamed, onlyLoggerNamed } from '../helpers/logger'; const mocks = vi.hoisted(() => ({ cacheConf: { REDIS: { ENABLED: false } }, @@ -45,7 +45,7 @@ describe('useMultiFileAuthStatePrisma', () => { }); const open = () => useMultiFileAuthStatePrisma('session', cache); - const authLogger = () => loggersNamed('useMultiFileAuthStatePrisma')[0]; + const authLogger = () => onlyLoggerNamed('useMultiFileAuthStatePrisma'); it('creates and persists fresh creds when nothing is stored', async () => { const { state } = await open(); diff --git a/test/utils/use-multi-file-auth-state-provider-files.test.ts b/test/utils/use-multi-file-auth-state-provider-files.test.ts index 55bd33ed8c..89b5435342 100644 --- a/test/utils/use-multi-file-auth-state-provider-files.test.ts +++ b/test/utils/use-multi-file-auth-state-provider-files.test.ts @@ -2,7 +2,7 @@ import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-fil import { proto } from 'baileys'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { loggers, loggersNamed } from '../helpers/logger'; +import { loggers, loggersNamed, onlyLoggerNamed } from '../helpers/logger'; vi.mock('@config/logger.config', async () => (await import('../helpers/logger')).loggerModule); @@ -147,7 +147,7 @@ describe('AuthStateProvider', () => { expect(providerFiles.removeSession).toHaveBeenCalledWith('instance'); expect(providerFiles.files.size).toBe(0); - expect(loggersNamed('useMultiFileAuthStatePrisma')[0].info).toHaveBeenCalledWith({ + expect(onlyLoggerNamed('useMultiFileAuthStatePrisma').info).toHaveBeenCalledWith({ action: 'remove.session', instance: 'instance', response: { status: 200, data: 'removed' }, From a63026b13f5d90c2aaa648bc1ecb89ece6230014 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 21:56:23 -0300 Subject: [PATCH 28/96] ci(security): allow braces GHSA-vfj7-8cjw-p6xm in dependency review braces@3.0.3 is the latest release and has no patched version. It is still reached through patch-package -> find-yarn-workspace-root -> micromatch, which only runs at install time over this repository's own patch files, so untrusted patterns never reach it. The lint-staged path was already removed by upgrading to lint-staged 17. Drop this allowance once braces ships a fix or patch-package stops depending on micromatch. --- .github/workflows/security.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index b83f0b5d98..0407098b41 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -53,3 +53,5 @@ jobs: submodules: recursive - name: Dependency Review uses: actions/dependency-review-action@v4 + with: + allow-ghsas: GHSA-vfj7-8cjw-p6xm From 190e35940427c04011d073aaf9c09ec813224d32 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Wed, 21 Oct 2026 23:09:54 -0300 Subject: [PATCH 29/96] chore(release): v0.1.0 First fork release: Baileys rc14 pairing fix, app-state sync key restore, instance auth hardening and the Vitest unit suite. VERSION already reads 0.1.0. --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 480a0d8114..ee0d4b7771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,20 @@ # evolution-in-purple -## 0.1.0 (Unreleased) +## 0.1.0 (2026-10-21) + +### Fixes + +* **Device pairing**: Baileys `7.0.0-rc.9` → `7.0.0-rc14` with a `patch-package` patch that handles WhatsApp's `companion_reg_refresh` notification and guards `link_code_companion_reg` without crypto fields, fixing intermittent "Couldn't link device" failures. rc14 also fixes CVE-2026-48063 (#4; backport of upstream evolution-foundation/evolution-api#2759, `5cdf3240`, vendoring WhiskeySockets/Baileys#2765 and #2602) +* **App-state sync keys**: the Prisma, Redis and provider-files auth states restore `app-state-sync-key` with `AppStateSyncKeyData.fromObject` so base64 key data is converted back to bytes (#5; upstream evolution-foundation/evolution-api#2685, `65b5acf3`) +* **Instance auth**: instance routes authorize the path `instanceName` and ignore a query override, preventing cross-instance access (#2; adapted from upstream evolution-foundation/evolution-api#2549, `7a55a2bf`) ### Chores * Fork baseline from Evolution API 2.3.7 * Agentic setup: AGENTS.md, Claude skills and hooks, docs drift check, Makefile, fork CI (`purple-checks.yml`) and GHCR publishing on `v*` tags +* Vitest unit suite (150 tests) for our own data processing, run shuffled in CI with type-checking and linting; known bugs pinned as `KNOWN BUG` + `it.fails` (#6) +* `@lid` → phone JID resolution for `MESSAGES_UPSERT` extracted to `resolvePhoneRemoteJid`, behavior unchanged (#6) +* `lint-staged` 17 and CI on Node 24; dependency review allows GHSA-vfj7-8cjw-p6xm (`braces`, no patched release, reached only via `patch-package` at install time) (#6) # Upstream Evolution API changelog From bd8224715f600fa8146d6970f2f330ffc15b6cdf Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 13:35:11 -0300 Subject: [PATCH 30/96] ci(release): stop publishing the latest image tag docker/metadata-action defaults to latest=auto, which added a floating latest tag to every v* release despite the purple-release rule that only X.Y.Z and X.Y are published. latest=false keeps images pinned to explicit versions. The latest tag already in GHCR from v0.1.0 stays until removed by hand. --- .github/workflows/publish_docker_image.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml index 7cb2802fbc..7af0ab6b71 100644 --- a/.github/workflows/publish_docker_image.yml +++ b/.github/workflows/publish_docker_image.yml @@ -25,6 +25,8 @@ jobs: uses: docker/metadata-action@v5 with: images: ghcr.io/purple-dice/evolution-in-purple + flavor: | + latest=false tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} From 4b6124fb2b8325d5caa512680a7777f60fdca24b Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 14:23:21 -0300 Subject: [PATCH 31/96] fix(baileys): close the connection state as soon as an instance logs out logoutInstance relied on Baileys' connection.update close event, which only fires after the websocket finishes closing (up to the 30s ws close timeout), so connectionState and Instance.connectionStatus reported open after a successful logout. logoutInstance now marks the session ended, sets the state to close (401), persists it, and sends STATUS_INSTANCE and CONNECTION_UPDATE immediately. logout.instance is emitted after credentials and the Session row are removed, so its cleaningUp listener cannot race the session delete. connectionUpdate ignores the late close event for an ended session, keeping webhooks single. --- .../whatsapp/whatsapp.baileys.service.ts | 24 +++++ .../channel/whatsapp/logoutInstance.test.ts | 97 +++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 test/api/integrations/channel/whatsapp/logoutInstance.test.ts diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index c13891593b..b6cf192242 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -269,6 +269,24 @@ export class BaileysStartupService extends ChannelStartupService { this.messageProcessor.onDestroy(); await this.client?.logout('Log out instance: ' + this.instanceName); + this.endSession = true; + this.stateConnection = { state: 'close', statusReason: DisconnectReason.loggedOut }; + const disconnectionAt = new Date(); + + this.sendDataWebhook(Events.STATUS_INSTANCE, { + instance: this.instance.name, + status: 'closed', + disconnectionAt, + disconnectionReasonCode: DisconnectReason.loggedOut, + }); + + await this.prismaRepository.instance.update({ + where: { id: this.instanceId }, + data: { connectionStatus: 'close', disconnectionAt, disconnectionReasonCode: DisconnectReason.loggedOut }, + }); + + this.sendDataWebhook(Events.CONNECTION_UPDATE, { instance: this.instance.name, ...this.stateConnection }); + this.client?.ws?.close(); const db = this.configService.get('DATABASE'); @@ -297,6 +315,8 @@ export class BaileysStartupService extends ChannelStartupService { if (sessionExists) { await this.prismaRepository.session.delete({ where: { sessionId: this.instanceId } }); } + + this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); } public async getProfileName() { @@ -417,6 +437,10 @@ export class BaileysStartupService extends ChannelStartupService { }); } + if (connection === 'close' && this.endSession && this.stateConnection.state === 'close') { + return; + } + if (connection) { this.stateConnection = { state: connection, diff --git a/test/api/integrations/channel/whatsapp/logoutInstance.test.ts b/test/api/integrations/channel/whatsapp/logoutInstance.test.ts new file mode 100644 index 0000000000..cabca6d722 --- /dev/null +++ b/test/api/integrations/channel/whatsapp/logoutInstance.test.ts @@ -0,0 +1,97 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { Events } from '@api/types/wa.types'; +import { Boom } from '@hapi/boom'; +import { DisconnectReason } from 'baileys'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@utils/use-multi-file-auth-state-prisma', () => ({ + default: vi.fn(async () => ({ removeCreds: vi.fn() })), +})); + +vi.mock('@utils/use-multi-file-auth-state-redis-db', () => ({ + useMultiFileAuthStateRedisDb: vi.fn(async () => ({ removeCreds: vi.fn() })), +})); + +const buildService = () => { + const service = Object.create(BaileysStartupService.prototype) as any; + const config: Record = { + DATABASE: { SAVE_DATA: { INSTANCE: true } }, + CACHE: { REDIS: { ENABLED: false } }, + PROVIDER: { ENABLED: false }, + CHATWOOT: { ENABLED: false }, + }; + service.messageProcessor = { onDestroy: vi.fn() }; + service.client = { logout: vi.fn(async () => undefined), ws: { close: vi.fn() }, end: vi.fn() }; + service.configService = { get: vi.fn((key: string) => config[key]) }; + service.prismaRepository = { + instance: { update: vi.fn(async () => ({})) }, + session: { findFirst: vi.fn(async () => null), delete: vi.fn() }, + }; + service.instance = { id: 'instance-id', name: 'instance-name', wuid: 'wuid' }; + service.eventEmitter = { emit: vi.fn() }; + service.sendDataWebhook = vi.fn(); + service.connectToWhatsapp = vi.fn(); + service.localChatwoot = { enabled: false }; + service.cache = {}; + service.stateConnection = { state: 'open' }; + service.endSession = false; + return service; +}; + +const connectionUpdates = (service: any) => + service.sendDataWebhook.mock.calls.filter(([event]: [string]) => event === Events.CONNECTION_UPDATE); + +const logoutEmits = (service: any) => + service.eventEmitter.emit.mock.calls.filter(([event]: [string]) => event === 'logout.instance'); + +describe('BaileysStartupService.logoutInstance', () => { + let service: any; + + beforeEach(() => { + service = buildService(); + }); + + it('closes the connection state immediately without waiting for Baileys', async () => { + await service.logoutInstance(); + + expect(service.connectionStatus).toEqual({ state: 'close', statusReason: DisconnectReason.loggedOut }); + expect(service.prismaRepository.instance.update).toHaveBeenCalledWith({ + where: { id: 'instance-id' }, + data: expect.objectContaining({ + connectionStatus: 'close', + disconnectionReasonCode: DisconnectReason.loggedOut, + }), + }); + expect(connectionUpdates(service)).toEqual([ + [Events.CONNECTION_UPDATE, expect.objectContaining({ instance: 'instance-name', state: 'close' })], + ]); + expect(logoutEmits(service)).toHaveLength(1); + expect(service.connectToWhatsapp).not.toHaveBeenCalled(); + }); + + it('ignores the late Baileys loggedOut close event after logout', async () => { + await service.logoutInstance(); + + await service.connectionUpdate({ + connection: 'close', + lastDisconnect: { error: new Boom('logged out', { statusCode: DisconnectReason.loggedOut }), date: new Date() }, + }); + + expect(connectionUpdates(service)).toHaveLength(1); + expect(logoutEmits(service)).toHaveLength(1); + expect(service.connectToWhatsapp).not.toHaveBeenCalled(); + }); + + it('emits logout.instance only after the session row is deleted', async () => { + service.prismaRepository.session.findFirst.mockResolvedValue({ sessionId: 'instance-id' }); + + await service.logoutInstance(); + + const sessionDeleteOrder = service.prismaRepository.session.delete.mock.invocationCallOrder[0]; + const logoutEmitOrder = + service.eventEmitter.emit.mock.invocationCallOrder[ + service.eventEmitter.emit.mock.calls.findIndex(([event]: [string]) => event === 'logout.instance') + ]; + expect(sessionDeleteOrder).toBeLessThan(logoutEmitOrder); + }); +}); From dfebce7e99c4256695d8d2e0201c1d6b9e1d8c1c Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 14:34:09 -0300 Subject: [PATCH 32/96] fix(baileys): keep logout cleanup running when persisting the closed state fails The new Instance update ran before credential and Session removal, so a database error aborted the logout halfway and returned 500. The update is now logged and skipped on failure; credentials, the Session row and logout.instance proceed as before. --- .../channel/whatsapp/whatsapp.baileys.service.ts | 12 ++++++++---- .../channel/whatsapp/logoutInstance.test.ts | 13 +++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index b6cf192242..e109f35316 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -280,10 +280,14 @@ export class BaileysStartupService extends ChannelStartupService { disconnectionReasonCode: DisconnectReason.loggedOut, }); - await this.prismaRepository.instance.update({ - where: { id: this.instanceId }, - data: { connectionStatus: 'close', disconnectionAt, disconnectionReasonCode: DisconnectReason.loggedOut }, - }); + try { + await this.prismaRepository.instance.update({ + where: { id: this.instanceId }, + data: { connectionStatus: 'close', disconnectionAt, disconnectionReasonCode: DisconnectReason.loggedOut }, + }); + } catch (error) { + this.logger.error(['Error persisting the closed connection state on logout', error?.message]); + } this.sendDataWebhook(Events.CONNECTION_UPDATE, { instance: this.instance.name, ...this.stateConnection }); diff --git a/test/api/integrations/channel/whatsapp/logoutInstance.test.ts b/test/api/integrations/channel/whatsapp/logoutInstance.test.ts index cabca6d722..064fa67c2d 100644 --- a/test/api/integrations/channel/whatsapp/logoutInstance.test.ts +++ b/test/api/integrations/channel/whatsapp/logoutInstance.test.ts @@ -94,4 +94,17 @@ describe('BaileysStartupService.logoutInstance', () => { ]; expect(sessionDeleteOrder).toBeLessThan(logoutEmitOrder); }); + + it('still removes the session and emits logout.instance when persisting the closed state fails', async () => { + service.prismaRepository.instance.update.mockRejectedValue(new Error('database unavailable')); + service.prismaRepository.session.findFirst.mockResolvedValue({ sessionId: 'instance-id' }); + service.logger = { error: vi.fn() }; + + await expect(service.logoutInstance()).resolves.toBeUndefined(); + + expect(service.connectionStatus).toEqual({ state: 'close', statusReason: 401 }); + expect(service.prismaRepository.session.delete).toHaveBeenCalledWith({ where: { sessionId: 'instance-id' } }); + expect(logoutEmits(service)).toHaveLength(1); + expect(service.logger.error).toHaveBeenCalled(); + }); }); From 89fe240db149c3c9128493b9feaa8381ec6b4fcc Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 14:50:49 -0300 Subject: [PATCH 33/96] fix(utils): keep the mobile 9 in Argentine jids built by createJid WhatsApp addresses Argentine mobiles as 549 plus ten digits, but createJid removed the 9, so callers that use its result without onWhatsApp verification (mentions, chat archive and unread, group participant updates, remoteJid lookups) targeted jids that do not exist. The third-digit removal now applies only to Mexico, the case it was introduced for. --- src/utils/createJid.ts | 17 +++++------------ test/utils/createJid.test.ts | 5 +++-- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/utils/createJid.ts b/src/utils/createJid.ts index a680e821e1..8ca21e2a35 100644 --- a/src/utils/createJid.ts +++ b/src/utils/createJid.ts @@ -1,15 +1,8 @@ -// Check if the number is MX or AR -function formatMXOrARNumber(jid: string): string { - const countryCode = jid.substring(0, 2); - - if (Number(countryCode) === 52 || Number(countryCode) === 54) { - if (jid.length === 13) { - const number = countryCode + jid.substring(3); - return number; - } - - return jid; +function formatMXNumber(jid: string): string { + if (jid.startsWith('52') && jid.length === 13) { + return jid.substring(0, 2) + jid.substring(3); } + return jid; } @@ -63,7 +56,7 @@ export function createJid(number: string): string { return `${number}@g.us`; } - number = formatMXOrARNumber(number); + number = formatMXNumber(number); number = formatBRNumber(number); diff --git a/test/utils/createJid.test.ts b/test/utils/createJid.test.ts index 1a56a44dd1..3f5639bf9e 100644 --- a/test/utils/createJid.test.ts +++ b/test/utils/createJid.test.ts @@ -43,8 +43,9 @@ describe('createJid', () => { expect(createJid('12036301234567890')).toBe('12036301234567890@s.whatsapp.net'); }); - it('KNOWN BUG: removes the third digit from 13 digit Argentine numbers (current behavior)', () => { - expect(createJid('5492616107398')).toBe('542616107398@s.whatsapp.net'); + it('keeps the mobile 9 in 13 digit Argentine numbers', () => { + expect(createJid('5492616107398')).toBe('5492616107398@s.whatsapp.net'); + expect(createJid('+54 9 261 610-7398')).toBe('5492616107398@s.whatsapp.net'); }); it('KNOWN BUG: removes the third digit from 13 digit Mexican numbers (current behavior)', () => { From 5dd4df7d31943d6812e7a4575810dd90d9bbfbdb Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 15:05:52 -0300 Subject: [PATCH 34/96] test(utils): cover more Argentine formats in createJid --- test/utils/createJid.test.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/utils/createJid.test.ts b/test/utils/createJid.test.ts index 3f5639bf9e..7d02dea252 100644 --- a/test/utils/createJid.test.ts +++ b/test/utils/createJid.test.ts @@ -48,6 +48,32 @@ describe('createJid', () => { expect(createJid('+54 9 261 610-7398')).toBe('5492616107398@s.whatsapp.net'); }); + it.each([ + ['Buenos Aires', '5491123456789'], + ['Cordoba', '5493511234567'], + ['Rosario', '5493411234567'], + ['four digit area code', '5492966123456'], + ])('keeps the mobile 9 for %s mobiles', (_area, number) => { + expect(createJid(number)).toBe(`${number}@s.whatsapp.net`); + }); + + it.each(['+54 9 11 2345-6789', '+54 (9) 11 2345 6789', '549-11-2345-6789', '+549 11 23456789'])( + 'normalizes the formatted Argentine mobile %s keeping the 9', + (formatted) => { + expect(createJid(formatted)).toBe('5491123456789@s.whatsapp.net'); + }, + ); + + it('passes Argentine jids through unchanged with or without the 9', () => { + expect(createJid('5491123456789@s.whatsapp.net')).toBe('5491123456789@s.whatsapp.net'); + expect(createJid('541123456789@s.whatsapp.net')).toBe('541123456789@s.whatsapp.net'); + expect(createJid('5491123456789:7@s.whatsapp.net')).toBe('5491123456789@s.whatsapp.net'); + }); + + it('keeps Argentine numbers with fewer than 12 digits unchanged', () => { + expect(createJid('54112345678')).toBe('54112345678@s.whatsapp.net'); + }); + it('KNOWN BUG: removes the third digit from 13 digit Mexican numbers (current behavior)', () => { expect(createJid('5215512345678')).toBe('525512345678@s.whatsapp.net'); }); From a6f76588f8a2366e42fa0041e059cf3139a10799 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 15:36:33 -0300 Subject: [PATCH 35/96] fix(baileys): return no message for unknown keys in getMessage getMessage answered an unknown key or a failed lookup with an empty conversation. Baileys treats any value as found, so a retry receipt resent an empty message and consumed the retry, leaving recipients on "Waiting for this message". Returning undefined lets Baileys skip the resend as designed. --- .../whatsapp/whatsapp.baileys.service.ts | 5 +- .../channel/whatsapp/getMessage.test.ts | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 test/api/integrations/channel/whatsapp/getMessage.test.ts diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index e109f35316..49f5a6181f 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -561,6 +561,9 @@ export class BaileysStartupService extends ChannelStartupService { if (full) { return webMessageInfo[0]; } + if (!webMessageInfo[0]) { + return undefined; + } if (webMessageInfo[0].message?.pollCreationMessage) { const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret; @@ -578,7 +581,7 @@ export class BaileysStartupService extends ChannelStartupService { return webMessageInfo[0].message; } catch { - return { conversation: '' }; + return undefined; } } diff --git a/test/api/integrations/channel/whatsapp/getMessage.test.ts b/test/api/integrations/channel/whatsapp/getMessage.test.ts new file mode 100644 index 0000000000..ffc9a69286 --- /dev/null +++ b/test/api/integrations/channel/whatsapp/getMessage.test.ts @@ -0,0 +1,49 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { describe, expect, it, vi } from 'vitest'; + +const buildService = (queryRaw: () => Promise) => { + const service = Object.create(BaileysStartupService.prototype) as any; + service.instance = { id: 'instance-id', name: 'instance-name' }; + service.prismaRepository = { $queryRaw: vi.fn(queryRaw) }; + return service; +}; + +const key = { id: 'MESSAGE-ID', remoteJid: '5491123456789@s.whatsapp.net', fromMe: true }; + +describe('BaileysStartupService.getMessage', () => { + it('returns undefined for an unknown key so Baileys skips the retry resend', async () => { + const service = buildService(async () => []); + + await expect(service.getMessage(key)).resolves.toBeUndefined(); + await expect(service.getMessage(key, true)).resolves.toBeUndefined(); + }); + + it('returns undefined when the lookup fails', async () => { + const service = buildService(async () => { + throw new Error('database unavailable'); + }); + + await expect(service.getMessage(key)).resolves.toBeUndefined(); + await expect(service.getMessage(key, true)).resolves.toBeUndefined(); + }); + + it('returns the stored message content for a known key', async () => { + const stored = { key, message: { conversation: 'hello' } }; + const service = buildService(async () => [stored]); + + await expect(service.getMessage(key)).resolves.toEqual({ conversation: 'hello' }); + await expect(service.getMessage(key, true)).resolves.toEqual(stored); + }); + + it('returns the poll secret for a stored poll creation', async () => { + const pollCreationMessage = { name: 'poll', options: [{ optionName: 'a' }] }; + const service = buildService(async () => [ + { key, message: { pollCreationMessage, messageContextInfo: { messageSecret: 'c2VjcmV0' } } }, + ]); + + const result = await service.getMessage(key); + + expect(result.pollCreationMessage).toEqual(pollCreationMessage); + expect(Buffer.from(result.messageContextInfo.messageSecret).toString()).toBe('secret'); + }); +}); From 1efc1ec4116a9d45af44f92438011337fe3e8548 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 16:16:48 -0300 Subject: [PATCH 36/96] fix(baileys): store and emit each incoming message once WhatsApp redelivers messages and Baileys may surface the same message under both its @lid and phone jid. messages.upsert stored and webhooked every copy, so production kept 2-3 rows per duplicated id and consumers received repeated MESSAGES_UPSERT events. Messages are now remembered in the baileys cache by instance, id and fromMe for 24h, across live upserts, API sends and history sync. A repeat is skipped unless it carries the phone number the first copy lacked. The mark is set after the webhook, so a failed store is retried on redelivery, and cache errors never block processing. --- .../whatsapp/whatsapp.baileys.service.ts | 43 +++++- src/utils/messageDedupe.ts | 43 ++++++ .../whatsapp/messagesUpsertDedupe.test.ts | 138 ++++++++++++++++++ test/utils/messageDedupe.test.ts | 82 +++++++++++ 4 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 src/utils/messageDedupe.ts create mode 100644 test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts create mode 100644 test/utils/messageDedupe.test.ts diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 49f5a6181f..926b424708 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -83,6 +83,13 @@ import { Instance, Message } from '@prisma/client'; import { createJid } from '@utils/createJid'; import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent'; +import { + MESSAGE_DEDUPE_TTL_SECONDS, + messageDedupeKey, + MessageDedupeKeyInput, + messageDedupeMark, + shouldProcessMessage, +} from '@utils/messageDedupe'; import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; import { status } from '@utils/renderStatus'; import { resolvePhoneRemoteJid } from '@utils/resolvePhoneRemoteJid'; @@ -955,6 +962,28 @@ export class BaileysStartupService extends ChannelStartupService { }, }; + private async isDuplicateMessage(key: MessageDedupeKeyInput): Promise { + try { + const seen = await this.baileysCache.get(messageDedupeKey(this.instanceId, key)); + return !shouldProcessMessage(seen, key); + } catch (error) { + this.logger.warn(`Message dedupe lookup failed, processing message: ${error?.message}`); + return false; + } + } + + private async markMessageSeen(key: MessageDedupeKeyInput): Promise { + try { + await this.baileysCache.set( + messageDedupeKey(this.instanceId, key), + messageDedupeMark(key), + MESSAGE_DEDUPE_TTL_SECONDS, + ); + } catch (error) { + this.logger.warn(`Message dedupe mark failed: ${error?.message}`); + } + } + private readonly messageHandle = { 'messaging-history.set': async ({ messages, @@ -1062,7 +1091,7 @@ export class BaileysStartupService extends ChannelStartupService { } } - if (messagesRepository?.has(m.key.id)) { + if (messagesRepository?.has(m.key.id) || (await this.isDuplicateMessage(m.key))) { continue; } @@ -1075,6 +1104,7 @@ export class BaileysStartupService extends ChannelStartupService { } } + await this.markMessageSeen(m.key); messagesRaw.push(this.prepareMessage(m)); } @@ -1199,6 +1229,11 @@ export class BaileysStartupService extends ChannelStartupService { continue; } + if (await this.isDuplicateMessage(received.key)) { + this.logger.verbose(`Duplicate message ignored: ${received.key.id}`); + continue; + } + if (Long.isLong(received.messageTimestamp)) { received.messageTimestamp = received.messageTimestamp?.toNumber(); } @@ -1515,6 +1550,8 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + await this.markMessageSeen(received.key); + await chatbotController.emit({ instance: { instanceName: this.instance.name, instanceId: this.instanceId }, remoteJid: messageRaw.key.remoteJid, @@ -2457,6 +2494,10 @@ export class BaileysStartupService extends ChannelStartupService { const messageRaw = this.prepareMessage(messageSent); + if (messageSent?.key) { + await this.markMessageSeen(messageSent.key); + } + const isMedia = messageSent?.message?.imageMessage || messageSent?.message?.videoMessage || diff --git a/src/utils/messageDedupe.ts b/src/utils/messageDedupe.ts new file mode 100644 index 0000000000..8e8a0d11f4 --- /dev/null +++ b/src/utils/messageDedupe.ts @@ -0,0 +1,43 @@ +export type MessageDedupeMark = 'phone' | 'no-phone'; + +export type MessageDedupeKeyInput = { + id?: string; + fromMe?: boolean; + remoteJid?: string; + remoteJidAlt?: string; + senderPn?: string; + participantAlt?: string; +}; + +export const MESSAGE_DEDUPE_TTL_SECONDS = 24 * 60 * 60; + +const PHONE_SUFFIX = '@s.whatsapp.net'; +const NON_PERSONAL_SUFFIXES = ['@g.us', '@broadcast', '@newsletter']; + +const isPhoneJid = (jid?: string) => typeof jid === 'string' && jid.endsWith(PHONE_SUFFIX); + +export function messageDedupeKey(instanceId: string, key: MessageDedupeKeyInput): string { + return `message_seen_${instanceId}_${key.id}_${key.fromMe ? 1 : 0}`; +} + +export function hasResolvablePhone(key: MessageDedupeKeyInput): boolean { + const remoteJid = key.remoteJid ?? ''; + + if (NON_PERSONAL_SUFFIXES.some((suffix) => remoteJid.endsWith(suffix))) { + return true; + } + + return [key.remoteJid, key.remoteJidAlt, key.senderPn, key.participantAlt].some(isPhoneJid); +} + +export function messageDedupeMark(key: MessageDedupeKeyInput): MessageDedupeMark { + return hasResolvablePhone(key) ? 'phone' : 'no-phone'; +} + +export function shouldProcessMessage(seen: MessageDedupeMark | null | undefined, key: MessageDedupeKeyInput): boolean { + if (!seen) { + return true; + } + + return seen === 'no-phone' && hasResolvablePhone(key); +} diff --git a/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts b/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts new file mode 100644 index 0000000000..7539b8b4eb --- /dev/null +++ b/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts @@ -0,0 +1,138 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { Events } from '@api/types/wa.types'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@api/server.module', () => ({ + chatbotController: { emit: vi.fn(async () => undefined) }, + waMonitor: {}, +})); + +vi.mock('@utils/onWhatsappCache', () => ({ + getOnWhatsappCache: vi.fn(async () => []), + saveOnWhatsappCache: vi.fn(async () => undefined), +})); + +vi.mock('@utils/sendTelemetry', () => ({ sendTelemetry: vi.fn() })); + +const buildCache = () => { + const store = new Map(); + return { + store, + get: vi.fn(async (key: string) => store.get(key)), + set: vi.fn(async (key: string, value: unknown) => { + store.set(key, value); + }), + }; +}; + +const buildService = (baileysCache = buildCache()) => { + const config: Record = { + LOG: { BAILEYS: 'error' }, + DATABASE: { SAVE_DATA: { NEW_MESSAGE: true, CHATS: false, CONTACTS: false } }, + CHATWOOT: { ENABLED: false }, + OPENAI: { ENABLED: false }, + S3: { ENABLE: false }, + }; + const configService = { get: vi.fn((key: string) => config[key]) } as any; + const prismaRepository = { + chat: { findFirst: vi.fn(async () => null) }, + message: { create: vi.fn(async ({ data }) => ({ id: 'row-id', ...data })) }, + contact: { findFirst: vi.fn(async () => null) }, + } as any; + const service = new BaileysStartupService( + configService, + { emit: vi.fn() } as any, + prismaRepository, + {} as any, + {} as any, + baileysCache as any, + {} as any, + ) as any; + service.instance.id = 'instance-id'; + service.instance.name = 'instance-name'; + service.sendDataWebhook = vi.fn(); + service.profilePicture = vi.fn(async () => ({ profilePictureUrl: null })); + service.updateMessagesReadedByTimestamp = vi.fn(async () => undefined); + service.updateChatUnreadMessages = vi.fn(async () => undefined); + service.client = { readMessages: vi.fn() }; + return { service, prismaRepository, baileysCache }; +}; + +const message = (key: Record) => ({ + key: { id: 'MSG-1', fromMe: false, ...key }, + message: { conversation: 'hello' }, + messageTimestamp: 1700000000, + pushName: 'Someone', +}); + +const upsertWebhooks = (service: any) => + service.sendDataWebhook.mock.calls.filter(([event]) => event === Events.MESSAGES_UPSERT); + +const deliver = (service: any, received: unknown) => + service.messageHandle['messages.upsert']({ messages: [received], type: 'notify' }, {}); + +describe('messages.upsert dedupe', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('stores and emits a redelivered message once', async () => { + const { service, prismaRepository } = buildService(); + const received = { remoteJid: '5491123456789@s.whatsapp.net' }; + + await deliver(service, message(received)); + await deliver(service, message(received)); + + expect(prismaRepository.message.create).toHaveBeenCalledTimes(1); + expect(upsertWebhooks(service)).toHaveLength(1); + }); + + it('stores a message delivered with lid and phone jids once', async () => { + const { service, prismaRepository } = buildService(); + + await deliver(service, message({ remoteJid: '123456789@lid', remoteJidAlt: '5491123456789@s.whatsapp.net' })); + await deliver(service, message({ remoteJid: '5491123456789@s.whatsapp.net' })); + + expect(prismaRepository.message.create).toHaveBeenCalledTimes(1); + expect(upsertWebhooks(service)).toHaveLength(1); + }); + + it('processes the phone delivery again when the first lid delivery had no phone', async () => { + const { service, prismaRepository } = buildService(); + + await deliver(service, message({ remoteJid: '123456789@lid' })); + await deliver(service, message({ remoteJid: '123456789@lid' })); + await deliver(service, message({ remoteJid: '5491123456789@s.whatsapp.net' })); + + expect(prismaRepository.message.create).toHaveBeenCalledTimes(2); + expect(upsertWebhooks(service)).toHaveLength(2); + }); + + it('processes a redelivery when storing the first delivery failed', async () => { + const { service, prismaRepository } = buildService(); + prismaRepository.message.create.mockRejectedValueOnce(new Error('database unavailable')); + const received = { remoteJid: '5491123456789@s.whatsapp.net' }; + + await deliver(service, message(received)); + await deliver(service, message(received)); + + expect(prismaRepository.message.create).toHaveBeenCalledTimes(2); + expect(upsertWebhooks(service)).toHaveLength(1); + }); + + it('keeps processing when the dedupe cache fails', async () => { + const cache = buildCache(); + const failOnDedupeKey = async (key: string) => { + if (key.startsWith('message_seen_')) throw new Error('cache down'); + return undefined; + }; + cache.get.mockImplementation(failOnDedupeKey); + cache.set.mockImplementation(failOnDedupeKey); + const { service, prismaRepository } = buildService(cache); + + await deliver(service, message({ remoteJid: '5491123456789@s.whatsapp.net' })); + + expect(prismaRepository.message.create).toHaveBeenCalledTimes(1); + expect(upsertWebhooks(service)).toHaveLength(1); + }); +}); diff --git a/test/utils/messageDedupe.test.ts b/test/utils/messageDedupe.test.ts new file mode 100644 index 0000000000..cb092b813d --- /dev/null +++ b/test/utils/messageDedupe.test.ts @@ -0,0 +1,82 @@ +import { + hasResolvablePhone, + MESSAGE_DEDUPE_TTL_SECONDS, + messageDedupeKey, + messageDedupeMark, + shouldProcessMessage, +} from '@utils/messageDedupe'; +import { describe, expect, it } from 'vitest'; + +const lidKey = { id: 'MSG-1', remoteJid: '123456789@lid', fromMe: false }; +const pnKey = { id: 'MSG-1', remoteJid: '5491123456789@s.whatsapp.net', fromMe: false }; + +describe('messageDedupeKey', () => { + it('ignores the remoteJid so lid and phone deliveries share a key', () => { + expect(messageDedupeKey('instance-id', lidKey)).toBe(messageDedupeKey('instance-id', pnKey)); + expect(messageDedupeKey('instance-id', pnKey)).toBe('message_seen_instance-id_MSG-1_0'); + }); + + it('separates messages sent by me from received ones', () => { + expect(messageDedupeKey('instance-id', { ...pnKey, fromMe: true })).toBe('message_seen_instance-id_MSG-1_1'); + expect(messageDedupeKey('instance-id', { ...pnKey, fromMe: true })).not.toBe( + messageDedupeKey('instance-id', pnKey), + ); + }); + + it('separates instances', () => { + expect(messageDedupeKey('a', pnKey)).not.toBe(messageDedupeKey('b', pnKey)); + }); +}); + +describe('hasResolvablePhone', () => { + it.each([ + ['phone remoteJid', pnKey], + ['lid with phone remoteJidAlt', { ...lidKey, remoteJidAlt: '5491123456789@s.whatsapp.net' }], + ['lid with phone senderPn', { ...lidKey, senderPn: '5491123456789@s.whatsapp.net' }], + ['lid with phone participantAlt', { ...lidKey, participantAlt: '5491123456789@s.whatsapp.net' }], + ['group', { id: 'MSG-1', remoteJid: '120363000000000000@g.us' }], + ['status broadcast', { id: 'MSG-1', remoteJid: 'status@broadcast' }], + ['newsletter', { id: 'MSG-1', remoteJid: '120363000000000000@newsletter' }], + ])('is true for %s', (_, key) => { + expect(hasResolvablePhone(key)).toBe(true); + }); + + it.each([ + ['lid without alternatives', lidKey], + ['lid with lid alternative', { ...lidKey, remoteJidAlt: '987654321@lid' }], + ['missing remoteJid', { id: 'MSG-1' }], + ])('is false for %s', (_, key) => { + expect(hasResolvablePhone(key)).toBe(false); + }); +}); + +describe('messageDedupeMark', () => { + it('marks by phone resolvability', () => { + expect(messageDedupeMark(pnKey)).toBe('phone'); + expect(messageDedupeMark(lidKey)).toBe('no-phone'); + }); +}); + +describe('shouldProcessMessage', () => { + it('processes an unseen message', () => { + expect(shouldProcessMessage(undefined, lidKey)).toBe(true); + expect(shouldProcessMessage(null, pnKey)).toBe(true); + }); + + it('skips a message already seen with a phone', () => { + expect(shouldProcessMessage('phone', pnKey)).toBe(false); + expect(shouldProcessMessage('phone', lidKey)).toBe(false); + }); + + it('processes an upgrade from no-phone to a resolvable phone', () => { + expect(shouldProcessMessage('no-phone', pnKey)).toBe(true); + }); + + it('skips a repeated no-phone delivery', () => { + expect(shouldProcessMessage('no-phone', lidKey)).toBe(false); + }); + + it('keeps entries for 24 hours in seconds', () => { + expect(MESSAGE_DEDUPE_TTL_SECONDS).toBe(86400); + }); +}); From cb721b538dbb23803e7b05bc8c62d9433be59de7 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 16:20:14 -0300 Subject: [PATCH 37/96] fix(baileys): mark history messages seen only after they are stored History sync marked each message before the batch insert, so a failed insert hid later redeliveries. Messages are now deduped within the batch and marked after the insert. Messages without an id are never deduped, since they would all share one key. --- .../whatsapp/whatsapp.baileys.service.ts | 26 +++++++- .../whatsapp/messagesUpsertDedupe.test.ts | 63 ++++++++++++++++++- 2 files changed, 84 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 926b424708..5899ddf31b 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -963,6 +963,10 @@ export class BaileysStartupService extends ChannelStartupService { }; private async isDuplicateMessage(key: MessageDedupeKeyInput): Promise { + if (!key?.id) { + return false; + } + try { const seen = await this.baileysCache.get(messageDedupeKey(this.instanceId, key)); return !shouldProcessMessage(seen, key); @@ -973,6 +977,10 @@ export class BaileysStartupService extends ChannelStartupService { } private async markMessageSeen(key: MessageDedupeKeyInput): Promise { + if (!key?.id) { + return; + } + try { await this.baileysCache.set( messageDedupeKey(this.instanceId, key), @@ -1057,6 +1065,8 @@ export class BaileysStartupService extends ChannelStartupService { } const messagesRaw: any[] = []; + const seenInBatch = new Set(); + const storedKeys: proto.IMessageKey[] = []; const messagesRepository: Set = new Set( chatwootImport.getRepositoryMessagesCache(instance) ?? @@ -1091,9 +1101,17 @@ export class BaileysStartupService extends ChannelStartupService { } } - if (messagesRepository?.has(m.key.id) || (await this.isDuplicateMessage(m.key))) { + const batchDedupeKey = m.key.id ? messageDedupeKey(this.instanceId, m.key) : undefined; + if ( + messagesRepository?.has(m.key.id) || + (batchDedupeKey && seenInBatch.has(batchDedupeKey)) || + (await this.isDuplicateMessage(m.key)) + ) { continue; } + if (batchDedupeKey) { + seenInBatch.add(batchDedupeKey); + } if (!m.pushName && !m.key.fromMe) { const participantJid = m.participant || m.key.participant || m.key.remoteJid; @@ -1104,8 +1122,8 @@ export class BaileysStartupService extends ChannelStartupService { } } - await this.markMessageSeen(m.key); messagesRaw.push(this.prepareMessage(m)); + storedKeys.push(m.key); } this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw], true, undefined, { @@ -1117,6 +1135,10 @@ export class BaileysStartupService extends ChannelStartupService { await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true }); } + for (const storedKey of storedKeys) { + await this.markMessageSeen(storedKey); + } + if ( this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && diff --git a/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts b/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts index 7539b8b4eb..986cca64e6 100644 --- a/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts +++ b/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts @@ -14,6 +14,10 @@ vi.mock('@utils/onWhatsappCache', () => ({ vi.mock('@utils/sendTelemetry', () => ({ sendTelemetry: vi.fn() })); +vi.mock('@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper', () => ({ + chatwootImport: { getRepositoryMessagesCache: vi.fn(() => null), setRepositoryMessagesCache: vi.fn() }, +})); + const buildCache = () => { const store = new Map(); return { @@ -28,15 +32,19 @@ const buildCache = () => { const buildService = (baileysCache = buildCache()) => { const config: Record = { LOG: { BAILEYS: 'error' }, - DATABASE: { SAVE_DATA: { NEW_MESSAGE: true, CHATS: false, CONTACTS: false } }, + DATABASE: { SAVE_DATA: { NEW_MESSAGE: true, HISTORIC: true, CHATS: false, CONTACTS: false } }, CHATWOOT: { ENABLED: false }, OPENAI: { ENABLED: false }, S3: { ENABLE: false }, }; const configService = { get: vi.fn((key: string) => config[key]) } as any; const prismaRepository = { - chat: { findFirst: vi.fn(async () => null) }, - message: { create: vi.fn(async ({ data }) => ({ id: 'row-id', ...data })) }, + chat: { findFirst: vi.fn(async () => null), findMany: vi.fn(async () => []), createMany: vi.fn(async () => ({})) }, + message: { + create: vi.fn(async ({ data }) => ({ id: 'row-id', ...data })), + createMany: vi.fn(async () => ({})), + findMany: vi.fn(async () => []), + }, contact: { findFirst: vi.fn(async () => null) }, } as any; const service = new BaileysStartupService( @@ -55,6 +63,7 @@ const buildService = (baileysCache = buildCache()) => { service.updateMessagesReadedByTimestamp = vi.fn(async () => undefined); service.updateChatUnreadMessages = vi.fn(async () => undefined); service.client = { readMessages: vi.fn() }; + service.contactHandle = { 'contacts.upsert': vi.fn(async () => undefined) }; return { service, prismaRepository, baileysCache }; }; @@ -68,6 +77,12 @@ const message = (key: Record) => ({ const upsertWebhooks = (service: any) => service.sendDataWebhook.mock.calls.filter(([event]) => event === Events.MESSAGES_UPSERT); +const syncHistory = (service: any, messages: unknown[]) => + service.messageHandle['messaging-history.set']({ messages, chats: [], contacts: [], isLatest: true, progress: 100 }); + +const storedHistoryRows = (prismaRepository: any) => + prismaRepository.message.createMany.mock.calls.flatMap(([{ data }]) => data); + const deliver = (service: any, received: unknown) => service.messageHandle['messages.upsert']({ messages: [received], type: 'notify' }, {}); @@ -135,4 +150,46 @@ describe('messages.upsert dedupe', () => { expect(prismaRepository.message.create).toHaveBeenCalledTimes(1); expect(upsertWebhooks(service)).toHaveLength(1); }); + + it('processes every message that has no id', async () => { + const { service, prismaRepository } = buildService(); + + await deliver(service, message({ id: undefined, remoteJid: '5491123456789@s.whatsapp.net' })); + await deliver(service, message({ id: undefined, remoteJid: '5491187654321@s.whatsapp.net' })); + + expect(prismaRepository.message.create).toHaveBeenCalledTimes(2); + }); + + it('stores a history message delivered with lid and phone jids once', async () => { + const { service, prismaRepository } = buildService(); + + await syncHistory(service, [ + message({ remoteJid: '123456789@lid', remoteJidAlt: '5491123456789@s.whatsapp.net' }), + message({ remoteJid: '5491123456789@s.whatsapp.net' }), + ]); + + expect(storedHistoryRows(prismaRepository)).toHaveLength(1); + }); + + it('skips a live redelivery of a message stored by history sync', async () => { + const { service, prismaRepository } = buildService(); + const received = message({ remoteJid: '5491123456789@s.whatsapp.net' }); + + await syncHistory(service, [received]); + await deliver(service, received); + + expect(storedHistoryRows(prismaRepository)).toHaveLength(1); + expect(prismaRepository.message.create).not.toHaveBeenCalled(); + }); + + it('processes a live delivery when storing the history batch failed', async () => { + const { service, prismaRepository } = buildService(); + prismaRepository.message.createMany.mockRejectedValueOnce(new Error('database unavailable')); + const received = message({ remoteJid: '5491123456789@s.whatsapp.net' }); + + await syncHistory(service, [received]); + await deliver(service, received); + + expect(prismaRepository.message.create).toHaveBeenCalledTimes(1); + }); }); From ea460d3773d6cc6bde5414d75efa41651643171b Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 16:22:54 -0300 Subject: [PATCH 38/96] fix(baileys): keep the phone copy of a history message within one batch Within a history batch, a phone-addressed copy was dropped when an earlier lid copy without a phone had the same id. The batch now applies the same upgrade rule as live upserts. --- .../channel/whatsapp/whatsapp.baileys.service.ts | 7 ++++--- .../channel/whatsapp/messagesUpsertDedupe.test.ts | 11 +++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 5899ddf31b..826868d18a 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -87,6 +87,7 @@ import { MESSAGE_DEDUPE_TTL_SECONDS, messageDedupeKey, MessageDedupeKeyInput, + MessageDedupeMark, messageDedupeMark, shouldProcessMessage, } from '@utils/messageDedupe'; @@ -1065,7 +1066,7 @@ export class BaileysStartupService extends ChannelStartupService { } const messagesRaw: any[] = []; - const seenInBatch = new Set(); + const seenInBatch = new Map(); const storedKeys: proto.IMessageKey[] = []; const messagesRepository: Set = new Set( @@ -1104,13 +1105,13 @@ export class BaileysStartupService extends ChannelStartupService { const batchDedupeKey = m.key.id ? messageDedupeKey(this.instanceId, m.key) : undefined; if ( messagesRepository?.has(m.key.id) || - (batchDedupeKey && seenInBatch.has(batchDedupeKey)) || + (batchDedupeKey && !shouldProcessMessage(seenInBatch.get(batchDedupeKey), m.key)) || (await this.isDuplicateMessage(m.key)) ) { continue; } if (batchDedupeKey) { - seenInBatch.add(batchDedupeKey); + seenInBatch.set(batchDedupeKey, messageDedupeMark(m.key)); } if (!m.pushName && !m.key.fromMe) { diff --git a/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts b/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts index 986cca64e6..1a9a445b56 100644 --- a/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts +++ b/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts @@ -171,6 +171,17 @@ describe('messages.upsert dedupe', () => { expect(storedHistoryRows(prismaRepository)).toHaveLength(1); }); + it('keeps the phone copy of a history message whose lid copy had no phone', async () => { + const { service, prismaRepository } = buildService(); + + await syncHistory(service, [ + message({ remoteJid: '123456789@lid' }), + message({ remoteJid: '5491123456789@s.whatsapp.net' }), + ]); + + expect(storedHistoryRows(prismaRepository)).toHaveLength(2); + }); + it('skips a live redelivery of a message stored by history sync', async () => { const { service, prismaRepository } = buildService(); const received = message({ remoteJid: '5491123456789@s.whatsapp.net' }); From 1d58af3e3b9f2fc2ab954cf1fd61887616b2612e Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 16:26:31 -0300 Subject: [PATCH 39/96] fix(baileys): only live upserts mark messages as seen History sync emits MESSAGES_SET and API sends emit SEND_MESSAGE, while consumers act on MESSAGES_UPSERT. Marking from those paths suppressed the first live MESSAGES_UPSERT of the same message, so a consumer could never see it. Only live upserts mark now; history sync still reads the mark to avoid storing a live message again. --- .../channel/whatsapp/whatsapp.baileys.service.ts | 10 ---------- .../whatsapp/messagesUpsertDedupe.test.ts | 16 +++++++++++++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 826868d18a..6032dd609c 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1067,7 +1067,6 @@ export class BaileysStartupService extends ChannelStartupService { const messagesRaw: any[] = []; const seenInBatch = new Map(); - const storedKeys: proto.IMessageKey[] = []; const messagesRepository: Set = new Set( chatwootImport.getRepositoryMessagesCache(instance) ?? @@ -1124,7 +1123,6 @@ export class BaileysStartupService extends ChannelStartupService { } messagesRaw.push(this.prepareMessage(m)); - storedKeys.push(m.key); } this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw], true, undefined, { @@ -1136,10 +1134,6 @@ export class BaileysStartupService extends ChannelStartupService { await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true }); } - for (const storedKey of storedKeys) { - await this.markMessageSeen(storedKey); - } - if ( this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && @@ -2517,10 +2511,6 @@ export class BaileysStartupService extends ChannelStartupService { const messageRaw = this.prepareMessage(messageSent); - if (messageSent?.key) { - await this.markMessageSeen(messageSent.key); - } - const isMedia = messageSent?.message?.imageMessage || messageSent?.message?.videoMessage || diff --git a/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts b/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts index 1a9a445b56..3cef82d817 100644 --- a/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts +++ b/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts @@ -182,15 +182,25 @@ describe('messages.upsert dedupe', () => { expect(storedHistoryRows(prismaRepository)).toHaveLength(2); }); - it('skips a live redelivery of a message stored by history sync', async () => { + it('still delivers a live message first seen by history sync', async () => { const { service, prismaRepository } = buildService(); const received = message({ remoteJid: '5491123456789@s.whatsapp.net' }); await syncHistory(service, [received]); await deliver(service, received); - expect(storedHistoryRows(prismaRepository)).toHaveLength(1); - expect(prismaRepository.message.create).not.toHaveBeenCalled(); + expect(prismaRepository.message.create).toHaveBeenCalledTimes(1); + expect(upsertWebhooks(service)).toHaveLength(1); + }); + + it('does not store again in history sync a message already delivered live', async () => { + const { service, prismaRepository } = buildService(); + const received = message({ remoteJid: '5491123456789@s.whatsapp.net' }); + + await deliver(service, received); + await syncHistory(service, [received]); + + expect(storedHistoryRows(prismaRepository)).toHaveLength(0); }); it('processes a live delivery when storing the history batch failed', async () => { From e4e269c01489f0447a2fb50371f10b2dc6b7a8d4 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 16:34:36 -0300 Subject: [PATCH 40/96] fix(baileys): dedupe messages per recipient phone A broadcast list sent from the phone yields one fromMe copy per recipient with the same id, and the id-only key collapsed them into one, so consumers lost per-recipient messages. The key now includes the recipient resolved through remoteJidAlt: lid and phone copies of one person still dedupe, each broadcast recipient is processed once, and a lid copy without a phone no longer blocks the phone copy, which replaces the previous upgrade rule. --- .../whatsapp/whatsapp.baileys.service.ts | 24 ++---- src/utils/messageDedupe.ts | 33 +------- .../whatsapp/messagesUpsertDedupe.test.ts | 13 +++ test/utils/messageDedupe.test.ts | 82 +++++-------------- 4 files changed, 42 insertions(+), 110 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 6032dd609c..4ed8c9656e 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -83,14 +83,7 @@ import { Instance, Message } from '@prisma/client'; import { createJid } from '@utils/createJid'; import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent'; -import { - MESSAGE_DEDUPE_TTL_SECONDS, - messageDedupeKey, - MessageDedupeKeyInput, - MessageDedupeMark, - messageDedupeMark, - shouldProcessMessage, -} from '@utils/messageDedupe'; +import { MESSAGE_DEDUPE_TTL_SECONDS, messageDedupeKey, MessageDedupeKeyInput } from '@utils/messageDedupe'; import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; import { status } from '@utils/renderStatus'; import { resolvePhoneRemoteJid } from '@utils/resolvePhoneRemoteJid'; @@ -969,8 +962,7 @@ export class BaileysStartupService extends ChannelStartupService { } try { - const seen = await this.baileysCache.get(messageDedupeKey(this.instanceId, key)); - return !shouldProcessMessage(seen, key); + return Boolean(await this.baileysCache.get(messageDedupeKey(this.instanceId, key))); } catch (error) { this.logger.warn(`Message dedupe lookup failed, processing message: ${error?.message}`); return false; @@ -983,11 +975,7 @@ export class BaileysStartupService extends ChannelStartupService { } try { - await this.baileysCache.set( - messageDedupeKey(this.instanceId, key), - messageDedupeMark(key), - MESSAGE_DEDUPE_TTL_SECONDS, - ); + await this.baileysCache.set(messageDedupeKey(this.instanceId, key), 1, MESSAGE_DEDUPE_TTL_SECONDS); } catch (error) { this.logger.warn(`Message dedupe mark failed: ${error?.message}`); } @@ -1066,7 +1054,7 @@ export class BaileysStartupService extends ChannelStartupService { } const messagesRaw: any[] = []; - const seenInBatch = new Map(); + const seenInBatch = new Set(); const messagesRepository: Set = new Set( chatwootImport.getRepositoryMessagesCache(instance) ?? @@ -1104,13 +1092,13 @@ export class BaileysStartupService extends ChannelStartupService { const batchDedupeKey = m.key.id ? messageDedupeKey(this.instanceId, m.key) : undefined; if ( messagesRepository?.has(m.key.id) || - (batchDedupeKey && !shouldProcessMessage(seenInBatch.get(batchDedupeKey), m.key)) || + (batchDedupeKey && seenInBatch.has(batchDedupeKey)) || (await this.isDuplicateMessage(m.key)) ) { continue; } if (batchDedupeKey) { - seenInBatch.set(batchDedupeKey, messageDedupeMark(m.key)); + seenInBatch.add(batchDedupeKey); } if (!m.pushName && !m.key.fromMe) { diff --git a/src/utils/messageDedupe.ts b/src/utils/messageDedupe.ts index 8e8a0d11f4..90ceacab5b 100644 --- a/src/utils/messageDedupe.ts +++ b/src/utils/messageDedupe.ts @@ -1,43 +1,14 @@ -export type MessageDedupeMark = 'phone' | 'no-phone'; +import { resolvePhoneRemoteJid } from '@utils/resolvePhoneRemoteJid'; export type MessageDedupeKeyInput = { id?: string; fromMe?: boolean; remoteJid?: string; remoteJidAlt?: string; - senderPn?: string; - participantAlt?: string; }; export const MESSAGE_DEDUPE_TTL_SECONDS = 24 * 60 * 60; -const PHONE_SUFFIX = '@s.whatsapp.net'; -const NON_PERSONAL_SUFFIXES = ['@g.us', '@broadcast', '@newsletter']; - -const isPhoneJid = (jid?: string) => typeof jid === 'string' && jid.endsWith(PHONE_SUFFIX); - export function messageDedupeKey(instanceId: string, key: MessageDedupeKeyInput): string { - return `message_seen_${instanceId}_${key.id}_${key.fromMe ? 1 : 0}`; -} - -export function hasResolvablePhone(key: MessageDedupeKeyInput): boolean { - const remoteJid = key.remoteJid ?? ''; - - if (NON_PERSONAL_SUFFIXES.some((suffix) => remoteJid.endsWith(suffix))) { - return true; - } - - return [key.remoteJid, key.remoteJidAlt, key.senderPn, key.participantAlt].some(isPhoneJid); -} - -export function messageDedupeMark(key: MessageDedupeKeyInput): MessageDedupeMark { - return hasResolvablePhone(key) ? 'phone' : 'no-phone'; -} - -export function shouldProcessMessage(seen: MessageDedupeMark | null | undefined, key: MessageDedupeKeyInput): boolean { - if (!seen) { - return true; - } - - return seen === 'no-phone' && hasResolvablePhone(key); + return `message_seen_${instanceId}_${key.id}_${key.fromMe ? 1 : 0}_${resolvePhoneRemoteJid(key)}`; } diff --git a/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts b/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts index 3cef82d817..617ff03345 100644 --- a/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts +++ b/test/api/integrations/channel/whatsapp/messagesUpsertDedupe.test.ts @@ -112,6 +112,19 @@ describe('messages.upsert dedupe', () => { expect(upsertWebhooks(service)).toHaveLength(1); }); + it('processes each recipient copy of a broadcast once', async () => { + const { service, prismaRepository } = buildService(); + const recipients = ['5491111111111@s.whatsapp.net', '5492222222222@s.whatsapp.net', '5493333333333@s.whatsapp.net']; + + for (const remoteJid of recipients) { + await deliver(service, message({ remoteJid, fromMe: true })); + } + await deliver(service, message({ remoteJid: recipients[1], fromMe: true })); + + expect(prismaRepository.message.create).toHaveBeenCalledTimes(3); + expect(upsertWebhooks(service)).toHaveLength(3); + }); + it('processes the phone delivery again when the first lid delivery had no phone', async () => { const { service, prismaRepository } = buildService(); diff --git a/test/utils/messageDedupe.test.ts b/test/utils/messageDedupe.test.ts index cb092b813d..6b0dd65901 100644 --- a/test/utils/messageDedupe.test.ts +++ b/test/utils/messageDedupe.test.ts @@ -1,79 +1,39 @@ -import { - hasResolvablePhone, - MESSAGE_DEDUPE_TTL_SECONDS, - messageDedupeKey, - messageDedupeMark, - shouldProcessMessage, -} from '@utils/messageDedupe'; +import { MESSAGE_DEDUPE_TTL_SECONDS, messageDedupeKey } from '@utils/messageDedupe'; import { describe, expect, it } from 'vitest'; +const phone = '5491123456789@s.whatsapp.net'; const lidKey = { id: 'MSG-1', remoteJid: '123456789@lid', fromMe: false }; -const pnKey = { id: 'MSG-1', remoteJid: '5491123456789@s.whatsapp.net', fromMe: false }; +const pnKey = { id: 'MSG-1', remoteJid: phone, fromMe: false }; describe('messageDedupeKey', () => { - it('ignores the remoteJid so lid and phone deliveries share a key', () => { - expect(messageDedupeKey('instance-id', lidKey)).toBe(messageDedupeKey('instance-id', pnKey)); - expect(messageDedupeKey('instance-id', pnKey)).toBe('message_seen_instance-id_MSG-1_0'); - }); - - it('separates messages sent by me from received ones', () => { - expect(messageDedupeKey('instance-id', { ...pnKey, fromMe: true })).toBe('message_seen_instance-id_MSG-1_1'); - expect(messageDedupeKey('instance-id', { ...pnKey, fromMe: true })).not.toBe( + it('shares a key between a lid copy with phone alternative and the phone copy', () => { + expect(messageDedupeKey('instance-id', { ...lidKey, remoteJidAlt: phone })).toBe( messageDedupeKey('instance-id', pnKey), ); + expect(messageDedupeKey('instance-id', pnKey)).toBe(`message_seen_instance-id_MSG-1_0_${phone}`); }); - it('separates instances', () => { - expect(messageDedupeKey('a', pnKey)).not.toBe(messageDedupeKey('b', pnKey)); - }); -}); - -describe('hasResolvablePhone', () => { - it.each([ - ['phone remoteJid', pnKey], - ['lid with phone remoteJidAlt', { ...lidKey, remoteJidAlt: '5491123456789@s.whatsapp.net' }], - ['lid with phone senderPn', { ...lidKey, senderPn: '5491123456789@s.whatsapp.net' }], - ['lid with phone participantAlt', { ...lidKey, participantAlt: '5491123456789@s.whatsapp.net' }], - ['group', { id: 'MSG-1', remoteJid: '120363000000000000@g.us' }], - ['status broadcast', { id: 'MSG-1', remoteJid: 'status@broadcast' }], - ['newsletter', { id: 'MSG-1', remoteJid: '120363000000000000@newsletter' }], - ])('is true for %s', (_, key) => { - expect(hasResolvablePhone(key)).toBe(true); - }); - - it.each([ - ['lid without alternatives', lidKey], - ['lid with lid alternative', { ...lidKey, remoteJidAlt: '987654321@lid' }], - ['missing remoteJid', { id: 'MSG-1' }], - ])('is false for %s', (_, key) => { - expect(hasResolvablePhone(key)).toBe(false); - }); -}); - -describe('messageDedupeMark', () => { - it('marks by phone resolvability', () => { - expect(messageDedupeMark(pnKey)).toBe('phone'); - expect(messageDedupeMark(lidKey)).toBe('no-phone'); - }); -}); - -describe('shouldProcessMessage', () => { - it('processes an unseen message', () => { - expect(shouldProcessMessage(undefined, lidKey)).toBe(true); - expect(shouldProcessMessage(null, pnKey)).toBe(true); + it('separates copies sent to different recipients', () => { + expect(messageDedupeKey('instance-id', { ...pnKey, fromMe: true })).not.toBe( + messageDedupeKey('instance-id', { ...pnKey, fromMe: true, remoteJid: '5491187654321@s.whatsapp.net' }), + ); }); - it('skips a message already seen with a phone', () => { - expect(shouldProcessMessage('phone', pnKey)).toBe(false); - expect(shouldProcessMessage('phone', lidKey)).toBe(false); + it('separates a lid copy without phone alternative from the phone copy', () => { + expect(messageDedupeKey('instance-id', lidKey)).not.toBe(messageDedupeKey('instance-id', pnKey)); }); - it('processes an upgrade from no-phone to a resolvable phone', () => { - expect(shouldProcessMessage('no-phone', pnKey)).toBe(true); + it('separates messages sent by me from received ones', () => { + expect(messageDedupeKey('instance-id', { ...pnKey, fromMe: true })).toBe( + `message_seen_instance-id_MSG-1_1_${phone}`, + ); + expect(messageDedupeKey('instance-id', { ...pnKey, fromMe: true })).not.toBe( + messageDedupeKey('instance-id', pnKey), + ); }); - it('skips a repeated no-phone delivery', () => { - expect(shouldProcessMessage('no-phone', lidKey)).toBe(false); + it('separates instances', () => { + expect(messageDedupeKey('a', pnKey)).not.toBe(messageDedupeKey('b', pnKey)); }); it('keeps entries for 24 hours in seconds', () => { From f54a5a23421cff2865516cbb54deb28bb20079fd Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 16:50:23 -0300 Subject: [PATCH 41/96] fix(utils): build onWhatsapp cache jids with a single domain Numbers outside Brazil, Mexico and Argentina kept their domain and got it appended again, and numbers without a domain got @undefined. getAvailableNumbers now appends the domain to the bare number and defaults it to s.whatsapp.net. Existing rows still match because lookups use contains. --- src/utils/onWhatsappCache.ts | 4 +-- test/utils/onWhatsappCache.test.ts | 53 +++--------------------------- 2 files changed, 6 insertions(+), 51 deletions(-) diff --git a/src/utils/onWhatsappCache.ts b/src/utils/onWhatsappCache.ts index 08de0714e2..f296f7a202 100644 --- a/src/utils/onWhatsappCache.ts +++ b/src/utils/onWhatsappCache.ts @@ -12,7 +12,7 @@ function getAvailableNumbers(remoteJid: string) { remoteJid = remoteJid.slice(1); } - const [number, domain] = remoteJid.split('@'); + const [number, domain = 's.whatsapp.net'] = remoteJid.split('@'); // TODO: Se já for @lid, retornar apenas ele mesmo SEM adicionar @domain novamente if (domain === 'lid' || domain === 'g.us') { @@ -52,7 +52,7 @@ function getAvailableNumbers(remoteJid: string) { // Other countries else { - numbersAvailable.push(remoteJid); + numbersAvailable.push(number); } // TODO: Adiciona @domain apenas para números que não são @lid diff --git a/test/utils/onWhatsappCache.test.ts b/test/utils/onWhatsappCache.test.ts index cb5a7ed9f1..95dc5e4ceb 100644 --- a/test/utils/onWhatsappCache.test.ts +++ b/test/utils/onWhatsappCache.test.ts @@ -90,13 +90,7 @@ describe('getOnWhatsappCache', () => { expect(queriedJids()).toEqual(['5215512345678@s.whatsapp.net', '525512345678@s.whatsapp.net']); }); - it('KNOWN BUG: appends the domain twice to numbers outside Brazil, Mexico and Argentina (current behavior)', async () => { - await getOnWhatsappCache(['+15551234567@s.whatsapp.net']); - - expect(queriedJids()).toEqual(['15551234567@s.whatsapp.net@s.whatsapp.net']); - }); - - it.fails('searches other numbers without the plus sign and with a single domain', async () => { + it('searches other numbers without the plus sign and with a single domain', async () => { await getOnWhatsappCache(['+15551234567@s.whatsapp.net']); expect(queriedJids()).toEqual(['15551234567@s.whatsapp.net']); @@ -108,13 +102,7 @@ describe('getOnWhatsappCache', () => { expect(queriedJids()).toEqual(['123456@lid', '5511999999999-1623281429@g.us']); }); - it('KNOWN BUG: appends an undefined domain to numbers without a domain (current behavior)', async () => { - await getOnWhatsappCache(['15551234567']); - - expect(queriedJids()).toEqual(['15551234567@undefined']); - }); - - it.fails('searches numbers without a domain as user jids', async () => { + it('searches numbers without a domain as user jids', async () => { await getOnWhatsappCache(['15551234567']); expect(queriedJids()).toEqual(['15551234567@s.whatsapp.net']); @@ -153,20 +141,7 @@ describe('saveOnWhatsappCache', () => { }); }); - it('KNOWN BUG: looks up existing records by a jid option with the domain appended twice (current behavior)', async () => { - await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); - - expect(isOnWhatsapp.findFirst).toHaveBeenCalledWith({ - where: { - OR: [ - { jidOptions: { contains: '15551234567@s.whatsapp.net@s.whatsapp.net' } }, - { remoteJid: '15551234567@s.whatsapp.net' }, - ], - }, - }); - }); - - it.fails('looks up existing records by jid options and remote jid', async () => { + it('looks up existing records by jid options and remote jid', async () => { await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); expect(isOnWhatsapp.findFirst).toHaveBeenCalledWith({ @@ -276,27 +251,7 @@ describe('saveOnWhatsappCache', () => { }); }); - it('KNOWN BUG: merges a jid option with the domain appended twice into the update (current behavior)', async () => { - isOnWhatsapp.findFirst.mockResolvedValue({ - id: 'r1', - remoteJid: '15551234567@s.whatsapp.net', - jidOptions: '888@lid', - lid: null, - }); - - await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); - - expect(isOnWhatsapp.update).toHaveBeenCalledWith({ - where: { id: 'r1' }, - data: { - remoteJid: '15551234567@s.whatsapp.net', - jidOptions: '15551234567@s.whatsapp.net@s.whatsapp.net,888@lid', - lid: null, - }, - }); - }); - - it.fails('merges existing jid options into an update when data differs', async () => { + it('merges existing jid options into an update when data differs', async () => { isOnWhatsapp.findFirst.mockResolvedValue({ id: 'r1', remoteJid: '15551234567@s.whatsapp.net', From 32e9d7cfc6a110bf4efe38f964c5a6573498c438 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 16:52:12 -0300 Subject: [PATCH 42/96] fix(utils): retry onWhatsapp cache saves that lose a create race saveOnWhatsappCache processes a batch in parallel with find-then-create, so a jid repeated in a batch or saved by concurrent calls hit the unique remoteJid constraint (38 errors in one second on a production bulk contacts sync) and the losing writer's jid options, such as its lid, were dropped. A unique-constraint failure now reprocesses the item once, which finds the row and merges into it. --- src/utils/onWhatsappCache.ts | 197 ++++++++++++++++------------- test/utils/onWhatsappCache.test.ts | 56 ++++++++ 2 files changed, 163 insertions(+), 90 deletions(-) diff --git a/src/utils/onWhatsappCache.ts b/src/utils/onWhatsappCache.ts index f296f7a202..042efe8452 100644 --- a/src/utils/onWhatsappCache.ts +++ b/src/utils/onWhatsappCache.ts @@ -70,6 +70,112 @@ function normalizeJid(jid: string | null | undefined): string | null { return jid.startsWith('+') ? jid.slice(1) : jid; } +async function saveOnWhatsappCacheItem(item: ISaveOnWhatsappCacheParams) { + const remoteJid = normalizeJid(item.remoteJid); + if (!remoteJid) { + logger.warn('[saveOnWhatsappCache] Item skipped, missing remoteJid.'); + return; + } + + const altJidNormalized = normalizeJid(item.remoteJidAlt); + const lidAltJid = altJidNormalized && altJidNormalized.includes('@lid') ? altJidNormalized : null; + + const baseJids = [remoteJid]; // Garante que o remoteJid esteja na lista inicial + if (lidAltJid) { + baseJids.push(lidAltJid); + } + + const expandedJids = baseJids.flatMap((jid) => getAvailableNumbers(jid)); + + // 1. Busca entrada por jidOptions e também remoteJid + // Às vezes acontece do remoteJid atual NÃO ESTAR no jidOptions ainda, ocasionando o erro: + // 'Unique constraint failed on the fields: (`remoteJid`)' + // Isso acontece principalmente em grupos que possuem o número do criador no ID (ex.: '559911223345-1234567890@g.us') + const existingRecord = await prismaRepository.isOnWhatsapp.findFirst({ + where: { + OR: [ + ...expandedJids.map((jid) => ({ jidOptions: { contains: jid } })), + { remoteJid: remoteJid }, // TODO: Descobrir o motivo que causa o remoteJid não estar (às vezes) incluso na lista de jidOptions + ], + }, + }); + + logger.verbose( + `[saveOnWhatsappCache] Register exists for [${expandedJids.join(',')}]? => ${existingRecord ? existingRecord.remoteJid : 'Not found'}`, + ); + + // 2. Unifica todos os JIDs usando um Set para garantir valores únicos + const finalJidOptions = new Set(expandedJids); + + if (lidAltJid) { + finalJidOptions.add(lidAltJid); + } + + if (existingRecord?.jidOptions) { + existingRecord.jidOptions.split(',').forEach((jid) => finalJidOptions.add(jid)); + } + + // 3. Prepara o payload final + // Ordena os JIDs para garantir consistência na string final + const sortedJidOptions = [...finalJidOptions].sort(); + const newJidOptionsString = sortedJidOptions.join(','); + const newLid = item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null; + + const dataPayload = { + remoteJid: remoteJid, + jidOptions: newJidOptionsString, + lid: newLid, + }; + + // 4. Decide entre Criar ou Atualizar + if (existingRecord) { + // Compara a string de JIDs ordenada existente com a nova + const existingJidOptionsString = existingRecord.jidOptions + ? existingRecord.jidOptions.split(',').sort().join(',') + : ''; + + const isDataSame = + existingRecord.remoteJid === dataPayload.remoteJid && + existingJidOptionsString === dataPayload.jidOptions && + existingRecord.lid === dataPayload.lid; + + if (isDataSame) { + logger.verbose(`[saveOnWhatsappCache] Data for ${remoteJid} is already up-to-date. Skipping update.`); + return; // Pula para o próximo item + } + + // Os dados são diferentes, então atualiza + logger.verbose( + `[saveOnWhatsappCache] Register exists, updating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`, + ); + await prismaRepository.isOnWhatsapp.update({ + where: { id: existingRecord.id }, + data: dataPayload, + }); + } else { + // Cria nova entrada + logger.verbose( + `[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`, + ); + await prismaRepository.isOnWhatsapp.create({ + data: dataPayload, + }); + } +} + +const isUniqueConstraintError = (error: unknown) => (error as { code?: string })?.code === 'P2002'; + +async function saveOnWhatsappCacheItemWithRetry(item: ISaveOnWhatsappCacheParams) { + try { + await saveOnWhatsappCacheItem(item); + } catch (error) { + if (!isUniqueConstraintError(error)) { + throw error; + } + await saveOnWhatsappCacheItem(item); + } +} + export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { if (!configService.get('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) { return; @@ -78,96 +184,7 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { // Processa todos os itens em paralelo para melhor performance const processingPromises = data.map(async (item) => { try { - const remoteJid = normalizeJid(item.remoteJid); - if (!remoteJid) { - logger.warn('[saveOnWhatsappCache] Item skipped, missing remoteJid.'); - return; - } - - const altJidNormalized = normalizeJid(item.remoteJidAlt); - const lidAltJid = altJidNormalized && altJidNormalized.includes('@lid') ? altJidNormalized : null; - - const baseJids = [remoteJid]; // Garante que o remoteJid esteja na lista inicial - if (lidAltJid) { - baseJids.push(lidAltJid); - } - - const expandedJids = baseJids.flatMap((jid) => getAvailableNumbers(jid)); - - // 1. Busca entrada por jidOptions e também remoteJid - // Às vezes acontece do remoteJid atual NÃO ESTAR no jidOptions ainda, ocasionando o erro: - // 'Unique constraint failed on the fields: (`remoteJid`)' - // Isso acontece principalmente em grupos que possuem o número do criador no ID (ex.: '559911223345-1234567890@g.us') - const existingRecord = await prismaRepository.isOnWhatsapp.findFirst({ - where: { - OR: [ - ...expandedJids.map((jid) => ({ jidOptions: { contains: jid } })), - { remoteJid: remoteJid }, // TODO: Descobrir o motivo que causa o remoteJid não estar (às vezes) incluso na lista de jidOptions - ], - }, - }); - - logger.verbose( - `[saveOnWhatsappCache] Register exists for [${expandedJids.join(',')}]? => ${existingRecord ? existingRecord.remoteJid : 'Not found'}`, - ); - - // 2. Unifica todos os JIDs usando um Set para garantir valores únicos - const finalJidOptions = new Set(expandedJids); - - if (lidAltJid) { - finalJidOptions.add(lidAltJid); - } - - if (existingRecord?.jidOptions) { - existingRecord.jidOptions.split(',').forEach((jid) => finalJidOptions.add(jid)); - } - - // 3. Prepara o payload final - // Ordena os JIDs para garantir consistência na string final - const sortedJidOptions = [...finalJidOptions].sort(); - const newJidOptionsString = sortedJidOptions.join(','); - const newLid = item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null; - - const dataPayload = { - remoteJid: remoteJid, - jidOptions: newJidOptionsString, - lid: newLid, - }; - - // 4. Decide entre Criar ou Atualizar - if (existingRecord) { - // Compara a string de JIDs ordenada existente com a nova - const existingJidOptionsString = existingRecord.jidOptions - ? existingRecord.jidOptions.split(',').sort().join(',') - : ''; - - const isDataSame = - existingRecord.remoteJid === dataPayload.remoteJid && - existingJidOptionsString === dataPayload.jidOptions && - existingRecord.lid === dataPayload.lid; - - if (isDataSame) { - logger.verbose(`[saveOnWhatsappCache] Data for ${remoteJid} is already up-to-date. Skipping update.`); - return; // Pula para o próximo item - } - - // Os dados são diferentes, então atualiza - logger.verbose( - `[saveOnWhatsappCache] Register exists, updating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`, - ); - await prismaRepository.isOnWhatsapp.update({ - where: { id: existingRecord.id }, - data: dataPayload, - }); - } else { - // Cria nova entrada - logger.verbose( - `[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`, - ); - await prismaRepository.isOnWhatsapp.create({ - data: dataPayload, - }); - } + await saveOnWhatsappCacheItemWithRetry(item); } catch (e) { // Loga o erro mas não para a execução dos outros promises logger.error(`[saveOnWhatsappCache] Error processing item for ${item.remoteJid}: `); diff --git a/test/utils/onWhatsappCache.test.ts b/test/utils/onWhatsappCache.test.ts index 95dc5e4ceb..b81747b2de 100644 --- a/test/utils/onWhatsappCache.test.ts +++ b/test/utils/onWhatsappCache.test.ts @@ -280,3 +280,59 @@ describe('saveOnWhatsappCache', () => { expect(isOnWhatsapp.create).toHaveBeenCalledTimes(1); }); }); + +describe('saveOnWhatsappCache with concurrent writers', () => { + const rows: { id: string; remoteJid: string; jidOptions: string; lid: string | null }[] = []; + const tick = () => new Promise((resolve) => setTimeout(resolve, 0)); + + const uniqueViolation = () => + Object.assign(new Error('Unique constraint failed on the fields: (`remoteJid`)'), { code: 'P2002' }); + + beforeEach(() => { + rows.length = 0; + mocks.database.SAVE_DATA.IS_ON_WHATSAPP = true; + isOnWhatsapp.findFirst.mockImplementation(async ({ where }) => { + await tick(); + return ( + rows.find((row) => + where.OR.some((clause: any) => + clause.remoteJid ? row.remoteJid === clause.remoteJid : row.jidOptions.includes(clause.jidOptions.contains), + ), + ) ?? null + ); + }); + isOnWhatsapp.create.mockImplementation(async ({ data }) => { + await tick(); + if (rows.some((row) => row.remoteJid === data.remoteJid)) throw uniqueViolation(); + const row = { id: `row-${rows.length}`, ...data }; + rows.push(row); + return row; + }); + isOnWhatsapp.update.mockImplementation(async ({ where, data }) => { + await tick(); + const row = rows.find((candidate) => candidate.id === where.id); + Object.assign(row, data); + return row; + }); + }); + + it('saves a jid repeated in one batch as a single record without errors', async () => { + await saveOnWhatsappCache([ + { remoteJid: '5491123456789@s.whatsapp.net' }, + { remoteJid: '5491123456789@s.whatsapp.net', remoteJidAlt: '999@lid' }, + ]); + + expect(rows).toHaveLength(1); + expect(rows[0].jidOptions.split(',')).toContain('999@lid'); + }); + + it('saves the same jid from concurrent calls as a single record', async () => { + await Promise.all([ + saveOnWhatsappCache([{ remoteJid: '5491123456789@s.whatsapp.net' }]), + saveOnWhatsappCache([{ remoteJid: '5491123456789@s.whatsapp.net', remoteJidAlt: '999@lid' }]), + ]); + + expect(rows).toHaveLength(1); + expect(rows[0].jidOptions.split(',')).toContain('999@lid'); + }); +}); From 1e8f13a5528aa7d4d4028ae37bb975a3fd48b774 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 17:05:51 -0300 Subject: [PATCH 43/96] test(utils): cover onWhatsapp cache retry error paths --- test/utils/onWhatsappCache.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/utils/onWhatsappCache.test.ts b/test/utils/onWhatsappCache.test.ts index b81747b2de..3bf9b4f3f2 100644 --- a/test/utils/onWhatsappCache.test.ts +++ b/test/utils/onWhatsappCache.test.ts @@ -335,4 +335,29 @@ describe('saveOnWhatsappCache with concurrent writers', () => { expect(rows).toHaveLength(1); expect(rows[0].jidOptions.split(',')).toContain('999@lid'); }); + + it('does not retry a create that fails for another reason', async () => { + isOnWhatsapp.create.mockRejectedValueOnce(new Error('database unavailable')); + + await expect(saveOnWhatsappCache([{ remoteJid: '5491123456789@s.whatsapp.net' }])).resolves.toBeUndefined(); + + expect(isOnWhatsapp.create).toHaveBeenCalledTimes(1); + expect(rows).toHaveLength(0); + }); + + it('keeps saving other jids when the retry also fails', async () => { + const conflictingJid = '5491123456789@s.whatsapp.net'; + const createRow = isOnWhatsapp.create.getMockImplementation(); + isOnWhatsapp.create.mockImplementation(async (args) => { + if (args.data.remoteJid === conflictingJid) throw uniqueViolation(); + return createRow(args); + }); + + await expect( + saveOnWhatsappCache([{ remoteJid: conflictingJid }, { remoteJid: '5491187654321@s.whatsapp.net' }]), + ).resolves.toBeUndefined(); + + expect(isOnWhatsapp.create.mock.calls.filter(([args]) => args.data.remoteJid === conflictingJid)).toHaveLength(2); + expect(rows.map((row) => row.remoteJid)).toEqual(['5491187654321@s.whatsapp.net']); + }); }); From 5c19f3f9af7d3856db7f290c763f15f417cf5073 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 18:47:34 -0300 Subject: [PATCH 44/96] fix(baileys): share one transaction storage across sockets Ports WhiskeySockets/Baileys#2722 to rc14. addTransactionCapability created one AsyncLocalStorage per socket that was never released, so memory grew with every reconnect. One module-level storage now keys each store's transaction context by its own symbol, keeping stores isolated and same-store nesting unchanged. --- patches/baileys+7.0.0-rc14.patch | 80 +++++++++- .../baileysTransactionIsolation.test.ts | 138 ++++++++++++++++++ 2 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 test/patches/baileysTransactionIsolation.test.ts diff --git a/patches/baileys+7.0.0-rc14.patch b/patches/baileys+7.0.0-rc14.patch index 480630fa3f..d22bf6c564 100644 --- a/patches/baileys+7.0.0-rc14.patch +++ b/patches/baileys+7.0.0-rc14.patch @@ -11,7 +11,7 @@ index f2003ac..1f72102 100644 const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'primary_identity_pub')); const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_wrapped_primary_ephemeral_pub')); diff --git a/node_modules/baileys/lib/Socket/socket.js b/node_modules/baileys/lib/Socket/socket.js -index ae34a8f..944188d 100644 +index ae34a8f..9e6a580 100644 --- a/node_modules/baileys/lib/Socket/socket.js +++ b/node_modules/baileys/lib/Socket/socket.js @@ -6,7 +6,7 @@ import { proto } from '../../WAProto/index.js'; @@ -71,8 +71,82 @@ index ae34a8f..944188d 100644 // device paired for the first time // if device pairs successfully, the server asks to restart the connection ws.on('CB:iq,,pair-success', async (stanza) => { +diff --git a/node_modules/baileys/lib/Utils/auth-utils.js b/node_modules/baileys/lib/Utils/auth-utils.js +index 24a3583..183d237 100644 +--- a/node_modules/baileys/lib/Utils/auth-utils.js ++++ b/node_modules/baileys/lib/Utils/auth-utils.js +@@ -74,6 +74,7 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) { + } + }; + } ++const txStorage = new AsyncLocalStorage(); + /** + * Adds DB-like transaction capability to the SignalKeyStore + * Uses AsyncLocalStorage for automatic context management +@@ -82,7 +83,10 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) { + * @returns SignalKeyStore with transaction capability + */ + export const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetweenTriesMs }) => { +- const txStorage = new AsyncLocalStorage(); ++ const storeToken = Symbol('signalTxStore'); ++ function getContext() { ++ return txStorage.getStore()?.get(storeToken); ++ } + // Queues for concurrency control (keyed by signal data type - bounded set) + const keyQueues = new Map(); + // Transaction mutexes with reference counting for cleanup +@@ -135,7 +139,7 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela + * Check if currently in a transaction + */ + function isInTransaction() { +- return !!txStorage.getStore(); ++ return !!getContext(); + } + /** + * Commit transaction with retries +@@ -164,7 +168,7 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela + } + return { + get: async (type, ids) => { +- const ctx = txStorage.getStore(); ++ const ctx = getContext(); + if (!ctx) { + // No transaction - direct read without exclusive lock for concurrency + return state.get(type, ids); +@@ -191,7 +195,7 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela + return result; + }, + set: async (data) => { +- const ctx = txStorage.getStore(); ++ const ctx = getContext(); + if (!ctx) { + // No transaction - direct write with queue protection + const types = Object.keys(data); +@@ -229,7 +233,8 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela + }, + isInTransaction, + transaction: async (work, key) => { +- const existing = txStorage.getStore(); ++ const stores = txStorage.getStore(); ++ const existing = stores?.get(storeToken); + // Nested transaction - reuse existing context + if (existing) { + logger.trace('reusing existing transaction context'); +@@ -245,9 +250,11 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela + mutations: {}, + dbQueries: 0 + }; ++ const next = new Map(stores); ++ next.set(storeToken, ctx); + logger.trace('entering transaction'); + try { +- const result = await txStorage.run(ctx, work); ++ const result = await txStorage.run(next, work); + // Commit mutations + await commitWithRetry(ctx.mutations); + logger.trace({ dbQueries: ctx.dbQueries }, 'transaction completed'); diff --git a/node_modules/baileys/lib/Utils/companion-reg-client-utils.d.ts b/node_modules/baileys/lib/Utils/companion-reg-client-utils.d.ts -index a79a52c..3bc161c 100644 +index a79a52c..3c45d4e 100644 --- a/node_modules/baileys/lib/Utils/companion-reg-client-utils.d.ts +++ b/node_modules/baileys/lib/Utils/companion-reg-client-utils.d.ts @@ -1,4 +1,6 @@ @@ -103,7 +177,7 @@ index a79a52c..3bc161c 100644 //# sourceMappingURL=companion-reg-client-utils.d.ts.map \ No newline at end of file diff --git a/node_modules/baileys/lib/Utils/companion-reg-client-utils.js b/node_modules/baileys/lib/Utils/companion-reg-client-utils.js -index f5edebd..5021c8f 100644 +index f5edebd..9efdd5e 100644 --- a/node_modules/baileys/lib/Utils/companion-reg-client-utils.js +++ b/node_modules/baileys/lib/Utils/companion-reg-client-utils.js @@ -1,3 +1,5 @@ diff --git a/test/patches/baileysTransactionIsolation.test.ts b/test/patches/baileysTransactionIsolation.test.ts new file mode 100644 index 0000000000..a144fcb93f --- /dev/null +++ b/test/patches/baileysTransactionIsolation.test.ts @@ -0,0 +1,138 @@ +import { AsyncLocalStorage } from 'async_hooks'; +import { addTransactionCapability, SignalKeyStore } from 'baileys'; +import { describe, expect, it, vi } from 'vitest'; + +const silentLogger = (): any => ({ + level: 'silent', + child: () => silentLogger(), + trace: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + fatal: () => {}, +}); + +const makeInMemoryStore = () => { + const data: Record> = {}; + const store: SignalKeyStore = { + async get(type: string, ids: string[]) { + const bucket = data[type] ?? {}; + const out: Record = {}; + for (const id of ids) { + if (bucket[id] !== undefined) out[id] = bucket[id]; + } + return out as any; + }, + async set(d: any) { + for (const type of Object.keys(d)) { + data[type] = data[type] ?? {}; + for (const id of Object.keys(d[type])) { + const v = d[type][id]; + if (v === null || v === undefined) delete data[type][id]; + else data[type][id] = v; + } + } + }, + } as any; + return store; +}; + +const opts = { maxCommitRetries: 1, delayBetweenTriesMs: 1 }; +const read = async (store: SignalKeyStore, id = '1') => (await store.get('app-state-sync-key', [id]))[id]; +const write = (keyId: number, id = '1') => ({ 'app-state-sync-key': { [id]: { keyId } } }) as any; + +describe('addTransactionCapability isolation', () => { + it('writes to store B inside a transaction of store A go to B only', async () => { + const backingA = makeInMemoryStore(); + const backingB = makeInMemoryStore(); + const a = addTransactionCapability(backingA, silentLogger(), opts); + const b = addTransactionCapability(backingB, silentLogger(), opts); + + await a.transaction(async () => { + expect(b.isInTransaction()).toBe(false); + await b.set(write(9)); + await a.set(write(1)); + + expect(await read(backingB)).toMatchObject({ keyId: 9 }); + expect(await read(backingA)).toBeUndefined(); + }, 'key'); + + expect(await read(backingA)).toMatchObject({ keyId: 1 }); + expect(await read(backingB)).toMatchObject({ keyId: 9 }); + }); + + it('keeps a nested transaction on another store isolated', async () => { + const backingA = makeInMemoryStore(); + const backingB = makeInMemoryStore(); + const a = addTransactionCapability(backingA, silentLogger(), opts); + const b = addTransactionCapability(backingB, silentLogger(), opts); + + await a.transaction(async () => { + await a.set(write(1)); + await b.transaction(async () => { + expect(a.isInTransaction()).toBe(true); + expect(b.isInTransaction()).toBe(true); + await b.set(write(2)); + }, 'key'); + + expect(b.isInTransaction()).toBe(false); + expect(a.isInTransaction()).toBe(true); + expect(await read(backingB)).toMatchObject({ keyId: 2 }); + expect(await read(backingA)).toBeUndefined(); + }, 'key'); + + expect(await read(backingA)).toMatchObject({ keyId: 1 }); + }); + + it('reuses the context for a nested transaction on the same store', async () => { + const backing = makeInMemoryStore(); + const a = addTransactionCapability(backing, silentLogger(), opts); + + await a.transaction(async () => { + await a.set(write(1)); + await a.transaction(async () => { + await a.set(write(2, '2')); + expect(await read(a)).toMatchObject({ keyId: 1 }); + }, 'other'); + + expect(await read(backing, '2')).toBeUndefined(); + expect(await read(a, '2')).toMatchObject({ keyId: 2 }); + }, 'key'); + + expect(await read(backing)).toMatchObject({ keyId: 1 }); + expect(await read(backing, '2')).toMatchObject({ keyId: 2 }); + }); + + it('get inside a transaction sees pending writes of its own store only', async () => { + const backingA = makeInMemoryStore(); + const backingB = makeInMemoryStore(); + const a = addTransactionCapability(backingA, silentLogger(), opts); + const b = addTransactionCapability(backingB, silentLogger(), opts); + + await a.transaction(async () => { + await a.set(write(1)); + expect(await read(a)).toMatchObject({ keyId: 1 }); + expect(await read(b)).toBeUndefined(); + }, 'key'); + }); + + it('runs every transaction on one shared AsyncLocalStorage', async () => { + const instances = new Set>(); + const originalRun = AsyncLocalStorage.prototype.run; + vi.spyOn(AsyncLocalStorage.prototype, 'run').mockImplementation(function ( + this: AsyncLocalStorage, + ...args: Parameters + ) { + instances.add(this); + return originalRun.apply(this, args); + }); + + for (let i = 0; i < 20; i++) { + const keys = addTransactionCapability(makeInMemoryStore(), silentLogger(), opts); + await keys.transaction(async () => {}, `key-${i}`); + } + + expect(instances.size).toBe(1); + }); +}); From 7991933b7dde61b54f9330c10c4bd055b70e75b7 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 18:49:45 -0300 Subject: [PATCH 45/96] test(baileys): restore spies and cover concurrent store transactions --- .../baileysTransactionIsolation.test.ts | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/patches/baileysTransactionIsolation.test.ts b/test/patches/baileysTransactionIsolation.test.ts index a144fcb93f..bb34bb3585 100644 --- a/test/patches/baileysTransactionIsolation.test.ts +++ b/test/patches/baileysTransactionIsolation.test.ts @@ -1,6 +1,6 @@ import { AsyncLocalStorage } from 'async_hooks'; import { addTransactionCapability, SignalKeyStore } from 'baileys'; -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; const silentLogger = (): any => ({ level: 'silent', @@ -43,6 +43,36 @@ const read = async (store: SignalKeyStore, id = '1') => (await store.get('app-st const write = (keyId: number, id = '1') => ({ 'app-state-sync-key': { [id]: { keyId } } }) as any; describe('addTransactionCapability isolation', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('keeps concurrent transactions of different stores isolated', async () => { + const backingA = makeInMemoryStore(); + const backingB = makeInMemoryStore(); + const a = addTransactionCapability(backingA, silentLogger(), opts); + const b = addTransactionCapability(backingB, silentLogger(), opts); + const tick = () => new Promise((resolve) => setTimeout(resolve, 0)); + + await Promise.all([ + a.transaction(async () => { + await a.set(write(1)); + await tick(); + expect(await read(a)).toMatchObject({ keyId: 1 }); + await tick(); + }, 'key'), + b.transaction(async () => { + await tick(); + await b.set(write(2)); + expect(await read(b)).toMatchObject({ keyId: 2 }); + await tick(); + }, 'key'), + ]); + + expect(await read(backingA)).toMatchObject({ keyId: 1 }); + expect(await read(backingB)).toMatchObject({ keyId: 2 }); + }); + it('writes to store B inside a transaction of store A go to B only', async () => { const backingA = makeInMemoryStore(); const backingB = makeInMemoryStore(); From 2c6409b15f0263225670d594c10139955b741421 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 22 Oct 2026 18:57:40 -0300 Subject: [PATCH 46/96] docs(changelog): release v0.1.1 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0d4b7771..2f99efdf84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # evolution-in-purple +## 0.1.1 (2026-10-22) + +### Fixes + +* **Logout state**: `logoutInstance` closes the connection state (401), persists it and sends `STATUS_INSTANCE` and `CONNECTION_UPDATE` immediately instead of ~40s later; `logout.instance` is emitted after the Session row is removed and the late Baileys close is ignored (#9) +* **Argentine JIDs**: `createJid` keeps the mobile 9 (`549…`); the third-digit removal now applies only to Mexico (#10) +* **"Waiting for this message"**: `getMessage` returns no message for unknown keys, so Baileys stops resending an empty message on retry receipts (#11; upstream evolution-foundation/evolution-api#2705, #2706, #1934) +* **Duplicate messages**: live `messages.upsert` stores and emits each message once per recipient, deduping redeliveries and `@lid`/phone copies through the baileys cache; history sync no longer re-stores live messages (#12; upstream evolution-foundation/evolution-api#2737, #2675) +* **onWhatsapp cache**: unique-constraint races in `saveOnWhatsappCache` retry and merge instead of dropping data, and cache JIDs get a single domain (#13) +* **Baileys memory**: one shared `AsyncLocalStorage` for signal key transactions instead of one per socket, stopping growth with every reconnect (#14; port of WhiskeySockets/Baileys#2722) + +### Chores + +* Release images are no longer tagged `latest` (#8) + ## 0.1.0 (2026-10-21) ### Fixes From 9de623e7554c3cd01c681c4b34fd32b30abf8b72 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 11:16:38 -0300 Subject: [PATCH 47/96] fix(utils): save onWhatsapp cache items with bounded concurrency saveOnWhatsappCache started every item of a batch at once. A contacts sync of thousands of contacts opened thousands of concurrent queries, exhausting the Prisma pool: production logged 53,719 P2024 timeouts in one day, and unrelated queries (chat, settings, webhook config) timed out with them. Items are now processed by ten workers. --- src/utils/onWhatsappCache.ts | 24 +++++++++++++----------- test/utils/onWhatsappCache.test.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/utils/onWhatsappCache.ts b/src/utils/onWhatsappCache.ts index 042efe8452..42f0231ba0 100644 --- a/src/utils/onWhatsappCache.ts +++ b/src/utils/onWhatsappCache.ts @@ -163,6 +163,8 @@ async function saveOnWhatsappCacheItem(item: ISaveOnWhatsappCacheParams) { } } +const SAVE_CONCURRENCY = 10; + const isUniqueConstraintError = (error: unknown) => (error as { code?: string })?.code === 'P2002'; async function saveOnWhatsappCacheItemWithRetry(item: ISaveOnWhatsappCacheParams) { @@ -181,19 +183,19 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { return; } - // Processa todos os itens em paralelo para melhor performance - const processingPromises = data.map(async (item) => { - try { - await saveOnWhatsappCacheItemWithRetry(item); - } catch (e) { - // Loga o erro mas não para a execução dos outros promises - logger.error(`[saveOnWhatsappCache] Error processing item for ${item.remoteJid}: `); - logger.error(e); + const pending = [...data]; + const worker = async () => { + for (let item = pending.shift(); item; item = pending.shift()) { + try { + await saveOnWhatsappCacheItemWithRetry(item); + } catch (e) { + logger.error(`[saveOnWhatsappCache] Error processing item for ${item.remoteJid}: `); + logger.error(e); + } } - }); + }; - // Espera todas as operações paralelas terminarem - await Promise.allSettled(processingPromises); + await Promise.all(Array.from({ length: Math.min(SAVE_CONCURRENCY, pending.length) }, worker)); } export async function getOnWhatsappCache(remoteJids: string[]) { diff --git a/test/utils/onWhatsappCache.test.ts b/test/utils/onWhatsappCache.test.ts index 3bf9b4f3f2..3ba030e079 100644 --- a/test/utils/onWhatsappCache.test.ts +++ b/test/utils/onWhatsappCache.test.ts @@ -361,3 +361,29 @@ describe('saveOnWhatsappCache with concurrent writers', () => { expect(rows.map((row) => row.remoteJid)).toEqual(['5491187654321@s.whatsapp.net']); }); }); + +describe('saveOnWhatsappCache concurrency', () => { + beforeEach(() => { + mocks.database.SAVE_DATA.IS_ON_WHATSAPP = true; + }); + + it('never runs more than ten items against the database at once', async () => { + let inFlight = 0; + let peak = 0; + isOnWhatsapp.findFirst.mockImplementation(async () => { + inFlight += 1; + peak = Math.max(peak, inFlight); + await new Promise((resolve) => setTimeout(resolve, 1)); + inFlight -= 1; + return null; + }); + isOnWhatsapp.create.mockResolvedValue({}); + const items = Array.from({ length: 100 }, (_, index) => ({ remoteJid: `1555000${index}@s.whatsapp.net` })); + + await saveOnWhatsappCache(items); + + expect(isOnWhatsapp.create).toHaveBeenCalledTimes(100); + expect(peak).toBeGreaterThan(1); + expect(peak).toBeLessThanOrEqual(10); + }); +}); From 04571e71b10c6476c10d2b32fcbada9d7a8749af Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 11:19:08 -0300 Subject: [PATCH 48/96] fix(utils): keep onWhatsapp cache workers running past empty items Workers looped while shift() returned an item, so empty entries stopped them and the rest of the batch was never saved. Workers now advance a shared index, which also avoids O(n) shifts on large batches. --- src/utils/onWhatsappCache.ts | 9 +++++---- test/utils/onWhatsappCache.test.ts | 12 ++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/utils/onWhatsappCache.ts b/src/utils/onWhatsappCache.ts index 42f0231ba0..e1d6bf9c28 100644 --- a/src/utils/onWhatsappCache.ts +++ b/src/utils/onWhatsappCache.ts @@ -183,19 +183,20 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { return; } - const pending = [...data]; + let nextIndex = 0; const worker = async () => { - for (let item = pending.shift(); item; item = pending.shift()) { + while (nextIndex < data.length) { + const item = data[nextIndex++]; try { await saveOnWhatsappCacheItemWithRetry(item); } catch (e) { - logger.error(`[saveOnWhatsappCache] Error processing item for ${item.remoteJid}: `); + logger.error(`[saveOnWhatsappCache] Error processing item for ${item?.remoteJid}: `); logger.error(e); } } }; - await Promise.all(Array.from({ length: Math.min(SAVE_CONCURRENCY, pending.length) }, worker)); + await Promise.all(Array.from({ length: Math.min(SAVE_CONCURRENCY, data.length) }, worker)); } export async function getOnWhatsappCache(remoteJids: string[]) { diff --git a/test/utils/onWhatsappCache.test.ts b/test/utils/onWhatsappCache.test.ts index 3ba030e079..8cf75ffccb 100644 --- a/test/utils/onWhatsappCache.test.ts +++ b/test/utils/onWhatsappCache.test.ts @@ -386,4 +386,16 @@ describe('saveOnWhatsappCache concurrency', () => { expect(peak).toBeGreaterThan(1); expect(peak).toBeLessThanOrEqual(10); }); + + it('keeps saving the rest of the batch when it contains empty items', async () => { + isOnWhatsapp.findFirst.mockResolvedValue(null); + isOnWhatsapp.create.mockResolvedValue({}); + const items = Array.from({ length: 30 }, (_, index) => + index < 10 ? undefined : { remoteJid: `1555000${index}@s.whatsapp.net` }, + ); + + await saveOnWhatsappCache(items as any); + + expect(isOnWhatsapp.create).toHaveBeenCalledTimes(20); + }); }); From dd8e65deda0841a78d87973353a76ed3de79fb3c Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 11:22:14 -0300 Subject: [PATCH 49/96] fix(baileys): take the remote jid of a status update from the stored message WhatsApp sometimes sends a status update whose key has no remoteJid. The MESSAGES_UPDATE webhook went out without it and messageUpdate.create failed with "Argument remoteJid is missing", aborting the rest of the batch. The update now falls back to the remoteJid of the stored message it was matched to. --- .../whatsapp/whatsapp.baileys.service.ts | 1 + .../whatsapp/messagesUpdateRemoteJid.test.ts | 111 ++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 test/api/integrations/channel/whatsapp/messagesUpdateRemoteJid.test.ts diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 4ed8c9656e..667b033503 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1725,6 +1725,7 @@ export class BaileysStartupService extends ChannelStartupService { continue; } message.messageId = findMessage.id; + message.remoteJid = message.remoteJid ?? findMessage.key?.remoteJid; } if (update.message === null && update.status === undefined) { diff --git a/test/api/integrations/channel/whatsapp/messagesUpdateRemoteJid.test.ts b/test/api/integrations/channel/whatsapp/messagesUpdateRemoteJid.test.ts new file mode 100644 index 0000000000..e8e4ddf330 --- /dev/null +++ b/test/api/integrations/channel/whatsapp/messagesUpdateRemoteJid.test.ts @@ -0,0 +1,111 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { Events } from '@api/types/wa.types'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@api/server.module', () => ({ + chatbotController: { emit: vi.fn(async () => undefined) }, + waMonitor: {}, +})); + +vi.mock('@utils/onWhatsappCache', () => ({ + getOnWhatsappCache: vi.fn(async () => []), + saveOnWhatsappCache: vi.fn(async () => undefined), +})); + +vi.mock('@utils/sendTelemetry', () => ({ sendTelemetry: vi.fn() })); + +vi.mock('@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper', () => ({ + chatwootImport: { getRepositoryMessagesCache: vi.fn(() => null), setRepositoryMessagesCache: vi.fn() }, +})); + +const buildCache = () => { + const store = new Map(); + return { + store, + get: vi.fn(async (key: string) => store.get(key)), + set: vi.fn(async (key: string, value: unknown) => { + store.set(key, value); + }), + }; +}; + +const buildService = (baileysCache = buildCache()) => { + const config: Record = { + LOG: { BAILEYS: 'error' }, + DATABASE: { SAVE_DATA: { NEW_MESSAGE: true, HISTORIC: true, MESSAGE_UPDATE: true, CHATS: false, CONTACTS: false } }, + CHATWOOT: { ENABLED: false }, + OPENAI: { ENABLED: false }, + S3: { ENABLE: false }, + }; + const configService = { get: vi.fn((key: string) => config[key]) } as any; + const prismaRepository = { + chat: { findFirst: vi.fn(async () => null), findMany: vi.fn(async () => []), createMany: vi.fn(async () => ({})) }, + message: { + create: vi.fn(async ({ data }) => ({ id: 'row-id', ...data })), + createMany: vi.fn(async () => ({})), + findMany: vi.fn(async () => []), + update: vi.fn(async () => ({})), + }, + messageUpdate: { create: vi.fn(async () => ({})) }, + contact: { findFirst: vi.fn(async () => null) }, + } as any; + const service = new BaileysStartupService( + configService, + { emit: vi.fn() } as any, + prismaRepository, + {} as any, + {} as any, + baileysCache as any, + {} as any, + ) as any; + service.instance.id = 'instance-id'; + service.instance.name = 'instance-name'; + service.sendDataWebhook = vi.fn(); + service.profilePicture = vi.fn(async () => ({ profilePictureUrl: null })); + service.updateMessagesReadedByTimestamp = vi.fn(async () => undefined); + service.updateChatUnreadMessages = vi.fn(async () => undefined); + service.client = { readMessages: vi.fn() }; + service.contactHandle = { 'contacts.upsert': vi.fn(async () => undefined) }; + return { service, prismaRepository, baileysCache }; +}; + +const storedMessage = (id: string, remoteJid: string) => ({ + id: `row-${id}`, + key: { id, remoteJid, fromMe: true }, + status: 'SERVER_ACK', + messageTimestamp: 1700000000, +}); + +const updateWebhooks = (service: any) => + service.sendDataWebhook.mock.calls.filter(([event]) => event === Events.MESSAGES_UPDATE).map(([, data]) => data); + +describe('messages.update without a remote jid in the key', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('records and emits the update with the stored message remote jid and keeps processing the batch', async () => { + const { service, prismaRepository } = buildService(); + prismaRepository.$queryRaw = vi.fn(async (_strings: TemplateStringsArray, _instanceId: string, id: string) => [ + storedMessage(id, `${id === 'MSG-1' ? '5491111111111' : '5492222222222'}@s.whatsapp.net`), + ]); + + await service.messageHandle['messages.update']( + [ + { key: { id: 'MSG-1', fromMe: true }, update: { status: 3 } }, + { key: { id: 'MSG-2', fromMe: true, remoteJid: '5492222222222@s.whatsapp.net' }, update: { status: 3 } }, + ], + {}, + ); + + const recorded = prismaRepository.messageUpdate.create.mock.calls.map(([{ data }]) => data); + expect(recorded.map((data) => [data.keyId, data.remoteJid])).toEqual([ + ['MSG-1', '5491111111111@s.whatsapp.net'], + ['MSG-2', '5492222222222@s.whatsapp.net'], + ]); + expect(updateWebhooks(service).map((data) => data.remoteJid)).toEqual([ + '5491111111111@s.whatsapp.net', + '5492222222222@s.whatsapp.net', + ]); + }); +}); From 54a860a2fca9c16ddb8e5c05364cf82d872ed569 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 11:29:42 -0300 Subject: [PATCH 50/96] refactor(utils): share one iterator between onWhatsapp cache workers Workers pull items from a shared array iterator instead of a mutable index; behavior is unchanged. --- src/utils/onWhatsappCache.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils/onWhatsappCache.ts b/src/utils/onWhatsappCache.ts index e1d6bf9c28..a16f9e8c40 100644 --- a/src/utils/onWhatsappCache.ts +++ b/src/utils/onWhatsappCache.ts @@ -183,10 +183,9 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { return; } - let nextIndex = 0; + const items = data.values(); const worker = async () => { - while (nextIndex < data.length) { - const item = data[nextIndex++]; + for (const item of items) { try { await saveOnWhatsappCacheItemWithRetry(item); } catch (e) { From bcbb7c96630541e90af1247cc5ab97769da8184e Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 13:03:40 -0300 Subject: [PATCH 51/96] fix(baileys): parse plain jid group stub parameters The group 'modify' notification (participant number change) stores raw jids in messageStubParameters, but processMessage JSON.parsed every parameter, so it threw a SyntaxError and the group-participants.update event was lost (15 times in 30 days on a production instance). Parameters that are not JSON are now read as the participant id. --- patches/baileys+7.0.0-rc14.patch | 56 +++++++++++++++++++ .../baileysGroupStubParameters.test.ts | 47 ++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 test/patches/baileysGroupStubParameters.test.ts diff --git a/patches/baileys+7.0.0-rc14.patch b/patches/baileys+7.0.0-rc14.patch index d22bf6c564..35c8903bdf 100644 --- a/patches/baileys+7.0.0-rc14.patch +++ b/patches/baileys+7.0.0-rc14.patch @@ -231,3 +231,59 @@ index f5edebd..9efdd5e 100644 +}; //# sourceMappingURL=companion-reg-client-utils.js.map \ No newline at end of file +diff --git a/node_modules/baileys/lib/Utils/process-message.js b/node_modules/baileys/lib/Utils/process-message.js +index 37449db..b0267d7 100644 +--- a/node_modules/baileys/lib/Utils/process-message.js ++++ b/node_modules/baileys/lib/Utils/process-message.js +@@ -183,6 +183,14 @@ export function decryptEventResponse({ encPayload, encIv }, { eventCreatorJid, e + return Buffer.from(txt); + } + } ++const parseStubParticipant = (parameter) => { ++ try { ++ return JSON.parse(parameter); ++ } ++ catch { ++ return { id: parameter }; ++ } ++}; + const processMessage = async (message, { shouldProcessHistoryMsg, placeholderResendCache, ev, creds, signalRepository, keyStore, logger, options, getMessage }) => { + const meId = creds.me.id; + const { accountSettings } = creds; +@@ -507,12 +515,12 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes + const participantsIncludesMe = () => participants.find(jid => areJidsSameUser(meId, jid.phoneNumber)); // ADD SUPPORT FOR LID + switch (message.messageStubType) { + case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER: +- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || []; ++ participants = message.messageStubParameters.map(parseStubParticipant) || []; + emitParticipantsUpdate('modify'); + break; + case WAMessageStubType.GROUP_PARTICIPANT_LEAVE: + case WAMessageStubType.GROUP_PARTICIPANT_REMOVE: +- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || []; ++ participants = message.messageStubParameters.map(parseStubParticipant) || []; + emitParticipantsUpdate('remove'); + // mark the chat read only if you left the group + if (participantsIncludesMe()) { +@@ -522,18 +530,18 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes + case WAMessageStubType.GROUP_PARTICIPANT_ADD: + case WAMessageStubType.GROUP_PARTICIPANT_INVITE: + case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN: +- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || []; ++ participants = message.messageStubParameters.map(parseStubParticipant) || []; + if (participantsIncludesMe()) { + chat.readOnly = false; + } + emitParticipantsUpdate('add'); + break; + case WAMessageStubType.GROUP_PARTICIPANT_DEMOTE: +- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || []; ++ participants = message.messageStubParameters.map(parseStubParticipant) || []; + emitParticipantsUpdate('demote'); + break; + case WAMessageStubType.GROUP_PARTICIPANT_PROMOTE: +- participants = message.messageStubParameters.map((a) => JSON.parse(a)) || []; ++ participants = message.messageStubParameters.map(parseStubParticipant) || []; + emitParticipantsUpdate('promote'); + break; + case WAMessageStubType.GROUP_CHANGE_ANNOUNCE: diff --git a/test/patches/baileysGroupStubParameters.test.ts b/test/patches/baileysGroupStubParameters.test.ts new file mode 100644 index 0000000000..65f15d06e9 --- /dev/null +++ b/test/patches/baileysGroupStubParameters.test.ts @@ -0,0 +1,47 @@ +import { proto, WAMessageStubType } from 'baileys'; +import processMessage from 'baileys/lib/Utils/process-message.js'; +import { describe, expect, it, vi } from 'vitest'; + +const groupStub = (messageStubType: number, messageStubParameters: string[]) => + ({ + key: { remoteJid: '120363000000000000@g.us', fromMe: false, id: 'STUB-1', participant: '111111111111111@lid' }, + messageStubType, + messageStubParameters, + messageTimestamp: 1700000000, + }) as proto.IWebMessageInfo; + +const run = async (message: proto.IWebMessageInfo) => { + const ev = { emit: vi.fn() }; + await processMessage( + message as any, + { + shouldProcessHistoryMsg: false, + ev, + creds: { me: { id: '5491100000000@s.whatsapp.net', lid: '999999999999999@lid' } }, + signalRepository: {}, + keyStore: {}, + logger: undefined, + options: {}, + getMessage: vi.fn(), + } as any, + ); + return ev.emit.mock.calls.filter(([event]) => event === 'group-participants.update').map(([, data]) => data); +}; + +describe('processMessage group stub parameters', () => { + it('emits a number change whose parameters are plain jids', async () => { + const updates = await run(groupStub(WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER, ['222222222222222@lid'])); + + expect(updates).toHaveLength(1); + expect(updates[0]).toMatchObject({ id: '120363000000000000@g.us', action: 'modify' }); + expect(updates[0].participants).toEqual([{ id: '222222222222222@lid' }]); + }); + + it('keeps parsing participants serialized as JSON', async () => { + const participant = { id: '333333333333333@lid', phoneNumber: '5491133333333@s.whatsapp.net' }; + const updates = await run(groupStub(WAMessageStubType.GROUP_PARTICIPANT_ADD, [JSON.stringify(participant)])); + + expect(updates).toHaveLength(1); + expect(updates[0]).toMatchObject({ action: 'add', participants: [participant] }); + }); +}); From 64b5cad7967aceb2cdb39ce3dfd7ec38ffdfd625 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 13:05:43 -0300 Subject: [PATCH 52/96] fix(baileys): read non-object and phone stub parameters as participants A stub parameter that parses to a non-object (a bare number) is now read as a jid, and plain phone jids also fill phoneNumber, which processMessage uses to detect events about the connected account. --- patches/baileys+7.0.0-rc14.patch | 27 +++++++++++++------ .../baileysGroupStubParameters.test.ts | 11 ++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/patches/baileys+7.0.0-rc14.patch b/patches/baileys+7.0.0-rc14.patch index 35c8903bdf..69c0d2a90b 100644 --- a/patches/baileys+7.0.0-rc14.patch +++ b/patches/baileys+7.0.0-rc14.patch @@ -232,25 +232,36 @@ index f5edebd..9efdd5e 100644 //# sourceMappingURL=companion-reg-client-utils.js.map \ No newline at end of file diff --git a/node_modules/baileys/lib/Utils/process-message.js b/node_modules/baileys/lib/Utils/process-message.js -index 37449db..b0267d7 100644 +index 37449db..678f3d3 100644 --- a/node_modules/baileys/lib/Utils/process-message.js +++ b/node_modules/baileys/lib/Utils/process-message.js -@@ -183,6 +183,14 @@ export function decryptEventResponse({ encPayload, encIv }, { eventCreatorJid, e +@@ -2,7 +2,7 @@ import { Boom } from '@hapi/boom'; + import { proto } from '../../WAProto/index.js'; + import { WAMessageStubType } from '../Types/index.js'; + import { getContentType, normalizeMessageContent } from '../Utils/messages.js'; +-import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidStatusBroadcast, isLidUser, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary/index.js'; ++import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary/index.js'; + import { aesDecryptGCM, hmacSign } from './crypto.js'; + import { getKeyAuthor, toNumber } from './generics.js'; + import { downloadAndProcessHistorySyncNotification } from './history.js'; +@@ -183,6 +183,16 @@ export function decryptEventResponse({ encPayload, encIv }, { eventCreatorJid, e return Buffer.from(txt); } } +const parseStubParticipant = (parameter) => { + try { -+ return JSON.parse(parameter); -+ } -+ catch { -+ return { id: parameter }; ++ const parsed = JSON.parse(parameter); ++ if (parsed && typeof parsed === 'object') { ++ return parsed; ++ } + } ++ catch { } ++ return isPnUser(parameter) ? { id: parameter, phoneNumber: parameter } : { id: parameter }; +}; const processMessage = async (message, { shouldProcessHistoryMsg, placeholderResendCache, ev, creds, signalRepository, keyStore, logger, options, getMessage }) => { const meId = creds.me.id; const { accountSettings } = creds; -@@ -507,12 +515,12 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes +@@ -507,12 +517,12 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes const participantsIncludesMe = () => participants.find(jid => areJidsSameUser(meId, jid.phoneNumber)); // ADD SUPPORT FOR LID switch (message.messageStubType) { case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER: @@ -265,7 +276,7 @@ index 37449db..b0267d7 100644 emitParticipantsUpdate('remove'); // mark the chat read only if you left the group if (participantsIncludesMe()) { -@@ -522,18 +530,18 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes +@@ -522,18 +532,18 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes case WAMessageStubType.GROUP_PARTICIPANT_ADD: case WAMessageStubType.GROUP_PARTICIPANT_INVITE: case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN: diff --git a/test/patches/baileysGroupStubParameters.test.ts b/test/patches/baileysGroupStubParameters.test.ts index 65f15d06e9..52ccc24929 100644 --- a/test/patches/baileysGroupStubParameters.test.ts +++ b/test/patches/baileysGroupStubParameters.test.ts @@ -44,4 +44,15 @@ describe('processMessage group stub parameters', () => { expect(updates).toHaveLength(1); expect(updates[0]).toMatchObject({ action: 'add', participants: [participant] }); }); + + it('reads plain phone jids as the participant phone number', async () => { + const updates = await run( + groupStub(WAMessageStubType.GROUP_PARTICIPANT_REMOVE, ['5491100000000@s.whatsapp.net', '12345']), + ); + + expect(updates[0].participants).toEqual([ + { id: '5491100000000@s.whatsapp.net', phoneNumber: '5491100000000@s.whatsapp.net' }, + { id: '12345' }, + ]); + }); }); From 235b662b444433cf3f40d142df7202817e9e7439 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 13:10:05 -0300 Subject: [PATCH 53/96] fix(baileys): build link previews for pages without an image getUrlInfo uploaded a thumbnail even when the page had no image, so prepareWAMessageMedia received url undefined and threw. The whole preview was dropped and the message went out without one (61 times in 30 days on a production instance). The thumbnail upload now only runs when the page has an image. --- patches/baileys+7.0.0-rc14.patch | 13 +++++++++++ test/patches/baileysLinkPreview.test.ts | 30 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 test/patches/baileysLinkPreview.test.ts diff --git a/patches/baileys+7.0.0-rc14.patch b/patches/baileys+7.0.0-rc14.patch index d22bf6c564..8af6860476 100644 --- a/patches/baileys+7.0.0-rc14.patch +++ b/patches/baileys+7.0.0-rc14.patch @@ -231,3 +231,16 @@ index f5edebd..9efdd5e 100644 +}; //# sourceMappingURL=companion-reg-client-utils.js.map \ No newline at end of file +diff --git a/node_modules/baileys/lib/Utils/link-preview.js b/node_modules/baileys/lib/Utils/link-preview.js +index 0f4ff1c..1d29fe6 100644 +--- a/node_modules/baileys/lib/Utils/link-preview.js ++++ b/node_modules/baileys/lib/Utils/link-preview.js +@@ -56,7 +56,7 @@ export const getUrlInfo = async (text, opts = { + description: info.description, + originalThumbnailUrl: image + }; +- if (opts.uploadImage) { ++ if (opts.uploadImage && image) { + const { imageMessage } = await prepareWAMessageMedia({ image: { url: image } }, { + upload: opts.uploadImage, + mediaTypeOverride: 'thumbnail-link', diff --git a/test/patches/baileysLinkPreview.test.ts b/test/patches/baileysLinkPreview.test.ts new file mode 100644 index 0000000000..ba4a317f0f --- /dev/null +++ b/test/patches/baileysLinkPreview.test.ts @@ -0,0 +1,30 @@ +import { getUrlInfo } from 'baileys'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const pageUrl = 'https://plain-page.example/article'; +const pageWithoutImage = + 'Plain pagehi'; + +describe('getUrlInfo for a page without an image', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('returns the title and description without uploading a thumbnail', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(async (url: string) => { + const response = new Response(pageWithoutImage, { headers: { 'content-type': 'text/html' } }); + Object.defineProperty(response, 'url', { value: url }); + return response; + }), + ); + const uploadImage = vi.fn(); + + const info = await getUrlInfo(pageUrl, { thumbnailWidth: 192, fetchOpts: { timeout: 3000 }, uploadImage } as any); + + expect(info).toMatchObject({ title: 'Plain page', description: 'No image here', 'matched-text': pageUrl }); + expect(info.jpegThumbnail).toBeUndefined(); + expect(uploadImage).not.toHaveBeenCalled(); + }); +}); From b7c6eb4a8d1b8d102285f4be32509aa3d0fe01da Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Nogara Date: Fri, 23 Oct 2026 14:16:14 -0300 Subject: [PATCH 54/96] build(docker): pin the Node.js base image to 24.22.1 by digest Both stages used the floating node:24-alpine tag, so an image built before the base was refreshed keeps a Node.js release affected by the October 2026 security release (including CVE-2026-111497, a Brotli decoder out-of-bounds write reachable through axios media downloads). Rebuilding an old commit could also pick a different Node.js than the one it was tested on. Pin both stages through one NODE_IMAGE build arg to node:24.22.1-alpine3.24 at its multi-arch index digest (amd64 and arm64). Bumping Node.js is now an explicit change, and --build-arg NODE_IMAGE=... still allows an override. Upstream: evolution-foundation/evolution-api#2761 / #2765 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CPViW4Kzn5aFydKQgfV3yq --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index df2a043827..3225df2e4e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ -FROM node:24-alpine AS builder +ARG NODE_IMAGE=node:24.22.1-alpine3.24@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 + +FROM ${NODE_IMAGE} AS builder RUN apk update && \ apk add --no-cache git ffmpeg wget curl bash openssl @@ -31,7 +33,7 @@ RUN ./Docker/scripts/generate_database.sh RUN npm run build -FROM node:24-alpine AS final +FROM ${NODE_IMAGE} AS final RUN apk update && \ apk add tzdata ffmpeg bash openssl From fd083bd979e99b4857faac29a660402c4b84c9b8 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Nogara Date: Fri, 23 Oct 2026 14:17:43 -0300 Subject: [PATCH 55/96] fix(telemetry): send telemetry only when TELEMETRY_ENABLED=true Telemetry was on whenever TELEMETRY_ENABLED was unset, and the Docker image copies .env.example (TELEMETRY_ENABLED=true) into .env, so every container reported to log.evolution-api.com unless the operator found and flipped the flag. For a fork that self-hosters run to get away from upstream's phone-home, the safe default is off. Telemetry now runs only when TELEMETRY_ENABLED is exactly 'true', and both env examples ship with false. Operators who want it set it explicitly; TELEMETRY_URL is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CPViW4Kzn5aFydKQgfV3yq --- .env.example | 2 +- env.example | 2 +- src/config/env.config.ts | 2 +- test/config/telemetryConfig.test.ts | 23 +++++++++++++++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 test/config/telemetryConfig.test.ts diff --git a/.env.example b/.env.example index 73a3b40d35..090f4df1d7 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ SSL_CONF_FULLCHAIN=/path/to/cert.crt SENTRY_DSN= # Telemetry - Set to false to disable telemetry -TELEMETRY_ENABLED=true +TELEMETRY_ENABLED=false TELEMETRY_URL= # Prometheus metrics - Set to true to enable Prometheus metrics diff --git a/env.example b/env.example index 5fe448b82c..72ebd5230d 100644 --- a/env.example +++ b/env.example @@ -258,7 +258,7 @@ METRICS_ALLOWED_IPS= # =========================================== # TELEMETRIA # =========================================== -TELEMETRY_ENABLED=true +TELEMETRY_ENABLED=false TELEMETRY_URL= # =========================================== diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 7c4e382e7e..9b031b201a 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -878,7 +878,7 @@ export class ConfigService { ALLOWED_IPS: process.env?.METRICS_ALLOWED_IPS, }, TELEMETRY: { - ENABLED: process.env?.TELEMETRY_ENABLED === undefined || process.env?.TELEMETRY_ENABLED === 'true', + ENABLED: process.env?.TELEMETRY_ENABLED === 'true', URL: process.env?.TELEMETRY_URL, }, PROXY: { diff --git a/test/config/telemetryConfig.test.ts b/test/config/telemetryConfig.test.ts new file mode 100644 index 0000000000..b6deb3751f --- /dev/null +++ b/test/config/telemetryConfig.test.ts @@ -0,0 +1,23 @@ +import { ConfigService } from '@config/env.config'; +import { describe, expect, it, vi } from 'vitest'; + +const telemetryEnabled = () => new ConfigService().get('TELEMETRY').ENABLED; + +describe('TELEMETRY config', () => { + it('stays disabled when TELEMETRY_ENABLED is unset', () => { + vi.stubEnv('TELEMETRY_ENABLED', undefined); + expect(telemetryEnabled()).toBe(false); + }); + + it('turns on only when TELEMETRY_ENABLED is true', () => { + vi.stubEnv('TELEMETRY_ENABLED', 'true'); + expect(telemetryEnabled()).toBe(true); + }); + + it('stays disabled for any other value', () => { + for (const value of ['false', '', '1', 'TRUE']) { + vi.stubEnv('TELEMETRY_ENABLED', value); + expect(telemetryEnabled()).toBe(false); + } + }); +}); From 9903e6e1304d7d217398c65841d5459a60b95375 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Nogara Date: Fri, 23 Oct 2026 14:24:29 -0300 Subject: [PATCH 56/96] build(docker): write the pinned Node.js image as literal FROM lines Renovate and Dependabot reliably parse and update a literal `FROM image:tag@sha256:...`, but not a `FROM ${ARG}` indirection, so the NODE_IMAGE build arg would hide the base image from automated bumps. Both stages now carry the same pinned reference; a bot updates them together in one PR. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CPViW4Kzn5aFydKQgfV3yq --- Dockerfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3225df2e4e..08276a8e0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,4 @@ -ARG NODE_IMAGE=node:24.22.1-alpine3.24@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 - -FROM ${NODE_IMAGE} AS builder +FROM node:24.22.1-alpine3.24@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 AS builder RUN apk update && \ apk add --no-cache git ffmpeg wget curl bash openssl @@ -33,7 +31,7 @@ RUN ./Docker/scripts/generate_database.sh RUN npm run build -FROM ${NODE_IMAGE} AS final +FROM node:24.22.1-alpine3.24@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 AS final RUN apk update && \ apk add tzdata ffmpeg bash openssl From 80784f9c4eb590a1c9169e852c253aa100743620 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 14:48:24 -0300 Subject: [PATCH 57/96] feat(utils): store the onWhatsapp cache as one row per jid IsOnWhatsapp kept every variant of a number comma-joined in jidOptions, so lookups used unindexed LIKE '%..%' scans and saves did find-then-write per contact: a 2,500-contact sync issued ~5,000 queries and exhausted the Prisma pool in production. The new IsOnWhatsappJid table has one row per jid pointing to its group's canonical remoteJid. Lookups are two indexed queries; saves are two reads and one delete+insert transaction per 500-item chunk (about 30 queries for 2,500 contacts against Postgres). Writes are serialized per process, since concurrent read-merge-write transactions on shared jids collided on the primary key and dropped data; a unique violation from another process is retried once. The migration is additive: the old table is kept, unwritten, so an upstream image still runs, and Postgres backfills from jidOptions skipping malformed entries. --- .../migration.sql | 10 + prisma/mysql-schema.prisma | 9 + .../migration.sql | 27 + prisma/postgresql-schema.prisma | 9 + prisma/psql_bouncer-schema.prisma | 9 + src/utils/onWhatsappCache.ts | 275 +++++----- test/utils/onWhatsappCache.test.ts | 482 ++++++++---------- 7 files changed, 422 insertions(+), 399 deletions(-) create mode 100644 prisma/mysql-migrations/20261023120000_add_is_on_whatsapp_jid/migration.sql create mode 100644 prisma/postgresql-migrations/20261023120000_add_is_on_whatsapp_jid/migration.sql diff --git a/prisma/mysql-migrations/20261023120000_add_is_on_whatsapp_jid/migration.sql b/prisma/mysql-migrations/20261023120000_add_is_on_whatsapp_jid/migration.sql new file mode 100644 index 0000000000..29a04fc32e --- /dev/null +++ b/prisma/mysql-migrations/20261023120000_add_is_on_whatsapp_jid/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE `IsOnWhatsappJid` ( + `jid` VARCHAR(100) NOT NULL, + `remoteJid` VARCHAR(100) NOT NULL, + `lid` VARCHAR(100) NULL, + `updatedAt` TIMESTAMP NOT NULL, + + INDEX `IsOnWhatsappJid_remoteJid_idx`(`remoteJid`), + PRIMARY KEY (`jid`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/prisma/mysql-schema.prisma b/prisma/mysql-schema.prisma index 71b5a743f0..16f6103694 100644 --- a/prisma/mysql-schema.prisma +++ b/prisma/mysql-schema.prisma @@ -659,6 +659,15 @@ model IsOnWhatsapp { updatedAt DateTime @updatedAt @db.Timestamp } +model IsOnWhatsappJid { + jid String @id @db.VarChar(100) + remoteJid String @db.VarChar(100) + lid String? @db.VarChar(100) + updatedAt DateTime @updatedAt @db.Timestamp + + @@index([remoteJid]) +} + model N8n { id String @id @default(cuid()) enabled Boolean @default(true) @db.TinyInt() diff --git a/prisma/postgresql-migrations/20261023120000_add_is_on_whatsapp_jid/migration.sql b/prisma/postgresql-migrations/20261023120000_add_is_on_whatsapp_jid/migration.sql new file mode 100644 index 0000000000..0144101b61 --- /dev/null +++ b/prisma/postgresql-migrations/20261023120000_add_is_on_whatsapp_jid/migration.sql @@ -0,0 +1,27 @@ +-- CreateTable +CREATE TABLE "IsOnWhatsappJid" ( + "jid" VARCHAR(100) NOT NULL, + "remoteJid" VARCHAR(100) NOT NULL, + "lid" VARCHAR(100), + "updatedAt" TIMESTAMP NOT NULL, + + CONSTRAINT "IsOnWhatsappJid_pkey" PRIMARY KEY ("jid") +); + +-- CreateIndex +CREATE INDEX "IsOnWhatsappJid_remoteJid_idx" ON "IsOnWhatsappJid"("remoteJid"); + +-- Backfill from IsOnWhatsapp, one row per jid option +INSERT INTO "IsOnWhatsappJid" ("jid", "remoteJid", "lid", "updatedAt") +SELECT DISTINCT ON ("jid") "jid", "remoteJid", "lid", "updatedAt" +FROM ( + SELECT trim(option) AS "jid", o."remoteJid", o."lid", o."updatedAt" + FROM "IsOnWhatsapp" o + CROSS JOIN LATERAL unnest(string_to_array(o."jidOptions", ',') || o."remoteJid") AS option +) expanded +WHERE "jid" <> '' + AND length("jid") <= 100 + AND length("jid") - length(replace("jid", '@', '')) <= 1 + AND "jid" NOT LIKE '%@undefined' +ORDER BY "jid", "updatedAt" DESC +ON CONFLICT ("jid") DO NOTHING; diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index 6b98f88da4..64ff71f4a7 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -665,6 +665,15 @@ model IsOnWhatsapp { updatedAt DateTime @updatedAt @db.Timestamp } +model IsOnWhatsappJid { + jid String @id @db.VarChar(100) + remoteJid String @db.VarChar(100) + lid String? @db.VarChar(100) + updatedAt DateTime @updatedAt @db.Timestamp + + @@index([remoteJid]) +} + model N8n { id String @id @default(cuid()) enabled Boolean @default(true) @db.Boolean diff --git a/prisma/psql_bouncer-schema.prisma b/prisma/psql_bouncer-schema.prisma index a3f4dbe902..3e8327ae50 100644 --- a/prisma/psql_bouncer-schema.prisma +++ b/prisma/psql_bouncer-schema.prisma @@ -665,6 +665,15 @@ model IsOnWhatsapp { updatedAt DateTime @updatedAt @db.Timestamp } +model IsOnWhatsappJid { + jid String @id @db.VarChar(100) + remoteJid String @db.VarChar(100) + lid String? @db.VarChar(100) + updatedAt DateTime @updatedAt @db.Timestamp + + @@index([remoteJid]) +} + model N8n { id String @id @default(cuid()) enabled Boolean @default(true) @db.Boolean diff --git a/src/utils/onWhatsappCache.ts b/src/utils/onWhatsappCache.ts index a16f9e8c40..58e48beb5d 100644 --- a/src/utils/onWhatsappCache.ts +++ b/src/utils/onWhatsappCache.ts @@ -14,12 +14,10 @@ function getAvailableNumbers(remoteJid: string) { const [number, domain = 's.whatsapp.net'] = remoteJid.split('@'); - // TODO: Se já for @lid, retornar apenas ele mesmo SEM adicionar @domain novamente if (domain === 'lid' || domain === 'g.us') { - return [remoteJid]; // Retorna direto para @lid e @g.us + return [remoteJid]; } - // Brazilian numbers if (remoteJid.startsWith('55')) { const numberWithDigit = number.slice(4, 5) === '9' && number.length === 13 ? number : `${number.slice(0, 4)}9${number.slice(4)}`; @@ -27,11 +25,7 @@ function getAvailableNumbers(remoteJid: string) { numbersAvailable.push(numberWithDigit); numbersAvailable.push(numberWithoutDigit); - } - - // Mexican/Argentina numbers - // Ref: https://faq.whatsapp.com/1294841057948784 - else if (number.startsWith('52') || number.startsWith('54')) { + } else if (number.startsWith('52') || number.startsWith('54')) { let prefix = ''; if (number.startsWith('52')) { prefix = '1'; @@ -48,14 +42,10 @@ function getAvailableNumbers(remoteJid: string) { numbersAvailable.push(numberWithDigit); numbersAvailable.push(numberWithoutDigit); - } - - // Other countries - else { + } else { numbersAvailable.push(number); } - // TODO: Adiciona @domain apenas para números que não são @lid return numbersAvailable.map((number) => `${number}@${domain}`); } @@ -65,117 +55,109 @@ interface ISaveOnWhatsappCacheParams { lid?: 'lid' | undefined; } +interface CacheGroup { + remoteJid: string; + jids: Set; + lid: string | null; +} + +interface NormalizedItem { + remoteJid: string; + jids: string[]; + lid: string | null; +} + +const SAVE_CHUNK_SIZE = 500; + function normalizeJid(jid: string | null | undefined): string | null { if (!jid) return null; return jid.startsWith('+') ? jid.slice(1) : jid; } -async function saveOnWhatsappCacheItem(item: ISaveOnWhatsappCacheParams) { - const remoteJid = normalizeJid(item.remoteJid); +function normalizeItem(item: ISaveOnWhatsappCacheParams | undefined): NormalizedItem | null { + const remoteJid = normalizeJid(item?.remoteJid); if (!remoteJid) { logger.warn('[saveOnWhatsappCache] Item skipped, missing remoteJid.'); - return; + return null; } - const altJidNormalized = normalizeJid(item.remoteJidAlt); - const lidAltJid = altJidNormalized && altJidNormalized.includes('@lid') ? altJidNormalized : null; + const altJid = normalizeJid(item.remoteJidAlt); + const baseJids = altJid && altJid.includes('@lid') ? [remoteJid, altJid] : [remoteJid]; - const baseJids = [remoteJid]; // Garante que o remoteJid esteja na lista inicial - if (lidAltJid) { - baseJids.push(lidAltJid); + return { + remoteJid, + jids: [...new Set(baseJids.flatMap((jid) => getAvailableNumbers(jid)))], + lid: item.lid === 'lid' || item.remoteJid.includes('@lid') ? 'lid' : null, + }; +} + +async function loadExistingGroups(jids: string[]) { + const groups = new Map(); + const hits = await prismaRepository.isOnWhatsappJid.findMany({ where: { jid: { in: jids } } }); + const canonicals = [...new Set(hits.map((row) => row.remoteJid))]; + if (canonicals.length === 0) return groups; + + const rows = await prismaRepository.isOnWhatsappJid.findMany({ where: { remoteJid: { in: canonicals } } }); + for (const row of [...hits, ...rows]) { + const group = groups.get(row.remoteJid) ?? { remoteJid: row.remoteJid, jids: new Set(), lid: null }; + group.jids.add(row.jid); + group.lid = group.lid ?? row.lid ?? null; + groups.set(row.remoteJid, group); } + return groups; +} - const expandedJids = baseJids.flatMap((jid) => getAvailableNumbers(jid)); - - // 1. Busca entrada por jidOptions e também remoteJid - // Às vezes acontece do remoteJid atual NÃO ESTAR no jidOptions ainda, ocasionando o erro: - // 'Unique constraint failed on the fields: (`remoteJid`)' - // Isso acontece principalmente em grupos que possuem o número do criador no ID (ex.: '559911223345-1234567890@g.us') - const existingRecord = await prismaRepository.isOnWhatsapp.findFirst({ - where: { - OR: [ - ...expandedJids.map((jid) => ({ jidOptions: { contains: jid } })), - { remoteJid: remoteJid }, // TODO: Descobrir o motivo que causa o remoteJid não estar (às vezes) incluso na lista de jidOptions - ], - }, - }); +function mergeGroups(items: NormalizedItem[], existing: Map) { + const existingByJid = new Map(); + existing.forEach((group) => group.jids.forEach((jid) => existingByJid.set(jid, group))); - logger.verbose( - `[saveOnWhatsappCache] Register exists for [${expandedJids.join(',')}]? => ${existingRecord ? existingRecord.remoteJid : 'Not found'}`, - ); + const builtByJid = new Map(); + for (const item of items) { + const group: CacheGroup = { remoteJid: item.remoteJid, jids: new Set(item.jids), lid: item.lid }; - // 2. Unifica todos os JIDs usando um Set para garantir valores únicos - const finalJidOptions = new Set(expandedJids); + for (const jid of item.jids) { + const found = existingByJid.get(jid); + if (!found) continue; + found.jids.forEach((existingJid) => group.jids.add(existingJid)); + group.lid = group.lid ?? found.lid; + } - if (lidAltJid) { - finalJidOptions.add(lidAltJid); - } + const previous = new Set([...group.jids].map((jid) => builtByJid.get(jid)).filter(Boolean)); + for (const earlier of previous) { + earlier.jids.forEach((jid) => group.jids.add(jid)); + group.lid = group.lid ?? earlier.lid; + } - if (existingRecord?.jidOptions) { - existingRecord.jidOptions.split(',').forEach((jid) => finalJidOptions.add(jid)); + group.jids.forEach((jid) => builtByJid.set(jid, group)); } - // 3. Prepara o payload final - // Ordena os JIDs para garantir consistência na string final - const sortedJidOptions = [...finalJidOptions].sort(); - const newJidOptionsString = sortedJidOptions.join(','); - const newLid = item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null; + return new Set(builtByJid.values()); +} - const dataPayload = { - remoteJid: remoteJid, - jidOptions: newJidOptionsString, - lid: newLid, - }; +async function saveChunk(items: NormalizedItem[]) { + const variants = [...new Set(items.flatMap((item) => item.jids))]; + const groups = mergeGroups(items, await loadExistingGroups(variants)); - // 4. Decide entre Criar ou Atualizar - if (existingRecord) { - // Compara a string de JIDs ordenada existente com a nova - const existingJidOptionsString = existingRecord.jidOptions - ? existingRecord.jidOptions.split(',').sort().join(',') - : ''; - - const isDataSame = - existingRecord.remoteJid === dataPayload.remoteJid && - existingJidOptionsString === dataPayload.jidOptions && - existingRecord.lid === dataPayload.lid; - - if (isDataSame) { - logger.verbose(`[saveOnWhatsappCache] Data for ${remoteJid} is already up-to-date. Skipping update.`); - return; // Pula para o próximo item - } + const rows = new Map(); + groups.forEach((group) => + group.jids.forEach((jid) => rows.set(jid, { jid, remoteJid: group.remoteJid, lid: group.lid })), + ); + const jids = [...rows.keys()]; - // Os dados são diferentes, então atualiza - logger.verbose( - `[saveOnWhatsappCache] Register exists, updating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`, - ); - await prismaRepository.isOnWhatsapp.update({ - where: { id: existingRecord.id }, - data: dataPayload, - }); - } else { - // Cria nova entrada - logger.verbose( - `[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`, - ); - await prismaRepository.isOnWhatsapp.create({ - data: dataPayload, - }); - } -} + logger.verbose(`[saveOnWhatsappCache] Saving ${jids.length} jids in ${groups.size} groups`); -const SAVE_CONCURRENCY = 10; + await prismaRepository.$transaction([ + prismaRepository.isOnWhatsappJid.deleteMany({ where: { jid: { in: jids } } }), + prismaRepository.isOnWhatsappJid.createMany({ data: [...rows.values()] }), + ]); +} -const isUniqueConstraintError = (error: unknown) => (error as { code?: string })?.code === 'P2002'; +const writeQueue: { tail: Promise } = { tail: Promise.resolve() }; -async function saveOnWhatsappCacheItemWithRetry(item: ISaveOnWhatsappCacheParams) { - try { - await saveOnWhatsappCacheItem(item); - } catch (error) { - if (!isUniqueConstraintError(error)) { - throw error; - } - await saveOnWhatsappCacheItem(item); - } +function enqueueWrite(write: () => Promise): Promise { + const run = writeQueue.tail.then(write); + writeQueue.tail = run.catch(() => undefined); + return run; } export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { @@ -183,48 +165,79 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { return; } - const items = data.values(); - const worker = async () => { - for (const item of items) { - try { - await saveOnWhatsappCacheItemWithRetry(item); - } catch (e) { - logger.error(`[saveOnWhatsappCache] Error processing item for ${item?.remoteJid}: `); - logger.error(e); - } - } - }; + const items = data.map(normalizeItem).filter((item): item is NormalizedItem => item !== null); + await enqueueWrite(() => saveItems(items)); +} + +async function saveChunkWithRetry(chunk: NormalizedItem[]) { + try { + await saveChunk(chunk); + } catch (error) { + if ((error as { code?: string })?.code !== 'P2002') throw error; + await saveChunk(chunk); + } +} + +async function saveItems(items: NormalizedItem[]) { + const chunks = Array.from({ length: Math.ceil(items.length / SAVE_CHUNK_SIZE) }, (_, index) => + items.slice(index * SAVE_CHUNK_SIZE, (index + 1) * SAVE_CHUNK_SIZE), + ); - await Promise.all(Array.from({ length: Math.min(SAVE_CONCURRENCY, data.length) }, worker)); + for (const [index, chunk] of chunks.entries()) { + try { + await saveChunkWithRetry(chunk); + } catch (error) { + logger.error(`[saveOnWhatsappCache] Error saving chunk starting at item ${index * SAVE_CHUNK_SIZE}: `); + logger.error(error); + } + } } export async function getOnWhatsappCache(remoteJids: string[]) { - let results: { + const results: { remoteJid: string; number: string; jidOptions: string[]; lid?: string; }[] = []; - if (configService.get('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) { - const remoteJidsWithoutPlus = remoteJids.map((remoteJid) => getAvailableNumbers(remoteJid)).flat(); - - const onWhatsappCache = await prismaRepository.isOnWhatsapp.findMany({ - where: { - OR: remoteJidsWithoutPlus.map((remoteJid) => ({ jidOptions: { contains: remoteJid } })), - updatedAt: { - gte: dayjs().subtract(configService.get('DATABASE').SAVE_DATA.IS_ON_WHATSAPP_DAYS, 'days').toDate(), - }, - }, - }); - - results = onWhatsappCache.map((item) => ({ - remoteJid: item.remoteJid, - number: item.remoteJid.split('@')[0], - jidOptions: item.jidOptions.split(','), - lid: item.lid, - })); + if (!configService.get('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) { + return results; + } + + const cutoff = dayjs() + .subtract(configService.get('DATABASE').SAVE_DATA.IS_ON_WHATSAPP_DAYS, 'days') + .toDate(); + const variants = [...new Set(remoteJids.flatMap((remoteJid) => getAvailableNumbers(remoteJid)))]; + + const hits = await prismaRepository.isOnWhatsappJid.findMany({ + where: { jid: { in: variants }, updatedAt: { gte: cutoff } }, + }); + const canonicals = [...new Set(hits.map((row) => row.remoteJid))]; + if (canonicals.length === 0) { + return results; } + const rows = await prismaRepository.isOnWhatsappJid.findMany({ + where: { remoteJid: { in: canonicals }, updatedAt: { gte: cutoff } }, + }); + + const groups = new Map; lid?: string }>(); + for (const row of rows) { + const group = groups.get(row.remoteJid) ?? { jids: new Set() }; + group.jids.add(row.jid); + group.lid = group.lid ?? row.lid ?? undefined; + groups.set(row.remoteJid, group); + } + + groups.forEach((group, remoteJid) => + results.push({ + remoteJid, + number: remoteJid.split('@')[0], + jidOptions: [...group.jids].sort(), + lid: group.lid, + }), + ); + return results; } diff --git a/test/utils/onWhatsappCache.test.ts b/test/utils/onWhatsappCache.test.ts index 8cf75ffccb..51cd7e25d6 100644 --- a/test/utils/onWhatsappCache.test.ts +++ b/test/utils/onWhatsappCache.test.ts @@ -1,33 +1,94 @@ import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +type Row = { jid: string; remoteJid: string; lid: string | null; updatedAt: Date }; + const mocks = vi.hoisted(() => ({ database: { SAVE_DATA: { IS_ON_WHATSAPP: true, IS_ON_WHATSAPP_DAYS: 7 } }, - isOnWhatsapp: { findFirst: vi.fn(), findMany: vi.fn(), create: vi.fn(), update: vi.fn() }, + rows: new Map(), + isOnWhatsappJid: { findMany: vi.fn(), deleteMany: vi.fn(), createMany: vi.fn() }, + transaction: vi.fn(), + logger: { verbose: vi.fn(), warn: vi.fn(), error: vi.fn() }, })); -vi.mock('@api/server.module', () => ({ prismaRepository: { isOnWhatsapp: mocks.isOnWhatsapp } })); +vi.mock('@api/server.module', () => ({ + prismaRepository: { isOnWhatsappJid: mocks.isOnWhatsappJid, $transaction: mocks.transaction }, +})); vi.mock('@config/env.config', () => ({ configService: { get: vi.fn(() => mocks.database) } })); vi.mock('@config/logger.config', () => ({ Logger: class { - verbose = vi.fn(); - warn = vi.fn(); - error = vi.fn(); + verbose = mocks.logger.verbose; + warn = mocks.logger.warn; + error = mocks.logger.error; }, })); -const { isOnWhatsapp } = mocks; +const { rows, isOnWhatsappJid, transaction, logger } = mocks; + +const uniqueViolation = () => Object.assign(new Error('Unique constraint failed'), { code: 'P2002' }); + +const matches = (row: Row, where: any) => + (!where.jid || where.jid.in.includes(row.jid)) && + (!where.remoteJid || where.remoteJid.in.includes(row.remoteJid)) && + (!where.updatedAt || row.updatedAt >= where.updatedAt.gte); -const queriedJids = () => - isOnWhatsapp.findMany.mock.calls[0][0].where.OR.map((clause: any) => clause.jidOptions.contains); +const lazy = (run: () => Promise) => ({ + run, + then: (resolve: (value: T) => unknown, reject: (reason: unknown) => unknown) => run().then(resolve, reject), +}); + +const installFake = () => { + isOnWhatsappJid.findMany.mockImplementation(async ({ where }) => + [...rows.values()].filter((row) => matches(row, where)).map((row) => ({ ...row })), + ); + isOnWhatsappJid.deleteMany.mockImplementation(({ where }) => + lazy(async () => { + const targets = [...rows.values()].filter((row) => matches(row, where)); + targets.forEach((row) => rows.delete(row.jid)); + return { count: targets.length }; + }), + ); + isOnWhatsappJid.createMany.mockImplementation(({ data }) => + lazy(async () => { + for (const row of data) { + if (rows.has(row.jid)) throw uniqueViolation(); + } + for (const row of data) { + rows.set(row.jid, { lid: null, updatedAt: new Date(), ...row }); + } + return { count: data.length }; + }), + ); + transaction.mockImplementation(async (operations: ReturnType[]) => { + const results = []; + for (const operation of operations) results.push(await operation.run()); + return results; + }); +}; + +const seed = (remoteJid: string, jids: string[], lid: string | null = null, updatedAt = new Date()) => + jids.forEach((jid) => rows.set(jid, { jid, remoteJid, lid, updatedAt })); + +const group = (remoteJid: string) => + [...rows.values()] + .filter((row) => row.remoteJid === remoteJid) + .map((row) => row.jid) + .sort(); + +const queriedJids = () => isOnWhatsappJid.findMany.mock.calls[0][0].where.jid.in; + +beforeEach(() => { + vi.clearAllMocks(); + rows.clear(); + mocks.database.SAVE_DATA.IS_ON_WHATSAPP = true; + mocks.database.SAVE_DATA.IS_ON_WHATSAPP_DAYS = 7; + installFake(); +}); describe('getOnWhatsappCache', () => { beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-01-15T12:00:00.000Z')); - mocks.database.SAVE_DATA.IS_ON_WHATSAPP = true; - mocks.database.SAVE_DATA.IS_ON_WHATSAPP_DAYS = 7; - isOnWhatsapp.findMany.mockResolvedValue([]); }); afterEach(() => { @@ -38,364 +99,249 @@ describe('getOnWhatsappCache', () => { mocks.database.SAVE_DATA.IS_ON_WHATSAPP = false; expect(await getOnWhatsappCache(['5511987654321@s.whatsapp.net'])).toEqual([]); - expect(isOnWhatsapp.findMany).not.toHaveBeenCalled(); + expect(isOnWhatsappJid.findMany).not.toHaveBeenCalled(); }); - it('only returns records updated within the configured number of days', async () => { - await getOnWhatsappCache(['15551234567@s.whatsapp.net']); + it('only returns rows updated within the configured number of days', async () => { + seed('15551234567@s.whatsapp.net', ['15551234567@s.whatsapp.net'], null, new Date('2026-01-01T00:00:00.000Z')); - expect(isOnWhatsapp.findMany.mock.calls[0][0].where.updatedAt.gte).toEqual(new Date('2026-01-08T12:00:00.000Z')); + expect(await getOnWhatsappCache(['15551234567@s.whatsapp.net'])).toEqual([]); + expect(isOnWhatsappJid.findMany.mock.calls[0][0].where.updatedAt.gte).toEqual(new Date('2026-01-08T12:00:00.000Z')); }); - it('maps cache hits to remote jid, number, jid options and lid', async () => { - isOnWhatsapp.findMany.mockResolvedValue([ - { remoteJid: '5511987654321@s.whatsapp.net', jidOptions: 'a@s.whatsapp.net,b@lid', lid: 'lid' }, - ]); + it('returns the whole group with sorted jid options and lid using two queries', async () => { + seed('5491123456789@s.whatsapp.net', ['5491123456789@s.whatsapp.net', '541123456789@s.whatsapp.net']); + seed('5491123456789@s.whatsapp.net', ['999@lid'], 'lid'); - expect(await getOnWhatsappCache(['5511987654321@s.whatsapp.net'])).toEqual([ + expect(await getOnWhatsappCache(['541123456789@s.whatsapp.net'])).toEqual([ { - remoteJid: '5511987654321@s.whatsapp.net', - number: '5511987654321', - jidOptions: ['a@s.whatsapp.net', 'b@lid'], + remoteJid: '5491123456789@s.whatsapp.net', + number: '5491123456789', + jidOptions: ['541123456789@s.whatsapp.net', '5491123456789@s.whatsapp.net', '999@lid'], lid: 'lid', }, ]); + expect(isOnWhatsappJid.findMany).toHaveBeenCalledTimes(2); + expect(isOnWhatsappJid.findMany.mock.calls[1][0].where.remoteJid.in).toEqual(['5491123456789@s.whatsapp.net']); }); - it('searches Brazilian numbers with and without the ninth digit', async () => { - await getOnWhatsappCache(['5511987654321@s.whatsapp.net', '551187654321@s.whatsapp.net']); + it('finds a group by its lid', async () => { + seed('15551234567@s.whatsapp.net', ['15551234567@s.whatsapp.net', '888@lid']); + + const [result] = await getOnWhatsappCache(['888@lid']); + + expect(result.remoteJid).toBe('15551234567@s.whatsapp.net'); + expect(result.jidOptions).toEqual(['15551234567@s.whatsapp.net', '888@lid']); + }); - expect(queriedJids()).toEqual([ + it('returns one entry per group when several inputs hit the same group', async () => { + seed('5511987654321@s.whatsapp.net', ['5511987654321@s.whatsapp.net', '551187654321@s.whatsapp.net']); + seed('15551234567@s.whatsapp.net', ['15551234567@s.whatsapp.net']); + + const results = await getOnWhatsappCache([ '5511987654321@s.whatsapp.net', '551187654321@s.whatsapp.net', + '15551234567@s.whatsapp.net', + ]); + + expect(results.map((result) => result.remoteJid).sort()).toEqual([ + '15551234567@s.whatsapp.net', '5511987654321@s.whatsapp.net', - '551187654321@s.whatsapp.net', ]); }); - it('searches Argentine numbers with and without the 9 prefix', async () => { - await getOnWhatsappCache(['5492616107398@s.whatsapp.net', '542616107398@s.whatsapp.net']); + it('queries Brazilian numbers with and without the ninth digit', async () => { + await getOnWhatsappCache(['5511987654321@s.whatsapp.net', '551187654321@s.whatsapp.net']); - expect(queriedJids()).toEqual([ - '5492616107398@s.whatsapp.net', - '542616107398@s.whatsapp.net', - '5492616107398@s.whatsapp.net', - '542616107398@s.whatsapp.net', - ]); + expect(queriedJids()).toEqual(['5511987654321@s.whatsapp.net', '551187654321@s.whatsapp.net']); }); - it('searches Mexican numbers with and without the 1 prefix', async () => { + it('queries Argentine numbers with and without the 9 prefix', async () => { + await getOnWhatsappCache(['542616107398@s.whatsapp.net']); + + expect(queriedJids()).toEqual(['5492616107398@s.whatsapp.net', '542616107398@s.whatsapp.net']); + }); + + it('queries Mexican numbers with and without the 1 prefix', async () => { await getOnWhatsappCache(['525512345678@s.whatsapp.net']); expect(queriedJids()).toEqual(['5215512345678@s.whatsapp.net', '525512345678@s.whatsapp.net']); }); - it('searches other numbers without the plus sign and with a single domain', async () => { + it('queries other numbers without the plus sign and with a single domain', async () => { await getOnWhatsappCache(['+15551234567@s.whatsapp.net']); expect(queriedJids()).toEqual(['15551234567@s.whatsapp.net']); }); - it('searches lid and group jids as given', async () => { + it('queries lid and group jids as given', async () => { await getOnWhatsappCache(['123456@lid', '5511999999999-1623281429@g.us']); expect(queriedJids()).toEqual(['123456@lid', '5511999999999-1623281429@g.us']); }); - it('searches numbers without a domain as user jids', async () => { + it('queries numbers without a domain as user jids', async () => { await getOnWhatsappCache(['15551234567']); expect(queriedJids()).toEqual(['15551234567@s.whatsapp.net']); }); -}); -describe('saveOnWhatsappCache', () => { - beforeEach(() => { - mocks.database.SAVE_DATA.IS_ON_WHATSAPP = true; - isOnWhatsapp.findFirst.mockResolvedValue(null); + it('skips the second query when nothing matches', async () => { + expect(await getOnWhatsappCache(['15551234567@s.whatsapp.net'])).toEqual([]); + expect(isOnWhatsappJid.findMany).toHaveBeenCalledTimes(1); }); +}); +describe('saveOnWhatsappCache', () => { it('does nothing when the cache is disabled', async () => { mocks.database.SAVE_DATA.IS_ON_WHATSAPP = false; await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); - expect(isOnWhatsapp.findFirst).not.toHaveBeenCalled(); + expect(isOnWhatsappJid.findMany).not.toHaveBeenCalled(); + expect(transaction).not.toHaveBeenCalled(); }); it('skips items without a remote jid', async () => { await saveOnWhatsappCache([{ remoteJid: '' }]); - expect(isOnWhatsapp.findFirst).not.toHaveBeenCalled(); + expect(transaction).not.toHaveBeenCalled(); + expect(logger.warn).toHaveBeenCalledWith('[saveOnWhatsappCache] Item skipped, missing remoteJid.'); }); - it('creates a record with sorted jid options when none exists', async () => { + it('creates one row per variant pointing to the canonical remote jid', async () => { await saveOnWhatsappCache([{ remoteJid: '+5511987654321@s.whatsapp.net' }]); - expect(isOnWhatsapp.create).toHaveBeenCalledWith({ - data: { - remoteJid: '5511987654321@s.whatsapp.net', - jidOptions: '551187654321@s.whatsapp.net,5511987654321@s.whatsapp.net', - lid: null, - }, - }); - }); - - it('looks up existing records by jid options and remote jid', async () => { - await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); - - expect(isOnWhatsapp.findFirst).toHaveBeenCalledWith({ - where: { - OR: [{ jidOptions: { contains: '15551234567@s.whatsapp.net' } }, { remoteJid: '15551234567@s.whatsapp.net' }], - }, - }); + expect(group('5511987654321@s.whatsapp.net')).toEqual([ + '551187654321@s.whatsapp.net', + '5511987654321@s.whatsapp.net', + ]); + expect(rows.get('551187654321@s.whatsapp.net')?.lid).toBeNull(); }); - it('adds a lid alternate jid and marks the record as lid', async () => { + it('adds a lid alternate jid and marks the group as lid', async () => { await saveOnWhatsappCache([{ remoteJid: '5511987654321@s.whatsapp.net', remoteJidAlt: '999@lid', lid: 'lid' }]); - expect(isOnWhatsapp.create).toHaveBeenCalledWith({ - data: { - remoteJid: '5511987654321@s.whatsapp.net', - jidOptions: '551187654321@s.whatsapp.net,5511987654321@s.whatsapp.net,999@lid', - lid: 'lid', - }, - }); + expect(group('5511987654321@s.whatsapp.net')).toEqual([ + '551187654321@s.whatsapp.net', + '5511987654321@s.whatsapp.net', + '999@lid', + ]); + expect([...rows.values()].every((row) => row.lid === 'lid')).toBe(true); }); it('ignores alternate jids that are not lids', async () => { await saveOnWhatsappCache([{ remoteJid: '999@lid', remoteJidAlt: '15550000000@s.whatsapp.net' }]); - expect(isOnWhatsapp.create.mock.calls[0][0].data.jidOptions).toBe('999@lid'); - }); - - it('marks lid remote jids as lid', async () => { - await saveOnWhatsappCache([{ remoteJid: '999@lid' }]); - - expect(isOnWhatsapp.create).toHaveBeenCalledWith({ - data: { remoteJid: '999@lid', jidOptions: '999@lid', lid: 'lid' }, - }); - }); - - it('skips the update when the existing record is identical', async () => { - isOnWhatsapp.findFirst.mockResolvedValue({ - id: 'r1', - remoteJid: '5511987654321@s.whatsapp.net', - jidOptions: '5511987654321@s.whatsapp.net,551187654321@s.whatsapp.net', - lid: null, - }); - - await saveOnWhatsappCache([{ remoteJid: '5511987654321@s.whatsapp.net' }]); - - expect(isOnWhatsapp.update).not.toHaveBeenCalled(); - expect(isOnWhatsapp.create).not.toHaveBeenCalled(); - }); - - it('updates when only the lid flag changes', async () => { - isOnWhatsapp.findFirst.mockResolvedValue({ - id: 'r1', - remoteJid: '5511987654321@s.whatsapp.net', - jidOptions: '5511987654321@s.whatsapp.net,551187654321@s.whatsapp.net', - lid: null, - }); - - await saveOnWhatsappCache([{ remoteJid: '5511987654321@s.whatsapp.net', lid: 'lid' }]); - - expect(isOnWhatsapp.update).toHaveBeenCalledWith({ - where: { id: 'r1' }, - data: { - remoteJid: '5511987654321@s.whatsapp.net', - jidOptions: '551187654321@s.whatsapp.net,5511987654321@s.whatsapp.net', - lid: 'lid', - }, - }); + expect([...rows.keys()]).toEqual(['999@lid']); + expect(rows.get('999@lid')).toMatchObject({ remoteJid: '999@lid', lid: 'lid' }); }); - it('updates when only the remote jid changes', async () => { - isOnWhatsapp.findFirst.mockResolvedValue({ - id: 'r1', - remoteJid: '551187654321@s.whatsapp.net', - jidOptions: '5511987654321@s.whatsapp.net,551187654321@s.whatsapp.net', - lid: null, - }); + it('merges an existing group and makes the item remote jid canonical', async () => { + seed('551187654321@s.whatsapp.net', ['551187654321@s.whatsapp.net', '888@lid'], 'lid', new Date('2020-01-01')); await saveOnWhatsappCache([{ remoteJid: '5511987654321@s.whatsapp.net' }]); - expect(isOnWhatsapp.update).toHaveBeenCalledWith({ - where: { id: 'r1' }, - data: { - remoteJid: '5511987654321@s.whatsapp.net', - jidOptions: '551187654321@s.whatsapp.net,5511987654321@s.whatsapp.net', - lid: null, - }, - }); - }); - - it('updates an existing record without jid options with the expanded options', async () => { - isOnWhatsapp.findFirst.mockResolvedValue({ - id: 'r1', - remoteJid: '5511987654321@s.whatsapp.net', - jidOptions: null, - lid: null, - }); - - await saveOnWhatsappCache([{ remoteJid: '5511987654321@s.whatsapp.net' }]); - - expect(isOnWhatsapp.update).toHaveBeenCalledWith({ - where: { id: 'r1' }, - data: { - remoteJid: '5511987654321@s.whatsapp.net', - jidOptions: '551187654321@s.whatsapp.net,5511987654321@s.whatsapp.net', - lid: null, - }, - }); - }); - - it('merges existing jid options into an update when data differs', async () => { - isOnWhatsapp.findFirst.mockResolvedValue({ - id: 'r1', - remoteJid: '15551234567@s.whatsapp.net', - jidOptions: '888@lid', - lid: null, - }); - - await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); - - expect(isOnWhatsapp.update).toHaveBeenCalledWith({ - where: { id: 'r1' }, - data: { - remoteJid: '15551234567@s.whatsapp.net', - jidOptions: '15551234567@s.whatsapp.net,888@lid', - lid: null, - }, - }); - }); - - it('keeps processing other items when one fails', async () => { - isOnWhatsapp.findFirst.mockRejectedValueOnce(new Error('db down')).mockResolvedValueOnce(null); - - await expect( - saveOnWhatsappCache([{ remoteJid: '111@s.whatsapp.net' }, { remoteJid: '222@s.whatsapp.net' }]), - ).resolves.toBeUndefined(); - expect(isOnWhatsapp.create).toHaveBeenCalledTimes(1); - }); -}); - -describe('saveOnWhatsappCache with concurrent writers', () => { - const rows: { id: string; remoteJid: string; jidOptions: string; lid: string | null }[] = []; - const tick = () => new Promise((resolve) => setTimeout(resolve, 0)); - - const uniqueViolation = () => - Object.assign(new Error('Unique constraint failed on the fields: (`remoteJid`)'), { code: 'P2002' }); - - beforeEach(() => { - rows.length = 0; - mocks.database.SAVE_DATA.IS_ON_WHATSAPP = true; - isOnWhatsapp.findFirst.mockImplementation(async ({ where }) => { - await tick(); - return ( - rows.find((row) => - where.OR.some((clause: any) => - clause.remoteJid ? row.remoteJid === clause.remoteJid : row.jidOptions.includes(clause.jidOptions.contains), - ), - ) ?? null - ); - }); - isOnWhatsapp.create.mockImplementation(async ({ data }) => { - await tick(); - if (rows.some((row) => row.remoteJid === data.remoteJid)) throw uniqueViolation(); - const row = { id: `row-${rows.length}`, ...data }; - rows.push(row); - return row; - }); - isOnWhatsapp.update.mockImplementation(async ({ where, data }) => { - await tick(); - const row = rows.find((candidate) => candidate.id === where.id); - Object.assign(row, data); - return row; - }); + expect(group('5511987654321@s.whatsapp.net')).toEqual([ + '551187654321@s.whatsapp.net', + '5511987654321@s.whatsapp.net', + '888@lid', + ]); + expect(group('551187654321@s.whatsapp.net')).toEqual([]); + expect([...rows.values()].every((row) => row.lid === 'lid' && row.updatedAt > new Date('2020-01-01'))).toBe(true); }); - it('saves a jid repeated in one batch as a single record without errors', async () => { + it('saves a jid repeated in one batch as a single group with both alternates', async () => { await saveOnWhatsappCache([ - { remoteJid: '5491123456789@s.whatsapp.net' }, + { remoteJid: '5491123456789@s.whatsapp.net', remoteJidAlt: '111@lid' }, { remoteJid: '5491123456789@s.whatsapp.net', remoteJidAlt: '999@lid' }, ]); - expect(rows).toHaveLength(1); - expect(rows[0].jidOptions.split(',')).toContain('999@lid'); - }); - - it('saves the same jid from concurrent calls as a single record', async () => { - await Promise.all([ - saveOnWhatsappCache([{ remoteJid: '5491123456789@s.whatsapp.net' }]), - saveOnWhatsappCache([{ remoteJid: '5491123456789@s.whatsapp.net', remoteJidAlt: '999@lid' }]), + expect(group('5491123456789@s.whatsapp.net')).toEqual([ + '111@lid', + '541123456789@s.whatsapp.net', + '5491123456789@s.whatsapp.net', + '999@lid', ]); - - expect(rows).toHaveLength(1); - expect(rows[0].jidOptions.split(',')).toContain('999@lid'); + expect(rows.size).toBe(4); }); - it('does not retry a create that fails for another reason', async () => { - isOnWhatsapp.create.mockRejectedValueOnce(new Error('database unavailable')); + it('saves 2,500 items with a bounded number of queries', async () => { + const items = Array.from({ length: 2500 }, (_, index) => ({ remoteJid: `1555${index}@s.whatsapp.net` })); - await expect(saveOnWhatsappCache([{ remoteJid: '5491123456789@s.whatsapp.net' }])).resolves.toBeUndefined(); + await saveOnWhatsappCache(items); - expect(isOnWhatsapp.create).toHaveBeenCalledTimes(1); - expect(rows).toHaveLength(0); + expect(rows.size).toBe(2500); + expect(isOnWhatsappJid.findMany.mock.calls.length).toBeLessThanOrEqual(10); + expect(transaction).toHaveBeenCalledTimes(5); + expect(isOnWhatsappJid.createMany).toHaveBeenCalledTimes(5); }); - it('keeps saving other jids when the retry also fails', async () => { - const conflictingJid = '5491123456789@s.whatsapp.net'; - const createRow = isOnWhatsapp.create.getMockImplementation(); - isOnWhatsapp.create.mockImplementation(async (args) => { - if (args.data.remoteJid === conflictingJid) throw uniqueViolation(); - return createRow(args); - }); + it('reads whole existing groups with two queries per chunk', async () => { + seed('15551234567@s.whatsapp.net', ['15551234567@s.whatsapp.net', '888@lid']); + const items = Array.from({ length: 1000 }, (_, index) => ({ remoteJid: `1555${index}@s.whatsapp.net` })); + items[700] = { remoteJid: '888@lid' }; - await expect( - saveOnWhatsappCache([{ remoteJid: conflictingJid }, { remoteJid: '5491187654321@s.whatsapp.net' }]), - ).resolves.toBeUndefined(); + await saveOnWhatsappCache(items); - expect(isOnWhatsapp.create.mock.calls.filter(([args]) => args.data.remoteJid === conflictingJid)).toHaveLength(2); - expect(rows.map((row) => row.remoteJid)).toEqual(['5491187654321@s.whatsapp.net']); + expect(isOnWhatsappJid.findMany).toHaveBeenCalledTimes(3); + expect(transaction).toHaveBeenCalledTimes(2); + expect(group('888@lid')).toEqual(['15551234567@s.whatsapp.net', '888@lid']); }); -}); -describe('saveOnWhatsappCache concurrency', () => { - beforeEach(() => { - mocks.database.SAVE_DATA.IS_ON_WHATSAPP = true; + it('retries a chunk once when a concurrent writer causes a unique violation', async () => { + transaction.mockRejectedValueOnce(uniqueViolation()); + + await saveOnWhatsappCache([{ remoteJid: '15551234567@s.whatsapp.net' }]); + + expect(transaction).toHaveBeenCalledTimes(2); + expect(group('15551234567@s.whatsapp.net')).toEqual(['15551234567@s.whatsapp.net']); + expect(logger.error).not.toHaveBeenCalled(); }); - it('never runs more than ten items against the database at once', async () => { - let inFlight = 0; - let peak = 0; - isOnWhatsapp.findFirst.mockImplementation(async () => { - inFlight += 1; - peak = Math.max(peak, inFlight); - await new Promise((resolve) => setTimeout(resolve, 1)); - inFlight -= 1; - return null; - }); - isOnWhatsapp.create.mockResolvedValue({}); - const items = Array.from({ length: 100 }, (_, index) => ({ remoteJid: `1555000${index}@s.whatsapp.net` })); + it('logs other errors and keeps processing the next chunk', async () => { + transaction.mockRejectedValueOnce(new Error('database unavailable')); + const items = Array.from({ length: 600 }, (_, index) => ({ remoteJid: `1555${index}@s.whatsapp.net` })); - await saveOnWhatsappCache(items); + await expect(saveOnWhatsappCache(items)).resolves.toBeUndefined(); - expect(isOnWhatsapp.create).toHaveBeenCalledTimes(100); - expect(peak).toBeGreaterThan(1); - expect(peak).toBeLessThanOrEqual(10); + expect(transaction).toHaveBeenCalledTimes(2); + expect(rows.size).toBe(100); + expect(logger.error).toHaveBeenCalled(); }); it('keeps saving the rest of the batch when it contains empty items', async () => { - isOnWhatsapp.findFirst.mockResolvedValue(null); - isOnWhatsapp.create.mockResolvedValue({}); const items = Array.from({ length: 30 }, (_, index) => index < 10 ? undefined : { remoteJid: `1555000${index}@s.whatsapp.net` }, ); await saveOnWhatsappCache(items as any); - expect(isOnWhatsapp.create).toHaveBeenCalledTimes(20); + expect(rows.size).toBe(20); + }); + + it('keeps every alternate jid when concurrent saves touch the same contact', async () => { + const tick = () => new Promise((resolve) => setTimeout(resolve, 0)); + transaction.mockImplementation(async (operations: ReturnType[]) => { + const results = []; + for (const operation of operations) { + await tick(); + results.push(await operation.run()); + } + return results; + }); + const contact = '5491123456789@s.whatsapp.net'; + + await Promise.all( + ['111@lid', '222@lid', '333@lid'].map((alt) => saveOnWhatsappCache([{ remoteJid: contact, remoteJidAlt: alt }])), + ); + + expect(logger.error).not.toHaveBeenCalled(); + expect( + [...rows.values()] + .filter((row) => row.jid.endsWith('@lid')) + .map((row) => `${row.jid}>${row.remoteJid}`) + .sort(), + ).toEqual([`111@lid>${contact}`, `222@lid>${contact}`, `333@lid>${contact}`]); }); }); From 51cfa32687cca0cc986f4217533c30ce320a1080 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 16:41:15 -0300 Subject: [PATCH 58/96] docs(changelog): release v0.1.2 --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f99efdf84..cbd5895907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # evolution-in-purple +## 0.1.2 (2026-10-23) + +### Features + +* **onWhatsapp cache storage**: the cache moves to a new `IsOnWhatsappJid` table with one row per jid and an indexed canonical `remoteJid`. Lookups are two indexed queries and a 2,500-contact sync takes about 30 queries instead of ~5,000; writes are serialized per process. The migration is additive (the old `IsOnWhatsapp` table is kept, so an upstream image still runs) and backfills Postgres from `jidOptions` (#22) + +### Fixes + +* **Database pool exhaustion**: contact syncs no longer open thousands of concurrent onWhatsapp cache queries (#16) +* **Status updates without `remoteJid`**: `messages.update` takes the jid from the stored message, so the webhook carries it and `messageUpdate.create` no longer fails and aborts the batch (#17) +* **Group number changes**: plain-jid `messageStubParameters` no longer throw in `processMessage`, so `group-participants.update` is emitted (#18; Baileys patch) +* **Link previews without an image**: pages without an image get a title and description preview instead of none (#19; Baileys patch) +* **Telemetry**: disabled unless `TELEMETRY_ENABLED=true`; it sent every request path, including instance names, to `log.evolution-api.com` (#21, by @nogara) + +### Chores + +* Docker base image pinned to the official `node:24.22.1-alpine3.24` by digest (#20, by @nogara) + +### Upgrade notes + +* The image applies the new migration on start (`Docker/scripts/deploy_database.sh`); it only adds a table and backfills it. +* Set `TELEMETRY_ENABLED=false` where it is set to `true` explicitly in the environment, since #21 only changes the default. + ## 0.1.1 (2026-10-22) ### Fixes From 55ebe72c41038c6c76379d10c7187b6bc0b67a8e Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Nogara Date: Fri, 23 Oct 2026 17:24:31 +0000 Subject: [PATCH 59/96] ci: smoke-test the Docker image against Postgres Imported verbatim from nogara/evolution-api 4e797413. --- .github/workflows/image-smoke-test.yml | 90 ++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .github/workflows/image-smoke-test.yml diff --git a/.github/workflows/image-smoke-test.yml b/.github/workflows/image-smoke-test.yml new file mode 100644 index 0000000000..58aae0655a --- /dev/null +++ b/.github/workflows/image-smoke-test.yml @@ -0,0 +1,90 @@ +name: Image smoke test + +on: + pull_request: + push: + branches: [nogara/stable] + workflow_dispatch: + +permissions: + contents: read + +jobs: + smoke: + runs-on: ubuntu-latest + timeout-minutes: 30 + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: evolution + POSTGRES_PASSWORD: evolution + POSTGRES_DB: evolution + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U evolution" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build image (amd64) + uses: docker/build-push-action@v6 + with: + context: . + load: true + tags: evolution-api:smoke + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Node.js in the image matches the Dockerfile pin + run: | + expected="v$(grep -m1 -oE '^FROM node:[0-9]+\.[0-9]+\.[0-9]+' Dockerfile | sed 's/^FROM node://')" + actual="$(docker run --rm --entrypoint node evolution-api:smoke -v)" + echo "expected=$expected actual=$actual" + test "$expected" = "$actual" + + - name: Start the container + run: | + docker run -d --name evolution --network host \ + -e SERVER_PORT=8080 \ + -e DATABASE_PROVIDER=postgresql \ + -e DATABASE_CONNECTION_URI='postgresql://evolution:evolution@localhost:5432/evolution?schema=evolution_api' \ + -e CACHE_REDIS_ENABLED=false \ + -e CACHE_LOCAL_ENABLED=true \ + -e AUTHENTICATION_API_KEY=smoke-test-key \ + -e TELEMETRY_ENABLED=false \ + evolution-api:smoke + + - name: Wait for HTTP 200 on / + run: | + for i in $(seq 1 60); do + if curl -fsS http://localhost:8080/ -o body.json; then + cat body.json + grep -q '"status":200' body.json + exit 0 + fi + if [ "$(docker inspect -f '{{.State.Running}}' evolution)" != "true" ]; then + echo "container exited" + exit 1 + fi + sleep 5 + done + echo "timed out waiting for the API" + exit 1 + + - name: Authenticated route answers + run: | + curl -fsS -H 'apikey: smoke-test-key' http://localhost:8080/instance/fetchInstances + + - name: Container logs + if: always() + run: docker logs evolution 2>&1 | tail -200 From 4157b0bfd33dbbaa1614abc47e4a1bdcb9eec7c2 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 19:39:17 -0300 Subject: [PATCH 60/96] ci: run the image smoke test on main, check migrations and pins, bound requests Adapts the workflow imported from nogara/evolution-api to this repository. --- .github/workflows/image-smoke-test.yml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/image-smoke-test.yml b/.github/workflows/image-smoke-test.yml index 58aae0655a..68f7d460ab 100644 --- a/.github/workflows/image-smoke-test.yml +++ b/.github/workflows/image-smoke-test.yml @@ -3,7 +3,7 @@ name: Image smoke test on: pull_request: push: - branches: [nogara/stable] + branches: [main] workflow_dispatch: permissions: @@ -47,7 +47,9 @@ jobs: - name: Node.js in the image matches the Dockerfile pin run: | - expected="v$(grep -m1 -oE '^FROM node:[0-9]+\.[0-9]+\.[0-9]+' Dockerfile | sed 's/^FROM node://')" + pins="$(grep -oE '^FROM node:[0-9]+\.[0-9]+\.[0-9]+' Dockerfile | sed 's/^FROM node://' | sort -u)" + test "$(echo "$pins" | wc -l)" -eq 1 + expected="v$pins" actual="$(docker run --rm --entrypoint node evolution-api:smoke -v)" echo "expected=$expected actual=$actual" test "$expected" = "$actual" @@ -66,8 +68,8 @@ jobs: - name: Wait for HTTP 200 on / run: | - for i in $(seq 1 60); do - if curl -fsS http://localhost:8080/ -o body.json; then + for _ in $(seq 1 60); do + if curl -fsS --max-time 5 http://localhost:8080/ -o body.json; then cat body.json grep -q '"status":200' body.json exit 0 @@ -83,7 +85,18 @@ jobs: - name: Authenticated route answers run: | - curl -fsS -H 'apikey: smoke-test-key' http://localhost:8080/instance/fetchInstances + curl -fsS --max-time 10 -H 'apikey: smoke-test-key' http://localhost:8080/instance/fetchInstances + + - name: Migrations ran on start + env: + PGPASSWORD: evolution + run: | + expected=$(find prisma/postgresql-migrations -mindepth 1 -maxdepth 1 -type d | wc -l) + applied=$(psql -h localhost -U evolution -d evolution -At -v ON_ERROR_STOP=1 \ + -c 'select count(*) from evolution_api."_prisma_migrations" where finished_at is not null and rolled_back_at is null') + echo "expected=$expected applied=$applied" + test "$expected" -eq "$applied" + psql -h localhost -U evolution -d evolution -v ON_ERROR_STOP=1 -c 'select count(*) from evolution_api."IsOnWhatsappJid"' - name: Container logs if: always() From 72644798c93c3e79241c141742287a408c62a39e Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Nogara Date: Fri, 23 Oct 2026 17:24:31 +0000 Subject: [PATCH 61/96] ci: scan the published image daily with Trivy Imported verbatim from nogara/evolution-api 4e797413. --- .github/workflows/image-scan.yml | 52 ++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/image-scan.yml diff --git a/.github/workflows/image-scan.yml b/.github/workflows/image-scan.yml new file mode 100644 index 0000000000..6270204fc7 --- /dev/null +++ b/.github/workflows/image-scan.yml @@ -0,0 +1,52 @@ +name: Image vulnerability scan + +on: + schedule: + - cron: '0 9 * * *' # Daily; new CVEs land in Trivy's DB without any image change + workflow_dispatch: + +permissions: + contents: read + packages: read + security-events: write + +jobs: + scan: + if: github.repository_owner == 'nogara' + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + IMAGE: ghcr.io/${{ github.repository_owner }}/evolution-api:stable + steps: + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Scan published image + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: ${{ env.IMAGE }} + format: sarif + output: trivy.sarif + severity: CRITICAL,HIGH + ignore-unfixed: true + limit-severities-for-sarif: true + + - name: Upload to code scanning + uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 + with: + sarif_file: trivy.sarif + category: image-stable + + - name: Fail on fixable critical or high findings + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: ${{ env.IMAGE }} + format: table + severity: CRITICAL,HIGH + ignore-unfixed: true + exit-code: '1' + skip-setup-trivy: true From a691515db6eb3b0a3f15f32c7766255f41bf83fe Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 19:39:19 -0300 Subject: [PATCH 62/96] ci: scan our latest final release in report-only mode Adapts the workflow imported from nogara/evolution-api to this repository. --- .github/workflows/image-scan.yml | 40 +++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/.github/workflows/image-scan.yml b/.github/workflows/image-scan.yml index 6270204fc7..ff0ccb2a18 100644 --- a/.github/workflows/image-scan.yml +++ b/.github/workflows/image-scan.yml @@ -2,7 +2,7 @@ name: Image vulnerability scan on: schedule: - - cron: '0 9 * * *' # Daily; new CVEs land in Trivy's DB without any image change + - cron: '0 9 * * *' workflow_dispatch: permissions: @@ -12,12 +12,22 @@ permissions: jobs: scan: - if: github.repository_owner == 'nogara' + if: github.repository_owner == 'purple-dice' runs-on: ubuntu-latest timeout-minutes: 20 - env: - IMAGE: ghcr.io/${{ github.repository_owner }}/evolution-api:stable steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Resolve the latest released image + id: image + run: | + tag="$(git tag -l 'v*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)" + test -n "$tag" + echo "ref=ghcr.io/${GITHUB_REPOSITORY}:${tag#v}" >> "$GITHUB_OUTPUT" + - name: Login to GHCR uses: docker/login-action@v3 with: @@ -28,7 +38,7 @@ jobs: - name: Scan published image uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: - image-ref: ${{ env.IMAGE }} + image-ref: ${{ steps.image.outputs.ref }} format: sarif output: trivy.sarif severity: CRITICAL,HIGH @@ -39,14 +49,26 @@ jobs: uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: sarif_file: trivy.sarif - category: image-stable + category: image-release - - name: Fail on fixable critical or high findings + - name: Summarize fixable critical and high findings uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: - image-ref: ${{ env.IMAGE }} + image-ref: ${{ steps.image.outputs.ref }} format: table + output: trivy.txt severity: CRITICAL,HIGH ignore-unfixed: true - exit-code: '1' + exit-code: '0' skip-setup-trivy: true + + - name: Write job summary + env: + IMAGE_REF: ${{ steps.image.outputs.ref }} + run: | + { + echo "### Fixable CRITICAL/HIGH findings in ${IMAGE_REF}" + echo '```' + cat trivy.txt + echo '```' + } >> "$GITHUB_STEP_SUMMARY" From 5c2703d8fa83ecc0ab94cb7474b1341c250306c3 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 20:11:50 -0300 Subject: [PATCH 63/96] build(docker): ship only production dependencies in the image The final image copied the builder's full node_modules, devDependencies included, and tsup (a build-only tool) sat in dependencies, keeping tsx, esbuild and rollup in production. tsup moves to devDependencies and the builder prunes devDependencies after building; the unused tsup.config.ts is no longer copied. The Baileys patch survives the prune, migrations and prisma generate still run on start. Trivy fixable CRITICAL/HIGH findings drop from 162 to 78 (critical 6 to 3) and the image from 367 to 347 MB, with no dependency version change. --- Dockerfile | 3 +- package-lock.json | 102 ++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 3 files changed, 97 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 08276a8e0f..da2a625d1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,6 +31,8 @@ RUN ./Docker/scripts/generate_database.sh RUN npm run build +RUN npm prune --omit=dev + FROM node:24.22.1-alpine3.24@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 AS final RUN apk update && \ @@ -52,7 +54,6 @@ COPY --from=builder /evolution/public ./public COPY --from=builder /evolution/.env ./.env COPY --from=builder /evolution/Docker ./Docker COPY --from=builder /evolution/runWithProvider.js ./runWithProvider.js -COPY --from=builder /evolution/tsup.config.ts ./tsup.config.ts ENV DOCKER_ENV=true diff --git a/package-lock.json b/package-lock.json index 89888f6e6b..e02ac53ed2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,6 @@ "socket.io-client": "^4.8.1", "socks-proxy-agent": "^8.0.5", "swagger-ui-express": "^5.0.1", - "tsup": "^8.3.5", "undici": "^7.16.0", "uuid": "^13.0.0" }, @@ -98,6 +97,7 @@ "lint-staged": "^17.6.0", "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", + "tsup": "^8.3.5", "tsx": "^4.20.5", "typescript": "^5.7.2", "vitest": "^3.2.7" @@ -1151,6 +1151,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1167,6 +1168,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1183,6 +1185,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1199,6 +1202,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1215,6 +1219,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1231,6 +1236,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1247,6 +1253,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1263,6 +1270,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1279,6 +1287,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1295,6 +1304,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1311,6 +1321,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1327,6 +1338,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1343,6 +1355,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1359,6 +1372,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1375,6 +1389,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1391,6 +1406,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1407,6 +1423,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1423,6 +1440,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1439,6 +1457,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1455,6 +1474,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1471,6 +1491,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1487,6 +1508,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1503,6 +1525,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1519,6 +1542,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1535,6 +1559,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1551,6 +1576,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2802,6 +2828,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2812,6 +2839,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2821,12 +2849,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3691,6 +3721,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3704,6 +3735,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3717,6 +3749,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3730,6 +3763,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3743,6 +3777,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3756,6 +3791,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3769,6 +3805,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3782,6 +3819,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3795,6 +3833,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3808,6 +3847,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3821,6 +3861,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3834,6 +3875,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3847,6 +3889,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3860,6 +3903,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3873,6 +3917,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3886,6 +3931,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3899,6 +3945,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3912,6 +3959,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3925,6 +3973,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3938,6 +3987,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3951,6 +4001,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3964,6 +4015,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4832,6 +4884,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/@types/express": { @@ -5649,6 +5702,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, "license": "MIT" }, "node_modules/append-field": { @@ -6176,6 +6230,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, "license": "MIT", "dependencies": { "load-tsconfig": "^0.2.3" @@ -6239,6 +6294,7 @@ "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7711,6 +7767,7 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -8853,6 +8910,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, "license": "MIT", "dependencies": { "magic-string": "^0.30.17", @@ -9039,6 +9097,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -9187,7 +9246,7 @@ "version": "4.13.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -10506,6 +10565,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -10764,6 +10824,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -10776,6 +10837,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/link-preview-js": { @@ -10832,6 +10894,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -11093,6 +11156,7 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -11453,6 +11517,7 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.15.0", @@ -11465,12 +11530,14 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, "license": "MIT" }, "node_modules/mlly/node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, "license": "MIT", "dependencies": { "confbox": "^0.1.8", @@ -11576,6 +11643,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -11587,7 +11655,7 @@ "version": "3.3.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -12645,6 +12713,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -12700,6 +12769,7 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -12759,7 +12829,7 @@ "version": "8.5.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", - "devOptional": true, + "dev": true, "funding": [ { "type": "opencollective", @@ -12788,6 +12858,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, "funding": [ { "type": "opencollective", @@ -13539,6 +13610,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13548,7 +13620,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" @@ -13600,6 +13672,7 @@ "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -14303,6 +14376,7 @@ "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">= 12" @@ -14312,7 +14386,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -14599,6 +14673,7 @@ "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -14621,6 +14696,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -14715,6 +14791,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -14724,6 +14801,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -14783,6 +14861,7 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -14799,6 +14878,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -14816,6 +14896,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -14916,6 +14997,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, "license": "MIT", "bin": { "tree-kill": "cli.js" @@ -14938,6 +15020,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/tsconfig-paths": { @@ -14975,6 +15058,7 @@ "version": "8.5.1", "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, "license": "MIT", "dependencies": { "bundle-require": "^5.1.0", @@ -15027,13 +15111,14 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, "license": "MIT" }, "node_modules/tsx": { "version": "4.20.6", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.25.0", @@ -15495,7 +15580,7 @@ "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -15695,6 +15780,7 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, "license": "MIT" }, "node_modules/uint8array-extras": { diff --git a/package.json b/package.json index 3aa052f968..d203d79c2b 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,6 @@ "socket.io-client": "^4.8.1", "socks-proxy-agent": "^8.0.5", "swagger-ui-express": "^5.0.1", - "tsup": "^8.3.5", "undici": "^7.16.0", "uuid": "^13.0.0" }, @@ -159,6 +158,7 @@ "lint-staged": "^17.6.0", "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", + "tsup": "^8.3.5", "tsx": "^4.20.5", "typescript": "^5.7.2", "vitest": "^3.2.7" From 4bc940d85d60ecac93ac23ea64402a8bc1e037fa Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 20:27:15 -0300 Subject: [PATCH 64/96] build(deps): apply semver-compatible security updates npm audit fix (without --force) updates 195 production packages within their declared ranges, including express, body-parser, qs, path-to-regexp, proxy-addr, fast-xml-parser, undici, multer, compression, form-data, ws, engine.io, socket.io-parser, jws and axios 1.20. package.json is unchanged. axios 1.20 types header values as AxiosHeaderValue, so the three content-type reads go through headerAsString, keeping the previous behavior. Trivy fixable CRITICAL/HIGH findings drop from 78 to 21 and no critical remains. --- package-lock.json | 2881 +++++------------ .../whatsapp/whatsapp.baileys.service.ts | 3 +- .../chatwoot/services/chatwoot.service.ts | 6 +- src/utils/headerAsString.ts | 1 + test/utils/headerAsString.test.ts | 16 + 5 files changed, 778 insertions(+), 2129 deletions(-) create mode 100644 src/utils/headerAsString.ts create mode 100644 test/utils/headerAsString.test.ts diff --git a/package-lock.json b/package-lock.json index e02ac53ed2..01318b9277 100644 --- a/package-lock.json +++ b/package-lock.json @@ -109,689 +109,289 @@ "integrity": "sha512-yprSnAtj80/VKuDqRcFFLDYltoNV8tChNwFfIgcf6PGD4sjzWIBgs08pRuTqGH5mk5wgL6PBRSsMCZqtZwzFEw==", "license": "MIT" }, - "node_modules/@apm-js-collab/code-transformer": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.8.2.tgz", - "integrity": "sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA==", - "license": "Apache-2.0" - }, - "node_modules/@apm-js-collab/tracing-hooks": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.3.1.tgz", - "integrity": "sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw==", - "license": "Apache-2.0", - "dependencies": { - "@apm-js-collab/code-transformer": "^0.8.0", - "debug": "^4.4.1", - "module-details-from-path": "^1.0.4" - } - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-sqs": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.936.0.tgz", - "integrity": "sha512-JDYdeGV8L+zXr9Ce7gVeleQB2z0F3YWWmNvbE6cS20fR9E0XVIsYw5Grymjw9/3AG4wbOIQJ5Nayy+HgxKANjA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.936.0", - "@aws-sdk/credential-provider-node": "3.936.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.936.0", - "@aws-sdk/middleware-sdk-sqs": "3.936.0", - "@aws-sdk/middleware-user-agent": "3.936.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.936.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.5", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/md5-js": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.12", - "@smithy/middleware-retry": "^4.4.12", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.11", - "@smithy/util-defaults-mode-node": "^4.2.14", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.936.0.tgz", - "integrity": "sha512-0G73S2cDqYwJVvqL08eakj79MZG2QRaB56Ul8/Ps9oQxllr7DMI1IQ/N3j3xjxgpq/U36pkoFZ8aK1n7Sbr3IQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.936.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.936.0", - "@aws-sdk/middleware-user-agent": "3.936.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.936.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.5", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.12", - "@smithy/middleware-retry": "^4.4.12", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.11", - "@smithy/util-defaults-mode-node": "^4.2.14", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-utf8": "^4.2.0", + "version": "3.1156.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1156.0.tgz", + "integrity": "sha512-tkDeDv5kVb87RobSQ1RLfeCcLKA/IXwj0TkGWjUIfLSYQ+mL+hbimEZ/yOr46ahR+sJXRQN+JXffqrNQeiCLIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/credential-provider-node": "^3.972.88", + "@aws-sdk/middleware-sdk-sqs": "^3.972.46", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/core": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.936.0.tgz", - "integrity": "sha512-eGJ2ySUMvgtOziHhDRDLCrj473RJoL4J1vPjVM3NrKC/fF3/LoHjkut8AAnKmrW6a2uTzNKubigw8dEnpmpERw==", + "version": "3.978.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.5.tgz", + "integrity": "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws-sdk/xml-builder": "3.930.0", - "@smithy/core": "^3.18.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/types": "^3.975.0", + "@aws-sdk/xml-builder": "^3.972.41", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.36.1", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.936.0.tgz", - "integrity": "sha512-dKajFuaugEA5i9gCKzOaVy9uTeZcApE+7Z5wdcZ6j40523fY1a56khDAUYkCfwqa7sHci4ccmxBkAo+fW1RChA==", + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.76.tgz", + "integrity": "sha512-YxmL1Ar7bV9ZkJvtv+lj+MF6le76NnZnbmgySyZuxpZaCv0/1lZ3KxKy6RIj1h9ui2hWnVjZxbmTMeO0y/NrQA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.936.0.tgz", - "integrity": "sha512-5FguODLXG1tWx/x8fBxH+GVrk7Hey2LbXV5h9SFzYCx/2h50URBm0+9hndg0Rd23+xzYe14F6SI9HA9c1sPnjg==", + "version": "3.972.78", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.78.tgz", + "integrity": "sha512-3VHknDRngoebsaqyT1iR75b5cBT2zfEtODK6yYmYltljkK1HEAQLs2FEd1IpWqAeOYdMGaG9wikngWLTufY7RA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.936.0.tgz", - "integrity": "sha512-TbUv56ERQQujoHcLMcfL0Q6bVZfYF83gu/TjHkVkdSlHPOIKaG/mhE2XZSQzXv1cud6LlgeBbfzVAxJ+HPpffg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.936.0", - "@aws-sdk/credential-provider-env": "3.936.0", - "@aws-sdk/credential-provider-http": "3.936.0", - "@aws-sdk/credential-provider-login": "3.936.0", - "@aws-sdk/credential-provider-process": "3.936.0", - "@aws-sdk/credential-provider-sso": "3.936.0", - "@aws-sdk/credential-provider-web-identity": "3.936.0", - "@aws-sdk/nested-clients": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "version": "3.973.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.21.tgz", + "integrity": "sha512-aLx9hbCX6mGAyv4ZZd8UagJ4RASHVJw6BFDSIyuKmLkqNL6H47EjBPK6qaJ4B5TDV6mc+vMa0SJFYyo4mkvVQg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/credential-provider-env": "^3.972.76", + "@aws-sdk/credential-provider-http": "^3.972.78", + "@aws-sdk/credential-provider-login": "^3.972.83", + "@aws-sdk/credential-provider-process": "^3.972.76", + "@aws-sdk/credential-provider-sso": "^3.973.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.82", + "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.936.0.tgz", - "integrity": "sha512-8DVrdRqPyUU66gfV7VZNToh56ZuO5D6agWrkLQE/xbLJOm2RbeRgh6buz7CqV8ipRd6m+zCl9mM4F3osQLZn8Q==", + "version": "3.972.83", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.83.tgz", + "integrity": "sha512-9bJU6FjRacF4FK/QHf8Dyxhur7B1grRj8C1Yh4IkpoFW2exQflPdCv0VrbemLW9ZctLNle+AE7TETs9KNvuLYg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.936.0", - "@aws-sdk/nested-clients": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.936.0.tgz", - "integrity": "sha512-rk/2PCtxX9xDsQW8p5Yjoca3StqmQcSfkmD7nQ61AqAHL1YgpSQWqHE+HjfGGiHDYKG7PvE33Ku2GyA7lEIJAw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.936.0", - "@aws-sdk/credential-provider-http": "3.936.0", - "@aws-sdk/credential-provider-ini": "3.936.0", - "@aws-sdk/credential-provider-process": "3.936.0", - "@aws-sdk/credential-provider-sso": "3.936.0", - "@aws-sdk/credential-provider-web-identity": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "version": "3.972.88", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.88.tgz", + "integrity": "sha512-AG0KID5qhQPzrFkh5b0Cws1Amytvzo0L0Xj6hMgJXEwD6ZXAqninDhSNAWJiN5zU4YVVZnuidnqwPwYJ04IamA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.76", + "@aws-sdk/credential-provider-http": "^3.972.78", + "@aws-sdk/credential-provider-ini": "^3.973.21", + "@aws-sdk/credential-provider-process": "^3.972.76", + "@aws-sdk/credential-provider-sso": "^3.973.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.82", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.936.0.tgz", - "integrity": "sha512-GpA4AcHb96KQK2PSPUyvChvrsEKiLhQ5NWjeef2IZ3Jc8JoosiedYqp6yhZR+S8cTysuvx56WyJIJc8y8OTrLA==", + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.76.tgz", + "integrity": "sha512-pdhKFTXv1zPYhh92MTlp0jVbLWvUEV9v+rziuEkoskGGQ2iJskxHTO38nA8UAbT/N4IM3n8OZJ9rIY5IBO15/A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.936.0.tgz", - "integrity": "sha512-wHlEAJJvtnSyxTfNhN98JcU4taA1ED2JvuI2eePgawqBwS/Tzi0mhED1lvNIaWOkjfLd+nHALwszGrtJwEq4yQ==", + "version": "3.973.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.20.tgz", + "integrity": "sha512-DlxrAB3MnjNLx/da7b89W+3DyJQYso/q7Ns15MsST2IV2nkAYQOYTUCaffNSjgrpMP99Dm2mV2jxzcbyF94ZfA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.936.0", - "@aws-sdk/core": "3.936.0", - "@aws-sdk/token-providers": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/token-providers": "3.1155.0", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.936.0.tgz", - "integrity": "sha512-v3qHAuoODkoRXsAF4RG+ZVO6q2P9yYBT4GMpMEfU9wXVNn7AIfwZgTwzSUfnjNiGva5BKleWVpRpJ9DeuLFbUg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.936.0", - "@aws-sdk/nested-clients": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", - "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", - "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.936.0.tgz", - "integrity": "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==", + "version": "3.972.82", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.82.tgz", + "integrity": "sha512-LUo46IqPTJ4xKBsrLuoaxiv/D2GTBCcFDMD1y3jKIqPR/p7Rw14RJFZ0iRWftqZtyLPQX9JMGnSi3mBu4y/pNA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws/lambda-invoke-store": "^0.2.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-sdk-sqs": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.936.0.tgz", - "integrity": "sha512-39WohFCCPeD6LV8zLQq7CyYbIieetEDDNLsEPeGJSh2Uv9qpY9r6zJRSTjb8hTuQbHDSEOGntHMYKpLoHdoxdQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/smithy-client": "^4.9.8", - "@smithy/types": "^4.9.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.936.0.tgz", - "integrity": "sha512-YB40IPa7K3iaYX0lSnV9easDOLPLh+fJyUDF3BH8doX4i1AOSsYn86L4lVldmOaSX+DwiaqKHpvk4wPBdcIPWw==", + "version": "3.972.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.46.tgz", + "integrity": "sha512-xXUZj3pl5rc2F2cf/n00WZRF4eI19gL5xr1cfqN882CJ6j53RAOt3GRLGOuFWz1iRgGm2OOVgpHqBDz/MXAP8Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@smithy/core": "^3.18.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.936.0.tgz", - "integrity": "sha512-eyj2tz1XmDSLSZQ5xnB7cLTVKkSJnYAEoNDSUNhzWPxrBDYeJzIbatecOKceKCU8NBf8gWWZCK/CSY0mDxMO0A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.936.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.936.0", - "@aws-sdk/middleware-user-agent": "3.936.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.936.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.5", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.12", - "@smithy/middleware-retry": "^4.4.12", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.8", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.11", - "@smithy/util-defaults-mode-node": "^4.2.14", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-utf8": "^4.2.0", + "version": "3.997.50", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.50.tgz", + "integrity": "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/signature-v4-multi-region": "^3.996.49", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", - "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.49.tgz", + "integrity": "sha512-nvuAZNRrRRVCiARRKsYlQ0VnXiEQiq7vXPH+UA+LSo+kN3Djl9eo/V2r0udkw85PnsNshmv2Em1clld5M3j67w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.975.0", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.936.0.tgz", - "integrity": "sha512-vvw8+VXk0I+IsoxZw0mX9TMJawUJvEsg3EF7zcCSetwhNPAU8Xmlhv7E/sN/FgSmm7b7DsqKoW6rVtQiCs1PWQ==", + "version": "3.1155.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1155.0.tgz", + "integrity": "sha512-iIcQnYLhWuq9qOLQbIWFrxF3PF0svVeZvHnUS4DUnO4zUFmFHIyGyjqrntXG+e/5PUgfavV0pYfDIKHxTzN2LA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.936.0", - "@aws-sdk/nested-clients": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/types": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", - "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", - "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-endpoints": "^3.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", - "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", - "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.936.0.tgz", - "integrity": "sha512-XOEc7PF9Op00pWV2AYCGDSu5iHgYjIO53Py2VUQTIvP7SRCaCsXmA33mjBvC2Ms6FhSyWNa4aK4naUGIz0hQcw==", + "version": "3.975.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.975.0.tgz", + "integrity": "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", - "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz", + "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", - "fast-xml-parser": "5.2.5", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws/lambda-invoke-store": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.1.tgz", - "integrity": "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -2897,6 +2497,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodable/entities": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.1.0.tgz", + "integrity": "sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2936,18 +2548,18 @@ } }, "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", "engines": { "node": ">=8.0.0" } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.204.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.204.0.tgz", - "integrity": "sha512-DqxY8yoAaiBPivoJD4UtgrMS8gEmzZ5lnaxzPojzLVHBGqPxgWm4zcuvcUHZiqQ6kRX2Klel2r9y8cA2HAtqpw==", + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -2956,22 +2568,10 @@ "node": ">=8.0.0" } }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz", - "integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/core": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", - "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz", + "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2984,14 +2584,14 @@ } }, "node_modules/@opentelemetry/instrumentation": { - "version": "0.204.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.204.0.tgz", - "integrity": "sha512-vV5+WSxktzoMP8JoYWKeopChy6G3HKk4UQ2hESCRDUUTZqQ3+nM3u8noVG0LmNfRWwcFBnbZ71GKC7vaYYdJ1g==", + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", + "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.204.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1" + "@opentelemetry/api-logs": "0.220.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3000,458 +2600,72 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.51.0.tgz", - "integrity": "sha512-XGmjYwjVRktD4agFnWBWQXo9SiYHKBxR6Ag3MLXwtLE4R99N3a08kGKM5SC1qOFKIELcQDGFEFT9ydXMH00Luw==", + "node_modules/@opentelemetry/resources": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.0.tgz", + "integrity": "sha512-+rSRoOdln6NhXTVcBdHSlZxJDNzwxadeweK2ltZO3tVtfp6aklABc06xclDk6ovRR+EiGgIcSbh/6NgjFhnb3A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@opentelemetry/core": "2.12.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/instrumentation-connect": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.48.0.tgz", - "integrity": "sha512-OMjc3SFL4pC16PeK+tDhwP7MRvDPalYCGSvGqUhX5rASkI2H0RuxZHOWElYeXkV0WP+70Gw6JHWac/2Zqwmhdw==", + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.0.tgz", + "integrity": "sha512-ImVuRQ6faOsTi1Wna88s2avgrrf/iDgXPigIeynkUhjstMuoQUDOJXgkez2j1mlU4btjXcXBGa+q7qKFYR3GsA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/connect": "3.4.38" + "@opentelemetry/core": "2.12.0", + "@opentelemetry/resources": "2.12.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/instrumentation-dataloader": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.22.0.tgz", - "integrity": "sha512-bXnTcwtngQsI1CvodFkTemrrRSQjAjZxqHVc+CJZTDnidT0T6wt3jkKhnsjU/Kkkc0lacr6VdRpCu2CUWa0OKw==", + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.12.0.tgz", + "integrity": "sha512-JOERgZCxRM06FEJ5kEWxlSRChah6MmXDx3KwCruqEv/iEyLkTokCSemf2y1w8oIHtfrr8EVTpzebq/31vn2Cpg==", + "deprecated": "Use @opentelemetry/sdk-trace package", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.204.0" + "@opentelemetry/core": "2.12.0", + "@opentelemetry/resources": "2.12.0", + "@opentelemetry/sdk-trace": "2.12.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.53.0.tgz", - "integrity": "sha512-r/PBafQmFYRjuxLYEHJ3ze1iBnP2GDA1nXOSS6E02KnYNZAVjj6WcDA1MSthtdAUUK0XnotHvvWM8/qz7DMO5A==", + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">=14" } }, - "node_modules/@opentelemetry/instrumentation-fs": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.24.0.tgz", - "integrity": "sha512-HjIxJ6CBRD770KNVaTdMXIv29Sjz4C1kPCCK5x1Ujpc6SNnLGPqUVyJYZ3LUhhnHAqdbrl83ogVWjCgeT4Q0yw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.204.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-generic-pool": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.48.0.tgz", - "integrity": "sha512-TLv/On8pufynNR+pUbpkyvuESVASZZKMlqCm4bBImTpXKTpqXaJJ3o/MUDeMlM91rpen+PEv2SeyOKcHCSlgag==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.204.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-graphql": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.52.0.tgz", - "integrity": "sha512-3fEJ8jOOMwopvldY16KuzHbRhPk8wSsOTSF0v2psmOCGewh6ad+ZbkTx/xyUK9rUdUMWAxRVU0tFpj4Wx1vkPA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.204.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-hapi": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.51.0.tgz", - "integrity": "sha512-qyf27DaFNL1Qhbo/da+04MSCw982B02FhuOS5/UF+PMhM61CcOiu7fPuXj8TvbqyReQuJFljXE6UirlvoT/62g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.204.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.204.0.tgz", - "integrity": "sha512-1afJYyGRA4OmHTv0FfNTrTAzoEjPQUYgd+8ih/lX0LlZBnGio/O80vxA0lN3knsJPS7FiDrsDrWq25K7oAzbkw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.1.0", - "@opentelemetry/instrumentation": "0.204.0", - "@opentelemetry/semantic-conventions": "^1.29.0", - "forwarded-parse": "2.1.2" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.1.0.tgz", - "integrity": "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/instrumentation-ioredis": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.52.0.tgz", - "integrity": "sha512-rUvlyZwI90HRQPYicxpDGhT8setMrlHKokCtBtZgYxQWRF5RBbG4q0pGtbZvd7kyseuHbFpA3I/5z7M8b/5ywg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/redis-common": "^0.38.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-kafkajs": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.14.0.tgz", - "integrity": "sha512-kbB5yXS47dTIdO/lfbbXlzhvHFturbux4EpP0+6H78Lk0Bn4QXiZQW7rmZY1xBCY16mNcCb8Yt0mhz85hTnSVA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.30.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.49.0.tgz", - "integrity": "sha512-NKsRRT27fbIYL4Ix+BjjP8h4YveyKc+2gD6DMZbr5R5rUeDqfC8+DTfIt3c3ex3BIc5Vvek4rqHnN7q34ZetLQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.33.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-koa": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.52.0.tgz", - "integrity": "sha512-JJSBYLDx/mNSy8Ibi/uQixu2rH0bZODJa8/cz04hEhRaiZQoeJ5UrOhO/mS87IdgVsHrnBOsZ6vDu09znupyuA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-lru-memoizer": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.49.0.tgz", - "integrity": "sha512-ctXu+O/1HSadAxtjoEg2w307Z5iPyLOMM8IRNwjaKrIpNAthYGSOanChbk1kqY6zU5CrpkPHGdAT6jk8dXiMqw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.204.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.57.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.57.0.tgz", - "integrity": "sha512-KD6Rg0KSHWDkik+qjIOWoksi1xqSpix8TSPfquIK1DTmd9OTFb5PHmMkzJe16TAPVEuElUW8gvgP59cacFcrMQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongoose": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.51.0.tgz", - "integrity": "sha512-gwWaAlhhV2By7XcbyU3DOLMvzsgeaymwP/jktDC+/uPkCmgB61zurwqOQdeiRq9KAf22Y2dtE5ZLXxytJRbEVA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.50.0.tgz", - "integrity": "sha512-duKAvMRI3vq6u9JwzIipY9zHfikN20bX05sL7GjDeLKr2qV0LQ4ADtKST7KStdGcQ+MTN5wghWbbVdLgNcB3rA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/mysql": "2.15.27" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql2": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.51.0.tgz", - "integrity": "sha512-zT2Wg22Xn43RyfU3NOUmnFtb5zlDI0fKcijCj9AcK9zuLZ4ModgtLXOyBJSSfO+hsOCZSC1v/Fxwj+nZJFdzLQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@opentelemetry/sql-common": "^0.41.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-pg": { - "version": "0.57.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.57.0.tgz", - "integrity": "sha512-dWLGE+r5lBgm2A8SaaSYDE3OKJ/kwwy5WLyGyzor8PLhUL9VnJRiY6qhp4njwhnljiLtzeffRtG2Mf/YyWLeTw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.34.0", - "@opentelemetry/sql-common": "^0.41.0", - "@types/pg": "8.15.5", - "@types/pg-pool": "2.0.6" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-redis": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.53.0.tgz", - "integrity": "sha512-WUHV8fr+8yo5RmzyU7D5BIE1zwiaNQcTyZPwtxlfr7px6NYYx7IIpSihJK7WA60npWynfxxK1T67RAVF0Gdfjg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/redis-common": "^0.38.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.23.0.tgz", - "integrity": "sha512-3TMTk/9VtlRonVTaU4tCzbg4YqW+Iq/l5VnN2e5whP6JgEg/PKfrGbqQ+CxQWNLfLaQYIUgEZqAn5gk/inh1uQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/tedious": "^4.0.14" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.15.0.tgz", - "integrity": "sha512-sNFGA/iCDlVkNjzTzPRcudmI11vT/WAfAguRdZY9IspCw02N4WSC72zTuQhSMheh2a1gdeM9my1imnKRvEEvEg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.204.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.7.0" - } - }, - "node_modules/@opentelemetry/redis-common": { - "version": "0.38.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz", - "integrity": "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", - "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", - "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/resources": "2.2.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", - "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sql-common": { - "version": "0.41.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz", - "integrity": "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0" - } - }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", - "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", - "license": "MIT", + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "license": "MIT", "dependencies": { "@noble/hashes": "^1.1.5" } @@ -3554,55 +2768,11 @@ "@prisma/debug": "6.19.0" } }, - "node_modules/@prisma/instrumentation": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.15.0.tgz", - "integrity": "sha512-6TXaH6OmDkMOQvOxwLZ8XS51hU2v4A3vmE2pSijCIiGRJYyNeMcL6nMHQMyYdZRD8wl7LF3Wzc+AMPMV/9Oo7A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.8" - } - }, - "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", - "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", - "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.57.2", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", - "shimmer": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", @@ -4008,718 +3178,218 @@ "win32" ] }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" - }, - "node_modules/@sentry/core": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.26.0.tgz", - "integrity": "sha512-TjDe5QI37SLuV0q3nMOH8JcPZhv2e85FALaQMIhRILH9Ce6G7xW5GSjmH91NUVq8yc3XtiqYlz/EenEZActc4Q==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/node": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.26.0.tgz", - "integrity": "sha512-VUwNoKYhRpnHQSj9lty1TgooO+1wcpS1V0K87HU8sZEas5gx3Ujiouk5ocPjlgbKreoYOApgOnEEIq5W/hfQcQ==", - "license": "MIT", - "dependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.1.0", - "@opentelemetry/core": "^2.1.0", - "@opentelemetry/instrumentation": "^0.204.0", - "@opentelemetry/instrumentation-amqplib": "0.51.0", - "@opentelemetry/instrumentation-connect": "0.48.0", - "@opentelemetry/instrumentation-dataloader": "0.22.0", - "@opentelemetry/instrumentation-express": "0.53.0", - "@opentelemetry/instrumentation-fs": "0.24.0", - "@opentelemetry/instrumentation-generic-pool": "0.48.0", - "@opentelemetry/instrumentation-graphql": "0.52.0", - "@opentelemetry/instrumentation-hapi": "0.51.0", - "@opentelemetry/instrumentation-http": "0.204.0", - "@opentelemetry/instrumentation-ioredis": "0.52.0", - "@opentelemetry/instrumentation-kafkajs": "0.14.0", - "@opentelemetry/instrumentation-knex": "0.49.0", - "@opentelemetry/instrumentation-koa": "0.52.0", - "@opentelemetry/instrumentation-lru-memoizer": "0.49.0", - "@opentelemetry/instrumentation-mongodb": "0.57.0", - "@opentelemetry/instrumentation-mongoose": "0.51.0", - "@opentelemetry/instrumentation-mysql": "0.50.0", - "@opentelemetry/instrumentation-mysql2": "0.51.0", - "@opentelemetry/instrumentation-pg": "0.57.0", - "@opentelemetry/instrumentation-redis": "0.53.0", - "@opentelemetry/instrumentation-tedious": "0.23.0", - "@opentelemetry/instrumentation-undici": "0.15.0", - "@opentelemetry/resources": "^2.1.0", - "@opentelemetry/sdk-trace-base": "^2.1.0", - "@opentelemetry/semantic-conventions": "^1.37.0", - "@prisma/instrumentation": "6.15.0", - "@sentry/core": "10.26.0", - "@sentry/node-core": "10.26.0", - "@sentry/opentelemetry": "10.26.0", - "import-in-the-middle": "^1.14.2", - "minimatch": "^9.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/node-core": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.26.0.tgz", - "integrity": "sha512-7OrHVn8XAsq9mMVMlpL18XTKQEVcLOJSo0n2M7QGKfFk/OfVtSFMcUWGqN1qhYtT9aMTr2bjtR5+BI33djnNTQ==", - "license": "MIT", - "dependencies": { - "@apm-js-collab/tracing-hooks": "^0.3.1", - "@sentry/core": "10.26.0", - "@sentry/opentelemetry": "10.26.0", - "import-in-the-middle": "^1.14.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", - "@opentelemetry/instrumentation": ">=0.57.1 <1", - "@opentelemetry/resources": "^1.30.1 || ^2.1.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", - "@opentelemetry/semantic-conventions": "^1.37.0" - } - }, - "node_modules/@sentry/opentelemetry": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.26.0.tgz", - "integrity": "sha512-ASJdOwn6NwMH2ZeBrnGJI+l/xkJp1AOiQ5FWkvTqLc/vHX+r3PDMO7c+koecY+LiZxSzZF4IV8sALXfOh6UnwA==", - "license": "MIT", - "dependencies": { - "@sentry/core": "10.26.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", - "@opentelemetry/semantic-conventions": "^1.37.0" - } - }, - "node_modules/@smithy/abort-controller": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", - "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/config-resolver": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", - "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/core": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.5.tgz", - "integrity": "sha512-6gnIz3h+PEPQGDj8MnRSjDvKBah042jEoPgjFGJ4iJLBE78L4lY/n98x14XyPF4u3lN179Ub/ZKFY5za9GeLQw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/middleware-serde": "^4.2.6", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-stream": "^4.5.6", - "@smithy/util-utf8": "^4.2.0", - "@smithy/uuid": "^1.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", - "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", - "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-node": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", - "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", - "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/md5-js": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.5.tgz", - "integrity": "sha512-Bt6jpSTMWfjCtC0s79gZ/WZ1w90grfmopVOWqkI2ovhjpD5Q2XRXuecIPB9689L2+cCySMbaXDhBPU56FKNDNg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", - "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.12.tgz", - "integrity": "sha512-9pAX/H+VQPzNbouhDhkW723igBMLgrI8OtX+++M7iKJgg/zY/Ig3i1e6seCcx22FWhE6Q/S61BRdi2wXBORT+A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.18.5", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-middleware": "^4.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.12.tgz", - "integrity": "sha512-S4kWNKFowYd0lID7/DBqWHOQxmxlsf0jBaos9chQZUWTVOjSW1Ogyh8/ib5tM+agFDJ/TCxuCTvrnlc+9cIBcQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/service-error-classification": "^4.2.5", - "@smithy/smithy-client": "^4.9.8", - "@smithy/types": "^4.9.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/uuid": "^1.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-serde": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", - "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-stack": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", - "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-config-provider": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", - "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", - "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/abort-controller": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/property-provider": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", - "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/protocol-http": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", - "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-builder": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", - "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "@smithy/util-uri-escape": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", - "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/service-error-classification": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", - "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", - "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", - "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-uri-escape": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/smithy-client": { - "version": "4.9.8", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.8.tgz", - "integrity": "sha512-8xgq3LgKDEFoIrLWBho/oYKyWByw9/corz7vuh1upv7ZBm0ZMjGYBhbn6v643WoIqA9UTcx5A5htEp/YatUwMA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.18.5", - "@smithy/middleware-endpoint": "^4.3.12", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@smithy/types": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", - "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" }, - "node_modules/@smithy/url-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", - "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/querystring-parser": "^4.2.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" }, - "node_modules/@smithy/util-base64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", - "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, + "node_modules/@sentry/conventions": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz", + "integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==", + "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=14" } }, - "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", - "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", - "license": "Apache-2.0", + "node_modules/@sentry/core": { + "version": "10.76.2", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.76.2.tgz", + "integrity": "sha512-vndrQalRiwYatTkjlLWvyf69KocXIb9TKJ4A9oiyWguL8PJYtqIRTlRe4OpkpjLrxS18fvRdV0jEm29c/9p3hQ==", + "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "@sentry/conventions": "^0.16.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=18" } }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", - "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", - "license": "Apache-2.0", + "node_modules/@sentry/node": { + "version": "10.76.2", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.76.2.tgz", + "integrity": "sha512-LSwGGE/GceTbkDHRoQQr3lq/dpxxA6RdooBbuGTJ/S6BdqKX2kw2FdV7Se9eJzZJJJ5AxCZum37+ejYS7N2pJg==", + "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.76.2", + "@sentry/node-core": "10.76.2", + "@sentry/opentelemetry": "10.76.2", + "@sentry/server-utils": "10.76.2", + "import-in-the-middle": "^3.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=18" } }, - "node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", - "license": "Apache-2.0", + "node_modules/@sentry/node-core": { + "version": "10.76.2", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.76.2.tgz", + "integrity": "sha512-TrgAc/SvU5kQfjw8pz6I8LLIUiblOSf9BPPNkTbS0Dzcqa4yQLVbmsbS3rvGT7ot64m9Vjzn327ZRy63MlI6/w==", + "license": "MIT", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "tslib": "^2.6.2" + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.76.2", + "@sentry/opentelemetry": "10.76.2", + "import-in-the-middle": "^3.0.0" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-config-provider": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", - "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" + "node": ">=18" }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.11.tgz", - "integrity": "sha512-yHv+r6wSQXEXTPVCIQTNmXVWs7ekBTpMVErjqZoWkYN75HIFN5y9+/+sYOejfAuvxWGvgzgxbTHa/oz61YTbKw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.5", - "@smithy/smithy-client": "^4.9.8", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" }, - "engines": { - "node": ">=18.0.0" + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/core": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-http": { + "optional": true + }, + "@opentelemetry/instrumentation": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + } } }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.14.tgz", - "integrity": "sha512-ljZN3iRvaJUgulfvobIuG97q1iUuCMrvXAlkZ4msY+ZuVHQHDIqn7FKZCEj+bx8omz6kF5yQXms/xhzjIO5XiA==", - "license": "Apache-2.0", + "node_modules/@sentry/opentelemetry": { + "version": "10.76.2", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.76.2.tgz", + "integrity": "sha512-deqWrKJGaku3nhiZchg34z8W9v6kUwtzoQ4cmEq6CeT8/4X93X4YfeJgoh1dhbswFRpwor4pQ6kj0KttagKtfA==", + "license": "MIT", "dependencies": { - "@smithy/config-resolver": "^4.4.3", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/smithy-client": "^4.9.8", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.76.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", - "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" + "node": ">=18" }, - "engines": { - "node": ">=18.0.0" + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" } }, - "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", - "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", - "license": "Apache-2.0", + "node_modules/@sentry/server-utils": { + "version": "10.76.2", + "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.76.2.tgz", + "integrity": "sha512-wogj6Trj8fAu+aD7HbpvHEBlyANMOvvO9MP5NFJaiQbprLKugtCdE6rFTxDhofXvRo4nswL9m4Cvysg3k5KcvQ==", + "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.76.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=18" } }, - "node_modules/@smithy/util-middleware": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", - "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", + "node_modules/@smithy/core": { + "version": "3.36.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.2.tgz", + "integrity": "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/util-retry": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", - "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", + "node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/util-stream": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", - "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", + "node_modules/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", - "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", "license": "Apache-2.0", "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "node_modules/@smithy/signature-v4": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz", + "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/uuid": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", - "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "node_modules/@smithy/types": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4849,6 +3519,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -4952,15 +3623,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mysql": { - "version": "2.15.27", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", - "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node": { "version": "24.10.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", @@ -4987,26 +3649,6 @@ "form-data": "^4.0.4" } }, - "node_modules/@types/pg": { - "version": "8.15.5", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", - "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@types/pg-pool": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", - "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", - "license": "MIT", - "dependencies": { - "@types/pg": "*" - } - }, "node_modules/@types/qrcode": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", @@ -5078,21 +3720,6 @@ "@types/node": "*" } }, - "node_modules/@types/shimmer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", - "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", - "license": "MIT" - }, - "node_modules/@types/tedious": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", - "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -5105,6 +3732,15 @@ "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.47.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", @@ -5519,13 +4155,6 @@ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "license": "BSD-2-Clause" }, - "node_modules/@zxing/text-encoding": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", - "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", - "license": "(Unlicense OR Apache-2.0)", - "optional": true - }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -5564,6 +4193,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5572,15 +4202,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -5705,6 +4326,18 @@ "dev": true, "license": "MIT" }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -5947,6 +4580,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -5968,14 +4602,40 @@ } }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" } }, "node_modules/baileys": { @@ -6037,6 +4697,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -6097,23 +4758,23 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.9", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.9.tgz", + "integrity": "sha512-FISIQxV5u91Y9gvuFq3q/tA/qTqLJn3AhRcEvyozyhJTh5OBFFtv827AdHU7Rbyct4r+Ba1/99oFa1Y3yuFsdw==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.16.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -6129,12 +4790,41 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -6142,15 +4832,16 @@ "license": "ISC" }, "node_modules/bowser": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.0.tgz", - "integrity": "sha512-yHAbSRuT6LTeKi6k2aS40csueHqgAsFEgmrOsfRyFpJnFv5O2hl9FYmWEUZ97gZ/dG17U4IQQcTx4YAFYPuWRQ==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -6214,12 +4905,6 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, "node_modules/buffer-more-ints": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", @@ -6515,9 +5200,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.3.0.tgz", + "integrity": "sha512-lmNyBzi6iYiqyrExTctfmjd8tInx+dHu9PBomTfH5RXS53c27JMFJgzNv5TyAnm3PgGf+MLy0+Ws41/HlVxdKQ==", "license": "MIT" }, "node_modules/class-validator": { @@ -6700,14 +5385,15 @@ } }, "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz", + "integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", + "destroy": "1.2.0", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", @@ -6715,6 +5401,10 @@ }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/compression/node_modules/debug": { @@ -6739,21 +5429,6 @@ "dev": true, "license": "MIT" }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, "node_modules/confbox": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", @@ -7222,9 +5897,9 @@ } }, "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "license": "MIT" }, "node_modules/delayed-stream": { @@ -7453,59 +6128,42 @@ } }, "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "version": "6.6.11", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.11.tgz", + "integrity": "sha512-z34mGpLoY/82ZbkX1LqGMIaf3wjVMPL6gVC681QDqnmzJ26synI8qAE32P+a7UazmpJZ8UGrFIeAavgRfXzYrg==", "license": "MIT", "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", "accepts": "~1.3.4", - "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" + "ws": "~8.21.0" }, "engines": { "node": ">=10.2.0" } }, "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.7.tgz", + "integrity": "sha512-GyAvmPA7gnRCpDQbYACAy79Lg4ewevrmaPzySZz0OVvgyZc3gEfLLqPTYk8wVBScjOEpM9ZO4KryyFpVItjGEA==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" } }, "node_modules/engine.io-client/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -7541,27 +6199,10 @@ "node": ">= 0.6" } }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/engine.io/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -8509,39 +7150,39 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz", + "integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.13", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.16.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -8696,10 +7337,26 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", + "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, "node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.2.tgz", + "integrity": "sha512-R9iDuNrQYeQut46cn2r2wHKn4HYzVDvDm5J1wW+koZewykv0yuO2HChTYeZtrULyHIDM9cj9TUXloCsueCQUog==", "funding": [ { "type": "github", @@ -8708,7 +7365,12 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "@nodable/entities": "^3.0.1", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.2", + "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -8977,9 +7639,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", + "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", "funding": [ { "type": "individual", @@ -9000,6 +7662,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -9012,16 +7675,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -9055,12 +7718,6 @@ "node": ">= 0.6" } }, - "node_modules/forwarded-parse": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", - "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", - "license": "MIT" - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -9152,6 +7809,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9576,9 +8234,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -9787,17 +8445,25 @@ } }, "node_modules/import-in-the-middle": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz", - "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.5.2.tgz", + "integrity": "sha512-WseIA/4o56+GYO3RmlOPvejYi771OFeJEeK/ejROsr9bdjW+E5NXTOD1Di8TL40Qgg3JJX9aa97Hf8iiSVIKZw==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.14.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^3.0.2", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" } }, + "node_modules/import-in-the-middle/node_modules/es-module-lexer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.4.tgz", + "integrity": "sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==", + "license": "MIT" + }, "node_modules/import-meta-resolve": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", @@ -9966,9 +8632,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.7.4", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz", + "integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==", "license": "MIT", "engines": { "node": ">= 12" @@ -9983,22 +8649,6 @@ "node": ">= 10" } }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -10091,6 +8741,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10103,6 +8754,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -10194,6 +8846,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.4", @@ -10308,6 +8961,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10403,6 +9057,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -10427,6 +9082,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz", + "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -10754,12 +9421,12 @@ } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -11417,12 +10084,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -11441,9 +10109,9 @@ } }, "node_modules/minio": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.6.tgz", - "integrity": "sha512-sOeh2/b/XprRmEtYsnNRFtOqNRTPDvYtMWh+spWlfsuCV/+IdxNeKVUMKLqI7b5Dr07ZqCPuaRGU/rB9pZYVdQ==", + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.7.tgz", + "integrity": "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==", "license": "Apache-2.0", "dependencies": { "async": "^3.2.4", @@ -11451,14 +10119,13 @@ "browser-or-node": "^2.1.1", "buffer-crc32": "^1.0.0", "eventemitter3": "^5.0.1", - "fast-xml-parser": "^4.4.1", + "fast-xml-parser": "^5.3.4", "ipaddr.js": "^2.0.1", "lodash": "^4.17.21", "mime-types": "^2.1.35", "query-string": "^7.1.3", "stream-json": "^1.8.0", "through2": "^4.0.2", - "web-encoding": "^1.1.5", "xml2js": "^0.5.0 || ^0.6.2" }, "engines": { @@ -11471,48 +10138,6 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, - "node_modules/minio/node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.1.1" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/minio/node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -11571,21 +10196,21 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.5.0.tgz", + "integrity": "sha512-BsqOzYGVq49JcW2IzoSAxeKYRevb+dGGPPWleOR8Lrlk4igwxu1/9pwSXlByhCb7MVZDrNJFC0IBP5/O5nLYRw==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" + "type-is": "^1.6.18" }, "engines": { "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/music-metadata": { @@ -12567,6 +11192,21 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -12590,12 +11230,13 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/pathe": { @@ -12717,9 +11358,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -12820,6 +11461,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13040,9 +11682,9 @@ } }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -13050,6 +11692,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/proxy-addr/node_modules/ipaddr.js": { @@ -13062,10 +11708,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "1.3.2", @@ -13322,12 +11971,13 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", + "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -13406,16 +12056,45 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -13547,17 +12226,16 @@ } }, "node_modules/require-in-the-middle": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", - "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", "license": "MIT", "dependencies": { "debug": "^4.3.5", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.8" + "module-details-from-path": "^1.0.3" }, "engines": { - "node": ">=8.6.0" + "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, "node_modules/require-main-filename": { @@ -13576,6 +12254,7 @@ "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -13814,6 +12493,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -14060,21 +12740,15 @@ "node": ">=8" } }, - "node_modules/shimmer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", - "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", - "license": "BSD-2-Clause" - }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -14086,13 +12760,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -14209,36 +12883,19 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", - "license": "MIT", - "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "debug": "~4.4.1", + "ws": "~8.21.0" } }, "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -14289,35 +12946,18 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "debug": "~4.4.1" }, "engines": { "node": ">=10.0.0" } }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/socket.io/node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -14642,16 +13282,19 @@ "license": "MIT" }, "node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.5.1.tgz", + "integrity": "sha512-UM4nTLWzQiDBseVxUGxN0wJzIk0UU89Qi7pKXz9qISo+lhviJikTuikBsK/oFRlpvYHvSdz0TVoLyQ+sEpYwxA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT" + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } }, "node_modules/strtok3": { "version": "10.3.5", @@ -14719,6 +13362,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -15756,12 +14400,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -15815,9 +14453,9 @@ } }, "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.30.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz", + "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -15909,19 +14547,6 @@ "pako": "^1.0.11" } }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -15938,9 +14563,9 @@ } }, "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -16200,18 +14825,6 @@ "defaults": "^1.0.3" } }, - "node_modules/web-encoding": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", - "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", - "license": "MIT", - "dependencies": { - "util": "^0.12.3" - }, - "optionalDependencies": { - "@zxing/text-encoding": "0.9.0" - } - }, "node_modules/web-streams-polyfill": { "version": "4.0.0-beta.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", @@ -16335,6 +14948,7 @@ "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -16447,9 +15061,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -16467,6 +15081,21 @@ } } }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xml-parse-from-string": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 667b033503..c5bb358a1d 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -82,6 +82,7 @@ import { createId as cuid } from '@paralleldrive/cuid2'; import { Instance, Message } from '@prisma/client'; import { createJid } from '@utils/createJid'; import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; +import { headerAsString } from '@utils/headerAsString'; import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent'; import { MESSAGE_DEDUPE_TTL_SECONDS, messageDedupeKey, MessageDedupeKeyInput } from '@utils/messageDedupe'; import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; @@ -2905,7 +2906,7 @@ export class BaileysStartupService extends ChannelStartupService { const response = await axios.get(mediaMessage.media, config); - mimetype = response.headers['content-type']; + mimetype = headerAsString(response.headers['content-type']) ?? false; } } diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 906fff1881..63e003fd7f 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -20,6 +20,7 @@ import ChatwootClient, { } from '@figuro/chatwoot-sdk'; import { request as chatwootRequest } from '@figuro/chatwoot-sdk/dist/core/request'; import { Chatwoot as ChatwootModel, Contact as ContactModel, Message as MessageModel } from '@prisma/client'; +import { headerAsString } from '@utils/headerAsString'; import i18next from '@utils/i18n'; import { sendTelemetry } from '@utils/sendTelemetry'; import axios from 'axios'; @@ -1204,7 +1205,7 @@ export class ChatwootService { const response = await axios.get(media, { responseType: 'arraybuffer', }); - mimeType = response.headers['content-type']; + mimeType = headerAsString(response.headers['content-type']); } let type = 'document'; @@ -2214,7 +2215,8 @@ export class ChatwootService { if (isAdsMessage) { const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' }); - const extension = mimeTypes.extension(imgBuffer.headers['content-type']); + const contentType = headerAsString(imgBuffer.headers['content-type']); + const extension = contentType && mimeTypes.extension(contentType); const mimeType = extension && mimeTypes.lookup(extension); if (!mimeType) { diff --git a/src/utils/headerAsString.ts b/src/utils/headerAsString.ts new file mode 100644 index 0000000000..b49096a4ca --- /dev/null +++ b/src/utils/headerAsString.ts @@ -0,0 +1 @@ +export const headerAsString = (value: unknown): string | undefined => (typeof value === 'string' ? value : undefined); diff --git a/test/utils/headerAsString.test.ts b/test/utils/headerAsString.test.ts new file mode 100644 index 0000000000..9535850f50 --- /dev/null +++ b/test/utils/headerAsString.test.ts @@ -0,0 +1,16 @@ +import { headerAsString } from '@utils/headerAsString'; +import { describe, expect, it } from 'vitest'; + +describe('headerAsString', () => { + it('returns string header values', () => { + expect(headerAsString('image/png')).toBe('image/png'); + }); + + it('returns undefined for missing or non-string header values', () => { + expect(headerAsString(undefined)).toBeUndefined(); + expect(headerAsString(null)).toBeUndefined(); + expect(headerAsString(42)).toBeUndefined(); + expect(headerAsString(true)).toBeUndefined(); + expect(headerAsString(['a', 'b'])).toBeUndefined(); + }); +}); From a83a22b201f8c8b01b18865104761de647b847ba Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 21:04:19 -0300 Subject: [PATCH 65/96] fix(baileys): keep the media mimetype when the file name has no extension prepareMediaMessage overwrote the mimetype with mimeTypes.lookup(fileName).toString(), which is the string "false" for names without a known extension. Images lost image/jpeg and URL media lost the downloaded content type, so WhatsApp received mimetype "false". The file name now only sets the mimetype when it resolves to one. --- .../whatsapp/whatsapp.baileys.service.ts | 8 +-- .../whatsapp/prepareMediaMessage.test.ts | 63 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 test/api/integrations/channel/whatsapp/prepareMediaMessage.test.ts diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 667b033503..daab3dc190 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2943,11 +2943,9 @@ export class BaileysStartupService extends ChannelStartupService { } } - if (mediaMessage?.fileName) { - mimetype = mimeTypes.lookup(mediaMessage.fileName).toString(); - if (mimetype === 'application/mp4') { - mimetype = 'video/mp4'; - } + const fileNameMimetype = mediaMessage?.fileName && mimeTypes.lookup(mediaMessage.fileName); + if (fileNameMimetype) { + mimetype = fileNameMimetype === 'application/mp4' ? 'video/mp4' : fileNameMimetype; } prepareMedia[mediaType].caption = mediaMessage?.caption; diff --git a/test/api/integrations/channel/whatsapp/prepareMediaMessage.test.ts b/test/api/integrations/channel/whatsapp/prepareMediaMessage.test.ts new file mode 100644 index 0000000000..f14a42273a --- /dev/null +++ b/test/api/integrations/channel/whatsapp/prepareMediaMessage.test.ts @@ -0,0 +1,63 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import axios from 'axios'; +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('baileys', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + prepareWAMessageMedia: vi.fn(async (content: Record) => ({ + [`${Object.keys(content)[0]}Message`]: {}, + })), + }; +}); + +vi.mock('axios', () => ({ default: { get: vi.fn() } })); + +const onePixelPng = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; + +const buildService = () => { + const service = Object.create(BaileysStartupService.prototype) as any; + service.client = { waUploadToServer: vi.fn() }; + service.instance = { wuid: '5491100000000@s.whatsapp.net' }; + service.logger = { verbose: vi.fn(), error: vi.fn() }; + return service; +}; + +const preparedMimetype = async (mediaMessage: Record) => { + const prepared = await buildService().prepareMediaMessage(mediaMessage); + return Object.values(prepared.message)[0] as { mimetype: unknown; fileName: unknown }; +}; + +describe('BaileysStartupService.prepareMediaMessage mimetype', () => { + it('keeps the downloaded content type when the file name has no extension', async () => { + vi.mocked(axios.get).mockResolvedValue({ headers: { 'content-type': 'application/pdf' }, data: Buffer.from('x') }); + + const media = await preparedMimetype({ + mediatype: 'document', + media: 'https://files.example.com/report', + fileName: 'report', + }); + + expect(media.mimetype).toBe('application/pdf'); + expect(media.fileName).toBe('report'); + }); + + it('keeps image/jpeg for images whose file name has no extension', async () => { + const media = await preparedMimetype({ mediatype: 'image', media: onePixelPng, fileName: 'logo' }); + + expect(media.mimetype).toBe('image/jpeg'); + }); + + it('still derives the mimetype from a file name extension', async () => { + const media = await preparedMimetype({ mediatype: 'document', media: 'aGVsbG8=', fileName: 'notes.txt' }); + + expect(media.mimetype).toBe('text/plain'); + }); + + it('still maps application/mp4 file names to video/mp4', async () => { + const media = await preparedMimetype({ mediatype: 'document', media: 'aGVsbG8=', fileName: 'clip.mp4' }); + + expect(media.mimetype).toBe('video/mp4'); + }); +}); From a4b32ddc341e6940adbeb8ffb99cdaf4fcbd454d Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 21:27:57 -0300 Subject: [PATCH 66/96] build(deps): clear transitive and npm CLI vulnerabilities - @figuro/chatwoot-sdk pinned axios 0.27.2 (10 findings); an override moves it to the shared axios 1.x, and a test drives its request helper against a local server (same result on 0.27 and 1.20). - lodash (via minio) overridden to 4.18.1; deepmerge-ts (via @prisma/config) overridden to 8; prisma and @prisma/client 6.19.3 bring effect 3.21. prisma generate and migrate deploy run unchanged. - The final image upgrades the bundled npm to 11.21.1, whose undici and brace-expansion are patched; the start script uses npm. Trivy fixable CRITICAL/HIGH findings drop from 21 to 5 (sharp 0.34 and link-preview-js 3, both major upgrades used by Baileys). --- Dockerfile | 2 + package-lock.json | 174 ++++++++++++++------------ package.json | 11 +- test/patches/chatwootSdkAxios.test.ts | 54 ++++++++ 4 files changed, 157 insertions(+), 84 deletions(-) create mode 100644 test/patches/chatwootSdkAxios.test.ts diff --git a/Dockerfile b/Dockerfile index da2a625d1a..b87bb3013d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,6 +38,8 @@ FROM node:24.22.1-alpine3.24@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c RUN apk update && \ apk add tzdata ffmpeg bash openssl +RUN npm install -g npm@11.21.1 + ENV TZ=America/Sao_Paulo ENV DOCKER_ENV=true diff --git a/package-lock.json b/package-lock.json index 01318b9277..df937d7ada 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@figuro/chatwoot-sdk": "^1.1.16", "@hapi/boom": "^10.0.1", "@paralleldrive/cuid2": "^2.2.2", - "@prisma/client": "^6.16.2", + "@prisma/client": "^6.19.3", "@sentry/node": "^10.12.0", "@types/uuid": "^10.0.0", "amqplib": "^0.10.5", @@ -57,7 +57,7 @@ "patch-package": "^8.0.1", "pg": "^8.13.1", "pino": "^9.10.0", - "prisma": "^6.1.0", + "prisma": "^6.19.3", "pusher": "^5.2.0", "qrcode": "^1.5.4", "qrcode-terminal": "^0.12.0", @@ -1448,16 +1448,6 @@ "axios": "^0.27.2" } }, - "node_modules/@figuro/chatwoot-sdk/node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, "node_modules/@hapi/boom": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", @@ -2690,9 +2680,9 @@ } }, "node_modules/@prisma/client": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.0.tgz", - "integrity": "sha512-QXFT+N/bva/QI2qoXmjBzL7D6aliPffIwP+81AdTGq0FXDoLxLkWivGMawG8iM5B9BKfxLIXxfWWAF6wbuJU6g==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -2712,60 +2702,60 @@ } }, "node_modules/@prisma/config": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.0.tgz", - "integrity": "sha512-zwCayme+NzI/WfrvFEtkFhhOaZb/hI+X8TTjzjJ252VbPxAl2hWHK5NMczmnG9sXck2lsXrxIZuK524E25UNmg==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", "license": "Apache-2.0", "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", - "effect": "3.18.4", + "effect": "3.21.0", "empathic": "2.0.0" } }, "node_modules/@prisma/debug": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.0.tgz", - "integrity": "sha512-8hAdGG7JmxrzFcTzXZajlQCidX0XNkMJkpqtfbLV54wC6LSSX6Vni25W/G+nAANwLnZ2TmwkfIuWetA7jJxJFA==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.0.tgz", - "integrity": "sha512-pMRJ+1S6NVdXoB8QJAPIGpKZevFjxhKt0paCkRDTZiczKb7F4yTgRP8M4JdVkpQwmaD4EoJf6qA+p61godDokw==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.19.0", - "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", - "@prisma/fetch-engine": "6.19.0", - "@prisma/get-platform": "6.19.0" + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" } }, "node_modules/@prisma/engines-version": { - "version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773.tgz", - "integrity": "sha512-gV7uOBQfAFlWDvPJdQxMT1aSRur3a0EkU/6cfbAC5isV67tKDWUrPauyaHNpB+wN1ebM4A9jn/f4gH+3iHSYSQ==", + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.0.tgz", - "integrity": "sha512-OOx2Lda0DGrZ1rodADT06ZGqHzr7HY7LNMaFE2Vp8dp146uJld58sRuasdX0OiwpHgl8SqDTUKHNUyzEq7pDdQ==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.19.0", - "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", - "@prisma/get-platform": "6.19.0" + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" } }, "node_modules/@prisma/get-platform": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.0.tgz", - "integrity": "sha512-ym85WDO2yDhC3fIXHWYpG3kVMBA49cL1XD2GCsCF8xbwoy2OkDQY44gEbAt2X46IQ4Apq9H6g0Ex1iFfPqEkHA==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.19.0" + "@prisma/debug": "6.19.3" } }, "node_modules/@protobufjs/aspromise": { @@ -3405,9 +3395,9 @@ "license": "MIT" }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, "node_modules/@thi.ng/bitstream": { @@ -5430,9 +5420,9 @@ "license": "MIT" }, "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "license": "MIT" }, "node_modules/consola": { @@ -5830,12 +5820,22 @@ "license": "MIT" }, "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz", + "integrity": "sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==", + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/deepmerge-ts" + } + ], "license": "BSD-3-Clause", "engines": { - "node": ">=16.0.0" + "node": ">=16.9.0" } }, "node_modules/defaults": { @@ -6094,9 +6094,9 @@ "license": "MIT" }, "node_modules/effect": { - "version": "3.18.4", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", - "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -7220,9 +7220,9 @@ "license": "MIT" }, "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.3.tgz", + "integrity": "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==", "license": "MIT" }, "node_modules/external-editor": { @@ -9584,9 +9584,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -10436,24 +10436,28 @@ } }, "node_modules/nypm": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", - "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.11.tgz", + "integrity": "sha512-wrVBTLWDu7nTUKGWplQebWOXWnhaIwyKQhBxobH/CGxJHoVw27r9vG5ba7kdZnxG+f4rC6dZd6m2oHmpkOcO7g==", "license": "MIT", "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", + "citty": "^0.2.2", "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "tinyexec": "^1.0.1" + "tinyexec": "^1.3.1" }, "bin": { "nypm": "dist/cli.mjs" }, "engines": { - "node": "^14.16.0 || >=16.10.0" + "node": ">=18" } }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10575,9 +10579,9 @@ } }, "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", "license": "MIT" }, "node_modules/omggif": { @@ -11438,16 +11442,22 @@ } }, "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.4.0.tgz", + "integrity": "sha512-07Fys7iVx0X6TcviMOgogM1Lk33ZyqOezMnU79S+kffWqwolG/5W9V8EgQSeYCIfFTYVgssRbnH630Oa2+soBA==", "license": "MIT", "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", + "confbox": "^0.3.1", + "exsolve": "^1.1.3", "pathe": "^2.0.3" } }, + "node_modules/pkg-types/node_modules/confbox": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", + "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "license": "MIT" + }, "node_modules/pngjs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", @@ -11618,14 +11628,14 @@ } }, "node_modules/prisma": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.0.tgz", - "integrity": "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "6.19.0", - "@prisma/engines": "6.19.0" + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" }, "bin": { "prisma": "build/index.js" diff --git a/package.json b/package.json index d203d79c2b..88defa8276 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@figuro/chatwoot-sdk": "^1.1.16", "@hapi/boom": "^10.0.1", "@paralleldrive/cuid2": "^2.2.2", - "@prisma/client": "^6.16.2", + "@prisma/client": "^6.19.3", "@sentry/node": "^10.12.0", "@types/uuid": "^10.0.0", "amqplib": "^0.10.5", @@ -118,7 +118,7 @@ "patch-package": "^8.0.1", "pg": "^8.13.1", "pino": "^9.10.0", - "prisma": "^6.1.0", + "prisma": "^6.19.3", "pusher": "^5.2.0", "qrcode": "^1.5.4", "qrcode-terminal": "^0.12.0", @@ -162,5 +162,12 @@ "tsx": "^4.20.5", "typescript": "^5.7.2", "vitest": "^3.2.7" + }, + "overrides": { + "@figuro/chatwoot-sdk": { + "axios": "^1.20.0" + }, + "lodash": "^4.18.1", + "deepmerge-ts": "^8.0.0" } } diff --git a/test/patches/chatwootSdkAxios.test.ts b/test/patches/chatwootSdkAxios.test.ts new file mode 100644 index 0000000000..3c51c14e8c --- /dev/null +++ b/test/patches/chatwootSdkAxios.test.ts @@ -0,0 +1,54 @@ +import { createServer, IncomingMessage, Server } from 'node:http'; +import { AddressInfo } from 'node:net'; + +import { request as chatwootRequest } from '@figuro/chatwoot-sdk/dist/core/request'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const readBody = (request: IncomingMessage) => + new Promise((resolve) => { + const chunks: Buffer[] = []; + request.on('data', (chunk) => chunks.push(chunk)); + request.on('end', () => resolve(Buffer.concat(chunks).toString())); + }); + +describe('chatwoot sdk on the shared axios', () => { + let server: Server; + let base: string; + const seen: { method?: string; url?: string; token?: string; body?: string } = {}; + + beforeAll(async () => { + server = createServer(async (request, response) => { + seen.method = request.method; + seen.url = request.url; + seen.token = request.headers.api_access_token as string; + seen.body = await readBody(request); + const status = request.url?.includes('missing') ? 404 : 200; + response.writeHead(status, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ payload: [{ id: 7 }] })); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + }); + + afterAll(async () => { + await new Promise((resolve) => server.close(resolve)); + }); + + const config = () => ({ basePath: base, with_credentials: true, credentials: 'include', token: 'token-1' }) as any; + + it('sends a json request and resolves the response body', async () => { + const result = await chatwootRequest(config(), { + method: 'POST', + url: '/api/v1/accounts/1/contacts/filter', + body: { payload: [{ attribute_key: 'phone_number' }] }, + }); + + expect(result).toEqual({ payload: [{ id: 7 }] }); + expect(seen).toMatchObject({ method: 'POST', url: '/api/v1/accounts/1/contacts/filter', token: 'token-1' }); + expect(JSON.parse(seen.body)).toEqual({ payload: [{ attribute_key: 'phone_number' }] }); + }); + + it('rejects with the http status on errors', async () => { + await expect(chatwootRequest(config(), { method: 'GET', url: '/missing' })).rejects.toMatchObject({ status: 404 }); + }); +}); From 4ff29ee4c338165bfb1529112388f211eb4a9904 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 21:43:53 -0300 Subject: [PATCH 67/96] build(deps): upgrade sharp to 0.35 and link-preview-js to 4 sharp 0.34 and link-preview-js 3 carry the last fixable CRITICAL/HIGH findings of the image. sharp 0.35 keeps the jpeg, webp and resize APIs Evolution and Baileys use and still exposes a default export to Baileys' dynamic import; link-preview-js 4 keeps the getLinkPreview options Baileys passes (an override satisfies Baileys' ^3 peer range). Tests cover jpeg conversion of media, webp stickers and the Baileys thumbnail; the link preview test runs on link-preview-js 4. --- package-lock.json | 354 +++++++++++----------- package.json | 7 +- test/patches/sharpImageProcessing.test.ts | 54 ++++ 3 files changed, 242 insertions(+), 173 deletions(-) create mode 100644 test/patches/sharpImageProcessing.test.ts diff --git a/package-lock.json b/package-lock.json index df937d7ada..0d0be6323b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "jsonwebtoken": "^9.0.2", "kafkajs": "^2.2.4", "libphonenumber-js": "^1.12.25", - "link-preview-js": "^3.0.13", + "link-preview-js": "^4.0.4", "long": "^5.2.3", "mediainfo.js": "^0.3.4", "mime": "^4.0.0", @@ -63,7 +63,7 @@ "qrcode-terminal": "^0.12.0", "redis": "^4.7.0", "rxjs": "^7.8.2", - "sharp": "^0.34.2", + "sharp": "^0.35.5", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "socks-proxy-agent": "^8.0.5", @@ -735,9 +735,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", "optional": true, "dependencies": { @@ -1526,18 +1526,18 @@ "license": "BSD-3-Clause" }, "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -1547,19 +1547,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -1569,19 +1569,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -1595,9 +1614,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -1611,9 +1630,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -1627,9 +1646,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -1643,9 +1662,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -1659,9 +1678,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -1675,9 +1694,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -1691,9 +1710,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -1707,9 +1726,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -1723,9 +1742,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -1739,9 +1758,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -1751,19 +1770,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -1773,19 +1792,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -1795,19 +1814,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -1817,19 +1836,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -1839,19 +1858,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -1861,19 +1880,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -1883,19 +1902,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -1905,38 +1924,54 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -1946,16 +1981,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -1965,16 +2000,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -1984,7 +2019,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -9508,13 +9543,12 @@ "license": "MIT" }, "node_modules/link-preview-js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/link-preview-js/-/link-preview-js-3.2.0.tgz", - "integrity": "sha512-FvrLltjOPGbTzt+RugbzM7g8XuUNLPO2U/INSLczrYdAA32E7nZVUrVL1gr61DGOArGJA2QkPGMEvNMLLsXREA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/link-preview-js/-/link-preview-js-4.0.4.tgz", + "integrity": "sha512-hJ2UlkFtwCihTtx8y5zBH6iuMYwz+W5v06GY7owDfFvTdAtMZMBfKzg7TjKegL+ehHw4CDIzZClJNKhwAD6Q0Q==", "license": "MIT", "dependencies": { - "cheerio": "1.0.0-rc.11", - "url": "0.11.0" + "cheerio": "1.0.0-rc.11" }, "engines": { "node": ">=18" @@ -11726,12 +11760,6 @@ "node": ">=10" } }, - "node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", - "license": "MIT" - }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -12014,15 +12042,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -12539,9 +12558,9 @@ "license": "BlueOak-1.0.0" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -12686,47 +12705,52 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/shebang-command": { @@ -14528,16 +14552,6 @@ "node": ">=6" } }, - "node_modules/url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", - "license": "MIT", - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", diff --git a/package.json b/package.json index 88defa8276..6288a19562 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "jsonwebtoken": "^9.0.2", "kafkajs": "^2.2.4", "libphonenumber-js": "^1.12.25", - "link-preview-js": "^3.0.13", + "link-preview-js": "^4.0.4", "long": "^5.2.3", "mediainfo.js": "^0.3.4", "mime": "^4.0.0", @@ -124,7 +124,7 @@ "qrcode-terminal": "^0.12.0", "redis": "^4.7.0", "rxjs": "^7.8.2", - "sharp": "^0.34.2", + "sharp": "^0.35.5", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "socks-proxy-agent": "^8.0.5", @@ -168,6 +168,7 @@ "axios": "^1.20.0" }, "lodash": "^4.18.1", - "deepmerge-ts": "^8.0.0" + "deepmerge-ts": "^8.0.0", + "link-preview-js": "^4.0.4" } } diff --git a/test/patches/sharpImageProcessing.test.ts b/test/patches/sharpImageProcessing.test.ts new file mode 100644 index 0000000000..a7cc669bf2 --- /dev/null +++ b/test/patches/sharpImageProcessing.test.ts @@ -0,0 +1,54 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { extractImageThumb } from 'baileys'; +import sharp from 'sharp'; +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('baileys', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + prepareWAMessageMedia: vi.fn(async (content: Record) => ({ + [`${Object.keys(content)[0]}Message`]: { input: Object.values(content)[0] }, + })), + }; +}); + +const png = (width: number, height: number) => + sharp({ create: { width, height, channels: 3, background: { r: 200, g: 30, b: 30 } } }) + .png() + .toBuffer(); + +const buildService = () => { + const service = Object.create(BaileysStartupService.prototype) as any; + service.client = { waUploadToServer: vi.fn() }; + service.instance = { wuid: '5491100000000@s.whatsapp.net' }; + service.logger = { verbose: vi.fn(), error: vi.fn() }; + return service; +}; + +describe('image processing on the bundled sharp', () => { + it('converts images sent as media to jpeg', async () => { + const source = await png(40, 20); + + const prepared = await buildService().prepareMediaMessage({ + mediatype: 'image', + media: source.toString('base64'), + }); + const converted = await sharp(prepared.message.imageMessage.input).metadata(); + + expect(converted).toMatchObject({ format: 'jpeg', width: 40, height: 20 }); + }); + + it('converts sticker images to webp', async () => { + const webp = await buildService().convertToWebP((await png(30, 30)).toString('base64')); + + expect(await sharp(webp).metadata()).toMatchObject({ format: 'webp', width: 30, height: 30 }); + }); + + it('builds the Baileys thumbnail with sharp', async () => { + const thumb = await extractImageThumb(await png(200, 100), 32); + + expect(thumb.original).toEqual({ width: 200, height: 100 }); + expect(await sharp(thumb.buffer).metadata()).toMatchObject({ format: 'jpeg', width: 32 }); + }); +}); From 543a6992c45867e1b9a394efc7a074907b060341 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 22:27:52 -0300 Subject: [PATCH 68/96] build(deps): upgrade node-cron to 4 node-cron 3 depends on uuid 8.3.2 (GHSA for v3/v5/v6 buffer bounds). node-cron 4 has no dependencies and ships its own types, so @types/node-cron is removed. The only use, the Chatwoot lost-message sync, keeps schedule/start/stop with the same semantics: start is idempotent and stop halts further runs. --- package-lock.json | 30 +++++------------------------- package.json | 3 +-- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d0be6323b..6a991fe527 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,7 @@ "multer": "^2.0.2", "nats": "^2.29.1", "node-cache": "^5.1.2", - "node-cron": "^3.0.3", + "node-cron": "^4.6.0", "openai": "^4.77.3", "patch-package": "^8.0.1", "pg": "^8.13.1", @@ -81,7 +81,6 @@ "@types/mime": "^4.0.0", "@types/mime-types": "^2.1.4", "@types/node": "^24.5.2", - "@types/node-cron": "^3.0.11", "@types/qrcode": "^1.5.5", "@types/qrcode-terminal": "^0.12.2", "@typescript-eslint/eslint-plugin": "^8.44.0", @@ -3657,13 +3656,6 @@ "undici-types": "~7.16.0" } }, - "node_modules/@types/node-cron": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", - "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node-fetch": { "version": "2.6.13", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", @@ -10382,24 +10374,12 @@ } }, "node_modules/node-cron": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", - "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.6.0.tgz", + "integrity": "sha512-Si/bzYiKRHOB8/a99T2+SDGN582ONDMSTlJr5oCkT6GtnqPjZ2s10eoQRYkW9ZHwjVxONL+W8Fb+qR0AHMQsdg==", "license": "ISC", - "dependencies": { - "uuid": "8.3.2" - }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/node-cron/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "node": ">=20" } }, "node_modules/node-domexception": { diff --git a/package.json b/package.json index 6288a19562..460a38a03f 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "multer": "^2.0.2", "nats": "^2.29.1", "node-cache": "^5.1.2", - "node-cron": "^3.0.3", + "node-cron": "^4.6.0", "openai": "^4.77.3", "patch-package": "^8.0.1", "pg": "^8.13.1", @@ -142,7 +142,6 @@ "@types/mime": "^4.0.0", "@types/mime-types": "^2.1.4", "@types/node": "^24.5.2", - "@types/node-cron": "^3.0.11", "@types/qrcode": "^1.5.5", "@types/qrcode-terminal": "^0.12.2", "@typescript-eslint/eslint-plugin": "^8.44.0", From 9839112afae23d5033ea8b3cd70a36dcb1c399a6 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 22:29:17 -0300 Subject: [PATCH 69/96] build(deps): run jimp and openai on zod 4 zod below 4.6.6 can exhaust memory validating oversized constrained collections (GHSA-mrhx-c7xc-m3w5) and zod 3 has no fix. jimp 1.6 (via Baileys and the Chatwoot ads thumbnail) only uses object, number, tuple, union, literal, nativeEnum, boolean and string schemas, all kept by zod 4; openai 4 only needs zod for its unused structured-output helpers. An override moves both to zod 4.6.7. Tests cover jimp cover, resize, jpeg/png encoding and option validation, passing on zod 3 and 4. --- package-lock.json | 6 ++--- package.json | 3 ++- test/patches/jimpImageProcessing.test.ts | 34 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 test/patches/jimpImageProcessing.test.ts diff --git a/package-lock.json b/package-lock.json index 6a991fe527..5af21c86b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15218,9 +15218,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 460a38a03f..b4933d9481 100644 --- a/package.json +++ b/package.json @@ -168,6 +168,7 @@ }, "lodash": "^4.18.1", "deepmerge-ts": "^8.0.0", - "link-preview-js": "^4.0.4" + "link-preview-js": "^4.0.4", + "zod": "^4.6.7" } } diff --git a/test/patches/jimpImageProcessing.test.ts b/test/patches/jimpImageProcessing.test.ts new file mode 100644 index 0000000000..16b6275a76 --- /dev/null +++ b/test/patches/jimpImageProcessing.test.ts @@ -0,0 +1,34 @@ +import { Jimp, JimpMime } from 'jimp'; +import sharp from 'sharp'; +import { describe, expect, it } from 'vitest'; + +const png = (width: number, height: number) => + sharp({ create: { width, height, channels: 3, background: { r: 10, g: 120, b: 200 } } }) + .png() + .toBuffer(); + +describe('jimp image processing', () => { + it('covers an image to the Chatwoot ads thumbnail size', async () => { + const image = await Jimp.read(await png(640, 480)); + + image.cover({ w: 320, h: 180 }); + const output = await sharp(await image.getBuffer(JimpMime.png)).metadata(); + + expect(output).toMatchObject({ format: 'png', width: 320, height: 180 }); + }); + + it('resizes and encodes jpeg like the Baileys fallback thumbnail', async () => { + const image = await Jimp.read(await png(200, 100)); + + image.resize({ w: 32 }); + const output = await sharp(await image.getBuffer(JimpMime.jpeg, { quality: 50 })).metadata(); + + expect(output).toMatchObject({ format: 'jpeg', width: 32, height: 16 }); + }); + + it('rejects invalid plugin options through its schema validation', async () => { + const image = await Jimp.read(await png(10, 10)); + + expect(() => image.resize({ w: 'wide' } as any)).toThrow(); + }); +}); From 792e3606c38b7f3884bd1756b1970c04bf1f70df Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 22:34:07 -0300 Subject: [PATCH 70/96] build(docker): run the image without npm The npm CLI bundled in the Node base image carries ip-address 10.7.2 (CVE-2026-111997) and no npm release ships a fixed one. The image only used npm to start: the entrypoint and the migration script now run package.json scripts through a small run_package_script helper with node_modules/.bin on PATH, scripts call prisma instead of npx prisma, the entrypoint execs node dist/main, and the final stage removes npm and npx. Migrations and prisma generate still run on start. --- Docker/scripts/deploy_database.sh | 8 ++++++-- Dockerfile | 4 ++-- package.json | 12 ++++++------ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Docker/scripts/deploy_database.sh b/Docker/scripts/deploy_database.sh index fea58ff850..7c00117c57 100755 --- a/Docker/scripts/deploy_database.sh +++ b/Docker/scripts/deploy_database.sh @@ -2,6 +2,10 @@ source ./Docker/scripts/env_functions.sh +run_package_script() { + PATH="$PWD/node_modules/.bin:$PATH" bash -c "$(node -p "require('./package.json').scripts['$1']")" +} + if [ "$DOCKER_ENV" != "true" ]; then export_env_vars fi @@ -12,14 +16,14 @@ if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" || echo "Database URL: $DATABASE_URL" # rm -rf ./prisma/migrations # cp -r ./prisma/$DATABASE_PROVIDER-migrations ./prisma/migrations - npm run db:deploy + run_package_script db:deploy if [ $? -ne 0 ]; then echo "Migration failed" exit 1 else echo "Migration succeeded" fi - npm run db:generate + run_package_script db:generate if [ $? -ne 0 ]; then echo "Prisma generate failed" exit 1 diff --git a/Dockerfile b/Dockerfile index b87bb3013d..1ab6676947 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ FROM node:24.22.1-alpine3.24@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c RUN apk update && \ apk add tzdata ffmpeg bash openssl -RUN npm install -g npm@11.21.1 +RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx ENV TZ=America/Sao_Paulo ENV DOCKER_ENV=true @@ -61,4 +61,4 @@ ENV DOCKER_ENV=true EXPOSE 8080 -ENTRYPOINT ["/bin/bash", "-c", ". ./Docker/scripts/deploy_database.sh && npm run start:prod" ] +ENTRYPOINT ["/bin/bash", "-c", ". ./Docker/scripts/deploy_database.sh && exec node dist/main" ] diff --git a/package.json b/package.json index b4933d9481..7bdff67eac 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,12 @@ "typecheck:test": "tsc -p tsconfig.test.json --noEmit", "commit": "cz", "commitlint": "commitlint --edit", - "db:generate": "node runWithProvider.js \"npx prisma generate --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", - "db:deploy": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate deploy --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", - "db:deploy:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate deploy --schema prisma\\DATABASE_PROVIDER-schema.prisma\"", - "db:studio": "node runWithProvider.js \"npx prisma studio --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", - "db:migrate:dev": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate dev --schema ./prisma/DATABASE_PROVIDER-schema.prisma && cp -r ./prisma/migrations/* ./prisma/DATABASE_PROVIDER-migrations\"", - "db:migrate:dev:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate dev --schema prisma\\DATABASE_PROVIDER-schema.prisma\"", + "db:generate": "node runWithProvider.js \"prisma generate --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", + "db:deploy": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && prisma migrate deploy --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", + "db:deploy:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && prisma migrate deploy --schema prisma\\DATABASE_PROVIDER-schema.prisma\"", + "db:studio": "node runWithProvider.js \"prisma studio --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", + "db:migrate:dev": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && prisma migrate dev --schema ./prisma/DATABASE_PROVIDER-schema.prisma && cp -r ./prisma/migrations/* ./prisma/DATABASE_PROVIDER-migrations\"", + "db:migrate:dev:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && prisma migrate dev --schema prisma\\DATABASE_PROVIDER-schema.prisma\"", "prepare": "husky", "postinstall": "patch-package", "test:watch": "vitest" From 5ecfbb9a6ec90e8cb553594920a0f813c9c40a40 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 23:00:13 -0300 Subject: [PATCH 71/96] build(deps): move minio to patched query-string 10 and stream-json 3 minio 8.0.7 is the latest release and pins query-string 7 (decode-uri-component 0.2.2, CVE-2026-45822) and stream-json 1 (three advisories). The fixed versions are ESM-only and changed their API, so overrides alone break minio: query-string's namespace lost stringify and stream-json renamed jsonl/Parser.js to jsonl/parser.js, which only resolves on case-insensitive file systems. A minio patch (CommonJS and ESM builds) takes query-string's default export and uses stream-json 3's jsonl parser asStream, which emits the same {key, value} records. Tests cover the exact-case require, presigned URL signing on both builds and bucket notification parsing; against a SigV4-validating S3 server, makeBucket, listBuckets, setBucketPolicy, putObject, presigned GET (non-ASCII names), removeObject and a tampered signature behave as before. --- package-lock.json | 76 +++++++++------------ package.json | 6 +- patches/minio+8.0.7.patch | 95 ++++++++++++++++++++++++++ test/patches/minioDependencies.test.ts | 62 +++++++++++++++++ 4 files changed, 196 insertions(+), 43 deletions(-) create mode 100644 patches/minio+8.0.7.patch create mode 100644 test/patches/minioDependencies.test.ts diff --git a/package-lock.json b/package-lock.json index 5af21c86b8..c88ce0f581 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5813,15 +5813,6 @@ "node": ">=0.10.0" } }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -7493,12 +7484,15 @@ } }, "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-6.1.0.tgz", + "integrity": "sha512-xdMtCAODmPloU9qtmPcdBV9Kd27NtMse+4ayThxqIHUES5Z2S6bGpap5PpdmNM56ub7y3i1eyr+vJJIIgWGKmA==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/finalhandler": { @@ -12005,18 +11999,16 @@ } }, "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-10.0.0.tgz", + "integrity": "sha512-QYodx/Ywk8lCcOmsyupXVN91yWPAdjq0xVqE2FE2BHFYnma71C8txR8JCBptjzoUpkfAK7Qjua9VjYRTru4LYg==", "license": "MIT", "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" + "filter-obj": "^6.1.0", + "split-on-first": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=22" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -13047,12 +13039,15 @@ } }, "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-4.0.0.tgz", + "integrity": "sha512-pyrgNqiEBHA9KlEtg9quYGagtrlTyukL3ryQ+RmjRn9SYmcNQmfa+sWIsI+hlcL4WrmFdrt7IfM6o8ktBAooDQ==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/split2": { @@ -13102,18 +13097,24 @@ } }, "node_modules/stream-chain": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", - "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", - "license": "BSD-3-Clause" + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-4.2.6.tgz", + "integrity": "sha512-1zeJ8CrtJfmiba26ui8jXkq/xLRFvhzkdH02D5QLO9Cnovgeb28IJxg88DdraWnjnkbOol/++uIAybJbJhk7ig==", + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/sponsors/uhop" + } }, "node_modules/stream-json": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", - "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-3.7.0.tgz", + "integrity": "sha512-rCSBdcBP/bPk6T8QFcxAj1MSzAuc5i49cYW6IE7sYObNEPccBJIUiL6fU9c9BWt40aJK0siVynLX7lWaW8alXw==", "license": "BSD-3-Clause", "dependencies": { - "stream-chain": "^2.2.5" + "stream-chain": "^4.2.5" + }, + "funding": { + "url": "https://github.com/sponsors/uhop" } }, "node_modules/streamsearch": { @@ -13124,15 +13125,6 @@ "node": ">=10.0.0" } }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", diff --git a/package.json b/package.json index 7bdff67eac..0b4a6e0ea8 100644 --- a/package.json +++ b/package.json @@ -169,6 +169,10 @@ "lodash": "^4.18.1", "deepmerge-ts": "^8.0.0", "link-preview-js": "^4.0.4", - "zod": "^4.6.7" + "zod": "^4.6.7", + "minio": { + "query-string": "^10.0.0", + "stream-json": "^3.7.0" + } } } diff --git a/patches/minio+8.0.7.patch b/patches/minio+8.0.7.patch new file mode 100644 index 0000000000..ac0d0a0b47 --- /dev/null +++ b/patches/minio+8.0.7.patch @@ -0,0 +1,95 @@ +diff --git a/node_modules/minio/dist/esm/helpers.mjs b/node_modules/minio/dist/esm/helpers.mjs +index edac39c..8e547bc 100644 +--- a/node_modules/minio/dist/esm/helpers.mjs ++++ b/node_modules/minio/dist/esm/helpers.mjs +@@ -1,6 +1,6 @@ + import * as fs from "fs"; + import * as path from "path"; +-import * as querystring from 'query-string'; ++import querystring from 'query-string'; + import * as errors from "./errors.mjs"; + import { getEncryptionHeaders, isEmpty, isEmptyObject, isNumber, isObject, isString, isValidBucketName, isValidObjectName } from "./internal/helper.mjs"; + import { RETENTION_MODES } from "./internal/type.mjs"; +diff --git a/node_modules/minio/dist/esm/internal/client.mjs b/node_modules/minio/dist/esm/internal/client.mjs +index e9f9669..17c4c8d 100644 +--- a/node_modules/minio/dist/esm/internal/client.mjs ++++ b/node_modules/minio/dist/esm/internal/client.mjs +@@ -8,7 +8,7 @@ import * as async from 'async'; + import BlockStream2 from 'block-stream2'; + import { isBrowser } from 'browser-or-node'; + import _ from 'lodash'; +-import * as qs from 'query-string'; ++import qs from 'query-string'; + import xml2js from 'xml2js'; + import { CredentialProvider } from "../CredentialProvider.mjs"; + import * as errors from "../errors.mjs"; +diff --git a/node_modules/minio/dist/esm/notification.mjs b/node_modules/minio/dist/esm/notification.mjs +index 2fe075b..a07da8d 100644 +--- a/node_modules/minio/dist/esm/notification.mjs ++++ b/node_modules/minio/dist/esm/notification.mjs +@@ -15,7 +15,7 @@ + */ + + import { EventEmitter } from 'eventemitter3'; +-import jsonLineParser from 'stream-json/jsonl/Parser.js'; ++import jsonLineParser from 'stream-json/jsonl/parser.js'; + import { DEFAULT_REGION } from "./helpers.mjs"; + import { pipesetup, uriEscape } from "./internal/helper.mjs"; + +@@ -176,7 +176,7 @@ export class NotificationPoller extends EventEmitter { + bucketName: this.bucketName, + query + }, '', [200], region).then(response => { +- const asm = jsonLineParser.make(); ++ const asm = jsonLineParser.asStream(); + pipesetup(response, asm).on('data', data => { + // Data is flushed periodically (every 5 seconds), so we should + // handle it after flushing from the JSON parser. +diff --git a/node_modules/minio/dist/main/helpers.js b/node_modules/minio/dist/main/helpers.js +index bea5b04..d19b46c 100644 +--- a/node_modules/minio/dist/main/helpers.js ++++ b/node_modules/minio/dist/main/helpers.js +@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { + exports.removeDirAndFiles = removeDirAndFiles; + var fs = _interopRequireWildcard(require("fs"), true); + var path = _interopRequireWildcard(require("path"), true); +-var querystring = _interopRequireWildcard(require("query-string"), true); ++var querystring = require("query-string").default; + var errors = _interopRequireWildcard(require("./errors.js"), true); + var _helper = require("./internal/helper.js"); + var _type = require("./internal/type.js"); +diff --git a/node_modules/minio/dist/main/internal/client.js b/node_modules/minio/dist/main/internal/client.js +index 5ab16ff..306b586 100644 +--- a/node_modules/minio/dist/main/internal/client.js ++++ b/node_modules/minio/dist/main/internal/client.js +@@ -13,7 +13,7 @@ var async = _interopRequireWildcard(require("async"), true); + var _blockStream = require("block-stream2"); + var _browserOrNode = require("browser-or-node"); + var _lodash = require("lodash"); +-var qs = _interopRequireWildcard(require("query-string"), true); ++var qs = require("query-string").default; + var _xml2js = require("xml2js"); + var _CredentialProvider = require("../CredentialProvider.js"); + var errors = _interopRequireWildcard(require("../errors.js"), true); +diff --git a/node_modules/minio/dist/main/notification.js b/node_modules/minio/dist/main/notification.js +index 8d8e8b3..6fe6e90 100644 +--- a/node_modules/minio/dist/main/notification.js ++++ b/node_modules/minio/dist/main/notification.js +@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { + value: true + }); + var _eventemitter = require("eventemitter3"); +-var _Parser = require("stream-json/jsonl/Parser.js"); ++var _jsonlParser = require("stream-json/jsonl/parser.js").default; + var _helpers = require("./helpers.js"); + var _helper = require("./internal/helper.js"); + /* +@@ -196,7 +196,7 @@ class NotificationPoller extends _eventemitter.EventEmitter { + bucketName: this.bucketName, + query + }, '', [200], region).then(response => { +- const asm = _Parser.make(); ++ const asm = _jsonlParser.asStream(); + (0, _helper.pipesetup)(response, asm).on('data', data => { + // Data is flushed periodically (every 5 seconds), so we should + // handle it after flushing from the JSON parser. diff --git a/test/patches/minioDependencies.test.ts b/test/patches/minioDependencies.test.ts new file mode 100644 index 0000000000..d15f28e44c --- /dev/null +++ b/test/patches/minioDependencies.test.ts @@ -0,0 +1,62 @@ +import { readdirSync, readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { basename, dirname } from 'node:path'; +import { Readable } from 'node:stream'; + +import * as Minio from 'minio'; +import { describe, expect, it, vi } from 'vitest'; + +const requireFromMinio = createRequire(createRequire(__filename).resolve('minio')); +const { NotificationPoller } = requireFromMinio('./notification.js'); + +describe('minio on its patched dependencies', () => { + it('requires the stream-json parser with the exact file name case', () => { + const source = readFileSync(requireFromMinio.resolve('./notification.js'), 'utf8'); + const [, specifier] = source.match(/require\("(stream-json[^"]*)"\)/); + const resolved = requireFromMinio.resolve(specifier); + + expect(readdirSync(dirname(resolved))).toContain(basename(resolved)); + }); + + it.each([ + ['commonjs', () => requireFromMinio('./minio.js').Client], + ['esm', () => Minio.Client], + ])('signs presigned urls with response header parameters (%s build)', async (_build, loadClient) => { + const Client = loadClient(); + const client = new Client({ + endPoint: 's3.example.com', + accessKey: 'access', + secretKey: 'secret', + region: 'us-east-1', + }); + + const url = new URL( + await client.presignedGetObject('media', 'photo.png', 60, { 'response-content-type': 'image/png' }, new Date(0)), + ); + + expect(url.searchParams.get('response-content-type')).toBe('image/png'); + expect(url.searchParams.get('X-Amz-Signature')).toMatch(/^[0-9a-f]{64}$/); + expect(url.searchParams.get('X-Amz-Credential')).toBe('access/19700101/us-east-1/s3/aws4_request'); + }); + + it('emits bucket notifications parsed from the json lines stream', async () => { + const records = [{ s3: { object: { key: 'a.png' } } }, { s3: { object: { key: 'b.png' } } }]; + const body = `${JSON.stringify({ Records: [records[0]] })}\n${JSON.stringify({ Records: [records[1]] })}\n`; + const client = { region: 'us-east-1', makeRequestAsync: vi.fn(async () => Readable.from([body])) }; + const poller = new NotificationPoller(client, 'media', '', '', ['s3:ObjectCreated:*']); + const received: unknown[] = []; + + await new Promise((resolve) => { + poller.on('notification', (record) => { + received.push(record); + if (received.length === records.length) { + poller.stop(); + resolve(); + } + }); + poller.start(); + }); + + expect(received).toEqual(records); + }); +}); From f85bf7559bfba57feb6e7d41dcc445e60fcf810b Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 23:14:47 -0300 Subject: [PATCH 72/96] build(deps): apply semver-compatible fixes to development dependencies npm audit fix updates eslint, @typescript-eslint, commitlint, tsup/rollup, lint-staged/picomatch and commitizen/tmp transitive packages within their ranges (lockfile only), clearing 12 of the 15 development findings. Lint, type checks, build, commitlint and the suite run unchanged; production dependencies stay at 0 findings. --- package-lock.json | 505 +++++++++++++++++++++++++++------------------- 1 file changed, 292 insertions(+), 213 deletions(-) diff --git a/package-lock.json b/package-lock.json index c88ce0f581..c94442af64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1245,9 +1245,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1262,9 +1262,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -1290,9 +1290,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1479,9 +1479,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -1490,9 +1490,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2024,6 +2024,45 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@jimp/core": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.1.tgz", @@ -2509,6 +2548,23 @@ "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", "license": "MIT" }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -2909,9 +2965,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.65.0.tgz", + "integrity": "sha512-t2AnVba25baFdq7gczzS13JyVcdubLVbFX1X04mxrggDrTQPUXNv3XM0k9H1hr5tqvg7AVvtQ720QHAj0iYqhg==", "cpu": [ "arm" ], @@ -2923,9 +2979,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.65.0.tgz", + "integrity": "sha512-dNONUaEwPGhl/nuO2KV8VABWlcjYyU6KtZGN8dQv5OS5JU7JSCxPijVNKXbRJd0iuTzTiJ4TjPt/4bbvDRNiMQ==", "cpu": [ "arm64" ], @@ -2937,9 +2993,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.65.0.tgz", + "integrity": "sha512-zWQqUH+9zavr2Bxuzfpuv2SIF3X31cr58TvSQC9o1GhpOvkqRwr60GkZdQcudyT/BEgdpdSafjgd2I13MbTL7g==", "cpu": [ "arm64" ], @@ -2951,9 +3007,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.65.0.tgz", + "integrity": "sha512-RQYreBFWtO4//hIuOaZkYUnF/lPU4lz35dveDimDshWCJvVisVtJ/shmT9bkXK8wFJY+FJF89X6F3Cq1qI3LCw==", "cpu": [ "x64" ], @@ -2965,9 +3021,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.65.0.tgz", + "integrity": "sha512-A0NfMj6Cuh/9BE5k3awJKv6HoxFVXIcLAb6SZqAMoxUHPA1ZNNK4RsGuUPD/vL7nEAm9LG+ZzXWWTrnC0XtkpA==", "cpu": [ "arm64" ], @@ -2979,9 +3035,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.65.0.tgz", + "integrity": "sha512-BI6/H7q3aP0I7S7GOrjyxTjgEJlSnovXE38+bdxBpuKN6z+d//uF+T+VQ3Q7ltJL/71p17ufartARX9Ja5gOGQ==", "cpu": [ "x64" ], @@ -2993,9 +3049,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.65.0.tgz", + "integrity": "sha512-W1vUprf09RWaVWnsFP1vlUkEWJzTZlpObOEAYZXt9Fq0UVAOYEkeGr7wBX7McKqqIQskXdBnpiOMgwPujordgA==", "cpu": [ "arm" ], @@ -3007,9 +3063,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.65.0.tgz", + "integrity": "sha512-s8QTcZWgrl9haFzEGIqyE5BWyo89pFDGLMBFZ752WYxk8DJwNU/O1X6pZgPw829d9/sm6CCHyrMP3aurt76p1Q==", "cpu": [ "arm" ], @@ -3021,9 +3077,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.65.0.tgz", + "integrity": "sha512-ke1f6dLRzkqveyjjF2fkEwQYZvxHtNNwjegY8nJrwcO0Bdqr8CzFO/pKE0Bns13cx0qz2Ba2Y0Hs3dSwMdnHxA==", "cpu": [ "arm64" ], @@ -3035,9 +3091,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.65.0.tgz", + "integrity": "sha512-926SMeUcy4y18Rz/w2aJiUKOnyAnm6czg72bXkQEaeemsll+Wr0hnsjb2BGRpHvWgwAMYy5ACJCotbvfLCfj0A==", "cpu": [ "arm64" ], @@ -3049,9 +3105,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.65.0.tgz", + "integrity": "sha512-6T7JNRP1hc4hqF2dUsEoVLOt9K8tmODoCLldtQRYnrddGGQY8Gqe2pM/qMMUo7EMowpP7oa2N95k0/8sYK/ipw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.65.0.tgz", + "integrity": "sha512-SImGFl1Je5kpyAgOCPONVT7h6ASV8mAUwGRjY7eZvVjPg//PfHOXgI+P241RF/+HqsA4PP3qD527ysWFKNQyhQ==", "cpu": [ "loong64" ], @@ -3063,9 +3133,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.65.0.tgz", + "integrity": "sha512-XeDTbmPF1Yu5asfXGPrMkWoxTDhgryQbmw5Q1+R582O51VNCk23J6f2pDrsAp5YkneBVBwtExx6ZGOb9D2aQ6A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.65.0.tgz", + "integrity": "sha512-VS55JUI/1nsljmV/jfqNkLZXFw8uMO8nNlfoHmSevNf6fO3neuhyDvx9dv3c/Ytt8r/431SL8Qax1dpYvOMZTg==", "cpu": [ "ppc64" ], @@ -3077,9 +3161,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.65.0.tgz", + "integrity": "sha512-Vblw84TbX7RS4la1Y4+yQ7aore/oSyaNtFbppuBr5Vs/4LfBeXKq9dIfoExw6zOvRERqd2rRAjnZRaMhH/0x5w==", "cpu": [ "riscv64" ], @@ -3091,9 +3175,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.65.0.tgz", + "integrity": "sha512-7+gHjtONvKgwR9InTKFaLq53f6YzBJkgeMRBENdWTsQr+spahGqQm8HF5aXpq8i4JaRrOqYyv6Zn1WltDTxaZQ==", "cpu": [ "riscv64" ], @@ -3105,9 +3189,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.65.0.tgz", + "integrity": "sha512-KvNDC0J6s4vVHhtJ6KrDXU0Er0au/hmRIBq/+1maCV8TseGupeKizq0bWcrbp2HE1T8xoknvN0Q/KZ/YF0qX3w==", "cpu": [ "s390x" ], @@ -3119,9 +3203,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.65.0.tgz", + "integrity": "sha512-GExdD15WPvu/x/drELjVam7vX8EsPgvTMOa/iJL/p/4m/oOqzbb7JXZg8fXR4hlXtg/sF2zyTZ3bR0AEtEMlhQ==", "cpu": [ "x64" ], @@ -3133,9 +3217,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.65.0.tgz", + "integrity": "sha512-8sT9PemIZfIMED4Vd4KGoOXVDj5CMSWNcGdbheRUx9TYS03yOWdT2Q0lXPyEyNBB9YTilwewIQKNIBk6Nxi0Kg==", "cpu": [ "x64" ], @@ -3146,10 +3230,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.65.0.tgz", + "integrity": "sha512-pO6Ehi0jo5yE0R6PltKhJhv9mlIiyzBJk5N5NUF2OF6xtVKI+1bdXxj/rb0jGXy3hck1ELVT0QPY3tzkAm360w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.65.0.tgz", + "integrity": "sha512-vo7N3/+cjjPDYr8vtxC+qSNgWa9sDvJ7g8N8R5SWqFY4WiNH1ULcAU70SKVyHeiIZOYYwNP9pTipmhN5sl1NPw==", "cpu": [ "arm64" ], @@ -3161,9 +3259,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.65.0.tgz", + "integrity": "sha512-ArwvgFAbjKcDM7qqotZ4OVYf+UMT9xH2YzkCzK4+AuZSDihMlz1Ekoe4INwBVXzn2mnJzuIYOcSx07iTBijGRQ==", "cpu": [ "arm64" ], @@ -3175,9 +3273,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.65.0.tgz", + "integrity": "sha512-QGMmK4/cxKE/36FoLMSHQBCyymv/eFTnsCvvsBF0rgmOvVbvX78VWZRerL3dHa77f4pM6Xcc2iGDhJ10w/svKQ==", "cpu": [ "ia32" ], @@ -3189,9 +3287,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.65.0.tgz", + "integrity": "sha512-XfKYFWHIPs4HPODIMsGF1IKlS3H1e3WokUZIgpmn4+uNnZBlnRDDHEylNyazbQXwhMKZre68OjtRYbKqb5+j8w==", "cpu": [ "x64" ], @@ -3203,9 +3301,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.65.0.tgz", + "integrity": "sha512-wPWi++K4Bs1VR4XihOhfGi4IGX6Cub7FlerTf8X2v+2BtDHddgSqWCXwGD+h2yAe4/niIhNDwAltioOC+wEogQ==", "cpu": [ "x64" ], @@ -3576,9 +3674,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -4251,9 +4349,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -5016,9 +5114,9 @@ } }, "node_modules/cachedir": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", - "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", "dev": true, "license": "MIT", "engines": { @@ -5122,9 +5220,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", "dev": true, "license": "MIT" }, @@ -5338,13 +5436,13 @@ } }, "node_modules/commitizen": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.3.1.tgz", - "integrity": "sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.3.2.tgz", + "integrity": "sha512-1Zs37z9JPvAcuTSSricZZwBhOPVNNxJouuY4yDEt+eD70EoxT2TU9kViG8CuB/PmVg2G4XsAGQiK4YCst97aDQ==", "dev": true, "license": "MIT", "dependencies": { - "cachedir": "2.3.0", + "cachedir": "2.4.0", "cz-conventional-changelog": "3.3.0", "dedent": "0.7.0", "detect-indent": "6.1.0", @@ -5352,10 +5450,10 @@ "find-root": "1.1.0", "fs-extra": "9.1.0", "glob": "7.2.3", - "inquirer": "8.2.5", + "inquirer": "8.2.7", "is-utf8": "^0.2.1", - "lodash": "4.17.21", - "minimist": "1.2.7", + "lodash": "4.18.1", + "minimist": "1.2.8", "strip-bom": "4.0.0", "strip-json-comments": "3.1.1" }, @@ -5365,17 +5463,7 @@ "git-cz": "bin/git-cz" }, "engines": { - "node": ">= 12" - } - }, - "node_modules/commitizen/node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 18" } }, "node_modules/compare-func": { @@ -6650,9 +6738,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -6697,9 +6785,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6814,9 +6902,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6847,9 +6935,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -6955,9 +7043,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7243,21 +7331,6 @@ "integrity": "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==", "license": "MIT" }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", @@ -7339,9 +7412,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "dev": true, "funding": [ { @@ -7627,9 +7700,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -8015,9 +8088,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -8026,9 +8099,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -8535,17 +8608,17 @@ } }, "node_modules/inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", + "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", "dev": true, "license": "MIT", "dependencies": { + "@inquirer/external-editor": "^1.0.0", "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", - "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", @@ -8555,7 +8628,7 @@ "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", - "wrap-ansi": "^7.0.0" + "wrap-ansi": "^6.0.1" }, "engines": { "node": ">=12.0.0" @@ -8637,6 +8710,21 @@ "node": ">=8" } }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -9273,10 +9361,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10846,16 +10944,6 @@ "node": ">=8" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -12349,13 +12437,13 @@ } }, "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.65.0.tgz", + "integrity": "sha512-QVCvQUTGDN6JRwi3jVGCY3H4aTHvi9rX0LyoBMkGUvdh/Z6O51CzcLz85BWZaTCDGhl6ySGqYZs+xI1jmPqGcw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -12365,28 +12453,32 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.65.0", + "@rollup/rollup-android-arm64": "4.65.0", + "@rollup/rollup-darwin-arm64": "4.65.0", + "@rollup/rollup-darwin-x64": "4.65.0", + "@rollup/rollup-freebsd-arm64": "4.65.0", + "@rollup/rollup-freebsd-x64": "4.65.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.65.0", + "@rollup/rollup-linux-arm-musleabihf": "4.65.0", + "@rollup/rollup-linux-arm64-gnu": "4.65.0", + "@rollup/rollup-linux-arm64-musl": "4.65.0", + "@rollup/rollup-linux-loong64-gnu": "4.65.0", + "@rollup/rollup-linux-loong64-musl": "4.65.0", + "@rollup/rollup-linux-ppc64-gnu": "4.65.0", + "@rollup/rollup-linux-ppc64-musl": "4.65.0", + "@rollup/rollup-linux-riscv64-gnu": "4.65.0", + "@rollup/rollup-linux-riscv64-musl": "4.65.0", + "@rollup/rollup-linux-s390x-gnu": "4.65.0", + "@rollup/rollup-linux-x64-gnu": "4.65.0", + "@rollup/rollup-linux-x64-musl": "4.65.0", + "@rollup/rollup-openbsd-x64": "4.65.0", + "@rollup/rollup-openharmony-arm64": "4.65.0", + "@rollup/rollup-win32-arm64-msvc": "4.65.0", + "@rollup/rollup-win32-ia32-msvc": "4.65.0", + "@rollup/rollup-win32-x64-gnu": "4.65.0", + "@rollup/rollup-win32-x64-msvc": "4.65.0", "fsevents": "~2.3.2" } }, @@ -13543,9 +13635,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -13585,19 +13677,6 @@ "node": ">=14.0.0" } }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", From c71a389ff319f879b7f7d7085440ffcc0ea37595 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 23:16:41 -0300 Subject: [PATCH 73/96] build(deps): upgrade vitest to 5 vitest 3 carries tinypool 1.1.1 (two critical advisories) and an affected @vitest/mocker; the fixes only ship with vitest 5. Since vitest 4, restoreMocks only restores spies and no longer resets vi.fn() call history and implementations between tests, which made order-dependent failures under the shuffled sequence; mockReset keeps the previous per-test isolation. The suite is stable across shuffled runs, and npm audit now reports 0 findings including development dependencies. --- package-lock.json | 1131 +++++++++++++++++++++++++++++++-------------- package.json | 2 +- vitest.config.ts | 1 + 3 files changed, 775 insertions(+), 359 deletions(-) diff --git a/package-lock.json b/package-lock.json index c94442af64..6caeaf3e35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,7 +99,7 @@ "tsup": "^8.3.5", "tsx": "^4.20.5", "typescript": "^5.7.2", - "vitest": "^3.2.7" + "vitest": "^5.0.5" } }, "node_modules/@adiwajshing/keyed-db": { @@ -2509,9 +2509,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -2741,6 +2741,17 @@ "node": ">=14" } }, + "node_modules/@oxc-project/types": { + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -2964,6 +2975,284 @@ "@redis/client": "^1.0.0" } }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", + "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", + "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", + "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", + "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", + "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", + "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", + "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", + "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", + "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", + "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", + "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", + "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", + "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", + "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", + "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.65.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.65.0.tgz", @@ -4098,40 +4387,24 @@ "dev": true, "license": "ISC" }, - "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", + "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.7", + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.5", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^1.4.3" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -4142,73 +4415,22 @@ } } }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", + "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, "funding": { "url": "https://opencollective.com/vitest" } @@ -5190,18 +5412,11 @@ } }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -5226,16 +5441,6 @@ "dev": true, "license": "MIT" }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/cheerio": { "version": "1.0.0-rc.11", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.11.tgz", @@ -5908,16 +6113,6 @@ "dev": true, "license": "MIT" }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6446,9 +6641,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -9549,61 +9744,334 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/kafkajs": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", - "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", - "license": "MIT", + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/keyv": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.4.tgz", + "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.29", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.29.tgz", + "integrity": "sha512-P2aLrbeqHbmh8+9P35LXQfXOKc7XJ0ymUKl7tyeyQjdRNfzunXWxQXGc4yl3fUf28fqLRfPY+vIVvFXK7KEBTw==", + "license": "MIT" + }, + "node_modules/libsignal": { + "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": "^7.5.5" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, "engines": { - "node": ">=14.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/keyv": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.4.tgz", - "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/klaw-sync": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11" + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/libphonenumber-js": { - "version": "1.12.29", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.29.tgz", - "integrity": "sha512-P2aLrbeqHbmh8+9P35LXQfXOKc7XJ0ymUKl7tyeyQjdRNfzunXWxQXGc4yl3fUf28fqLRfPY+vIVvFXK7KEBTw==", - "license": "MIT" - }, - "node_modules/libsignal": { - "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": "^7.5.5" + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lilconfig": { @@ -9921,13 +10389,6 @@ "node": ">=0.10.0" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "11.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", @@ -10406,6 +10867,7 @@ } ], "license": "MIT", + "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -10668,6 +11130,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/ogg-opus-decoder": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/ogg-opus-decoder/-/ogg-opus-decoder-1.7.3.tgz", @@ -11345,16 +11821,6 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -11593,6 +12059,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", @@ -12436,6 +12903,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", + "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.155.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.15", + "@rolldown/binding-android-arm64": "1.2.15", + "@rolldown/binding-darwin-arm64": "1.2.15", + "@rolldown/binding-darwin-x64": "1.2.15", + "@rolldown/binding-freebsd-x64": "1.2.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", + "@rolldown/binding-linux-arm64-gnu": "1.2.15", + "@rolldown/binding-linux-arm64-musl": "1.2.15", + "@rolldown/binding-linux-ppc64-gnu": "1.2.15", + "@rolldown/binding-linux-s390x-gnu": "1.2.15", + "@rolldown/binding-linux-x64-gnu": "1.2.15", + "@rolldown/binding-linux-x64-musl": "1.2.15", + "@rolldown/binding-openharmony-arm64": "1.2.15", + "@rolldown/binding-win32-arm64-msvc": "1.2.15", + "@rolldown/binding-win32-x64-msvc": "1.2.15" + } + }, "node_modules/rollup": { "version": "4.65.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.65.0.tgz", @@ -12910,13 +13412,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -13126,6 +13621,7 @@ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13151,13 +13647,6 @@ "node": ">= 10.x" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -13168,9 +13657,9 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "dev": true, "license": "MIT" }, @@ -13359,26 +13848,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/strnum": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.5.1.tgz", @@ -13578,11 +14047,14 @@ } }, "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } }, "node_modules/tinycolor2": { "version": "1.6.0", @@ -13600,14 +14072,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -13647,36 +14119,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", - "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -14669,18 +15111,18 @@ } }, "node_modules/vite": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", - "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -14696,9 +15138,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -14711,13 +15154,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -14743,53 +15189,13 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14798,65 +15204,72 @@ } }, "node_modules/vitest": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", - "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", + "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.7", - "@vitest/mocker": "3.2.7", - "@vitest/pretty-format": "^3.2.7", - "@vitest/runner": "3.2.7", - "@vitest/snapshot": "3.2.7", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" + "@vitest/mocker": "5.0.5", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.7", - "@vitest/ui": "3.2.7", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.5", + "@vitest/browser-preview": "5.0.5", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.5", + "@vitest/coverage-v8": "5.0.5", + "@vitest/ui": "5.0.5", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -14867,9 +15280,22 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, "node_modules/vitest/node_modules/picomatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", @@ -14883,13 +15309,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vitest/node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -15042,20 +15461,16 @@ } }, "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", "dev": true, "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, "bin": { "why-is-node-running": "cli.js" }, "engines": { - "node": ">=8" + "node": ">=20.11" } }, "node_modules/win-guid": { diff --git a/package.json b/package.json index 0b4a6e0ea8..b0249c9ddd 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ "tsup": "^8.3.5", "tsx": "^4.20.5", "typescript": "^5.7.2", - "vitest": "^3.2.7" + "vitest": "^5.0.5" }, "overrides": { "@figuro/chatwoot-sdk": { diff --git a/vitest.config.ts b/vitest.config.ts index e50f86fed4..0651aa82a4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -18,6 +18,7 @@ export default defineConfig({ test: { include: ['test/**/*.test.ts'], environment: 'node', + mockReset: true, restoreMocks: true, unstubEnvs: true, unstubGlobals: true, From c9896b662cf82f8e9304c1f93f26ca6b71bd91ea Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 23:38:11 -0300 Subject: [PATCH 74/96] docs(changelog): release v0.1.3 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd5895907..80998311c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # evolution-in-purple +## 0.1.3 (2026-10-24) + +### Security + +* **Image vulnerabilities**: the release image goes from 162 fixable CRITICAL/HIGH Trivy findings in 0.1.2 to **0 findings of any severity**, and `npm audit` reports 0 including development dependencies (#26, #27, #29, #30) + * The image ships only production dependencies; `tsup` moves to devDependencies (#26) + * Semver-compatible updates of production dependencies, including axios 1.20, express, undici, multer and ws (#27) + * `@figuro/chatwoot-sdk` runs on the shared axios 1.x instead of axios 0.27.2 (#27) + * sharp 0.35 and link-preview-js 4 (#27); node-cron 4; zod 4 for jimp and openai (#29) + * `minio` is patched to run on query-string 10 and stream-json 3, verified against a SigV4-validating S3 server (#29) + * The image runs without npm/npx: start scripts call node and prisma directly (#29) + * vitest 5 and semver fixes for development tooling (#30) + +### Fixes + +* **Media mimetype**: sending media with a file name without a known extension no longer sets the mimetype to the string "false"; images keep `image/jpeg` and URL media keep the downloaded content type (#28) + +### Chores + +* CI builds the image and smoke-tests it against Postgres on every PR, checking the Node.js pin and that every migration applied (#24; adapted from @nogara) +* A daily Trivy scan reports findings in the latest released image to code scanning (#25; adapted from @nogara) + +### Upgrade notes + +* `npm` and `npx` are no longer available inside the container. Use `node` and `./node_modules/.bin/prisma` for manual operations. +* `zod` is overridden to 4 for jimp and openai; Evolution's OpenAI structured-output helpers are not used. + ## 0.1.2 (2026-10-23) ### Features From 65a73b9112539e41626fb8aac346ee7b9a52289a Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 23:52:20 -0300 Subject: [PATCH 75/96] ci: fail the daily image scan on fixable critical or high findings 0.1.3 has no findings of any severity, so the report-only phase ends: the table step now exits 1 on fixable CRITICAL/HIGH findings, and the job summary is still written when it fails. --- .github/workflows/image-scan.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/image-scan.yml b/.github/workflows/image-scan.yml index ff0ccb2a18..d565bb085f 100644 --- a/.github/workflows/image-scan.yml +++ b/.github/workflows/image-scan.yml @@ -51,7 +51,7 @@ jobs: sarif_file: trivy.sarif category: image-release - - name: Summarize fixable critical and high findings + - name: Fail on fixable critical or high findings uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ steps.image.outputs.ref }} @@ -59,10 +59,11 @@ jobs: output: trivy.txt severity: CRITICAL,HIGH ignore-unfixed: true - exit-code: '0' + exit-code: '1' skip-setup-trivy: true - name: Write job summary + if: always() env: IMAGE_REF: ${{ steps.image.outputs.ref }} run: | From 9208272bdb628c01b5310de3c70a9a86086b6d65 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Fri, 23 Oct 2026 23:54:02 -0300 Subject: [PATCH 76/96] ci: keep the scan summary step working when no table was produced --- .github/workflows/image-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/image-scan.yml b/.github/workflows/image-scan.yml index d565bb085f..163345fd41 100644 --- a/.github/workflows/image-scan.yml +++ b/.github/workflows/image-scan.yml @@ -70,6 +70,6 @@ jobs: { echo "### Fixable CRITICAL/HIGH findings in ${IMAGE_REF}" echo '```' - cat trivy.txt + cat trivy.txt 2>/dev/null || echo 'The scan did not produce a findings table; see the scan step logs.' echo '```' } >> "$GITHUB_STEP_SUMMARY" From 58b927689d1539026c489637d9db82e0d0818b19 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 01:28:32 -0300 Subject: [PATCH 77/96] fix(baileys): fill remoteJidAlt for @lid history sync messages History sync delivers 1:1 messages keyed by @lid without remoteJidAlt, so consumers cannot map them to a phone number. Before persisting and emitting messaging-history.set, resolve the phone JID from payload contacts (lid/phoneNumber pairs) and then from signalRepository.lidMapping.getPNsForLIDs, called once per batch with the distinct unresolved lids. Lookup failures are logged at warn and never throw. key.remoteJid is never rewritten; remoteJidAlt is only filled when missing and only with an @s.whatsapp.net JID. Chat records are unchanged. Inspired by upstream EvolutionAPI/evolution-api#2663 by @fidelis05. --- .../whatsapp/whatsapp.baileys.service.ts | 9 ++ src/utils/resolveLidRemoteJidAlt.ts | 60 ++++++++++++ .../whatsapp/messagingHistorySetLid.test.ts | 87 +++++++++++++++++ test/utils/resolveLidRemoteJidAlt.test.ts | 96 +++++++++++++++++++ 4 files changed, 252 insertions(+) create mode 100644 src/utils/resolveLidRemoteJidAlt.ts create mode 100644 test/api/integrations/channel/whatsapp/messagingHistorySetLid.test.ts create mode 100644 test/utils/resolveLidRemoteJidAlt.test.ts diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index af21f1ae5a..d021e6a91a 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -87,6 +87,7 @@ import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent'; import { MESSAGE_DEDUPE_TTL_SECONDS, messageDedupeKey, MessageDedupeKeyInput } from '@utils/messageDedupe'; import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; import { status } from '@utils/renderStatus'; +import { resolveLidRemoteJidAlt } from '@utils/resolveLidRemoteJidAlt'; import { resolvePhoneRemoteJid } from '@utils/resolvePhoneRemoteJid'; import { sendTelemetry } from '@utils/sendTelemetry'; import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma'; @@ -1075,6 +1076,14 @@ export class BaileysStartupService extends ChannelStartupService { chatwootImport.setRepositoryMessagesCache(instance, messagesRepository); } + const lidMapping = this.client?.signalRepository?.lidMapping; + await resolveLidRemoteJidAlt( + messages, + contacts, + lidMapping?.getPNsForLIDs ? (lids) => lidMapping.getPNsForLIDs(lids) : undefined, + (error) => this.logger.warn(`History LID to PN lookup failed: ${(error as Error)?.message}`), + ); + for (const m of messages) { if (!m.message || !m.key || !m.messageTimestamp) { continue; diff --git a/src/utils/resolveLidRemoteJidAlt.ts b/src/utils/resolveLidRemoteJidAlt.ts new file mode 100644 index 0000000000..cee04132c3 --- /dev/null +++ b/src/utils/resolveLidRemoteJidAlt.ts @@ -0,0 +1,60 @@ +import { type Contact, isLidUser, isPnUser, jidNormalizedUser, type LIDMapping, type WAMessage } from 'baileys'; + +export type LidToPnLookup = (lids: string[]) => Promise; + +const toPhoneJid = (jid?: string | null): string | undefined => { + const normalized = jid ? jidNormalizedUser(jid) : undefined; + return normalized && isPnUser(normalized) ? normalized : undefined; +}; + +const contactLidToPn = (contacts: Contact[]): Map => { + const pairs = new Map(); + for (const contact of contacts) { + const lid = [contact.lid, contact.id].find((jid) => jid && isLidUser(jid)); + const pn = toPhoneJid(contact.phoneNumber) ?? toPhoneJid(contact.id); + if (lid && pn) { + pairs.set(jidNormalizedUser(lid), pn); + } + } + return pairs; +}; + +export async function resolveLidRemoteJidAlt( + messages: WAMessage[], + contacts: Contact[], + lookup?: LidToPnLookup, + onLookupError?: (error: unknown) => void, +): Promise { + const pending = messages.filter( + (m) => m.key?.remoteJid && isLidUser(m.key.remoteJid) && !toPhoneJid(m.key.remoteJidAlt), + ); + if (pending.length === 0) { + return; + } + + const lidToPn = contactLidToPn(contacts); + const unresolvedLids = [...new Set(pending.map((m) => jidNormalizedUser(m.key.remoteJid)))].filter( + (lid) => !lidToPn.has(lid), + ); + + if (unresolvedLids.length > 0 && lookup) { + try { + const mappings = (await lookup(unresolvedLids)) ?? []; + for (const { lid, pn } of mappings) { + const phoneJid = toPhoneJid(pn); + if (lid && phoneJid) { + lidToPn.set(jidNormalizedUser(lid), phoneJid); + } + } + } catch (error) { + onLookupError?.(error); + } + } + + for (const m of pending) { + const pn = lidToPn.get(jidNormalizedUser(m.key.remoteJid)); + if (pn) { + m.key.remoteJidAlt = pn; + } + } +} diff --git a/test/api/integrations/channel/whatsapp/messagingHistorySetLid.test.ts b/test/api/integrations/channel/whatsapp/messagingHistorySetLid.test.ts new file mode 100644 index 0000000000..ef8ac991d5 --- /dev/null +++ b/test/api/integrations/channel/whatsapp/messagingHistorySetLid.test.ts @@ -0,0 +1,87 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { Events } from '@api/types/wa.types'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@api/server.module', () => ({ + chatbotController: { emit: vi.fn(async () => undefined) }, + waMonitor: {}, +})); + +vi.mock('@utils/onWhatsappCache', () => ({ + getOnWhatsappCache: vi.fn(async () => []), + saveOnWhatsappCache: vi.fn(async () => undefined), +})); + +vi.mock('@utils/sendTelemetry', () => ({ sendTelemetry: vi.fn() })); + +vi.mock('@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper', () => ({ + chatwootImport: { getRepositoryMessagesCache: vi.fn(() => null), setRepositoryMessagesCache: vi.fn() }, +})); + +const buildService = () => { + const store = new Map(); + const baileysCache = { + get: vi.fn(async (key: string) => store.get(key)), + set: vi.fn(async (key: string, value: unknown) => { + store.set(key, value); + }), + }; + const config: Record = { + LOG: { BAILEYS: 'error' }, + DATABASE: { SAVE_DATA: { NEW_MESSAGE: true, HISTORIC: true, MESSAGE_UPDATE: true, CHATS: false, CONTACTS: false } }, + CHATWOOT: { ENABLED: false }, + OPENAI: { ENABLED: false }, + S3: { ENABLE: false }, + }; + const configService = { get: vi.fn((key: string) => config[key]) } as any; + const prismaRepository = { + chat: { findMany: vi.fn(async () => []), createMany: vi.fn(async () => ({})) }, + message: { createMany: vi.fn(async () => ({})), findMany: vi.fn(async () => []) }, + } as any; + const service = new BaileysStartupService( + configService, + { emit: vi.fn() } as any, + prismaRepository, + {} as any, + {} as any, + baileysCache as any, + {} as any, + ) as any; + service.instance.id = 'instance-id'; + service.instance.name = 'instance-name'; + service.sendDataWebhook = vi.fn(); + service.contactHandle = { 'contacts.upsert': vi.fn(async () => undefined) }; + const getPNsForLIDs = vi.fn(async () => [{ lid: '111@lid', pn: '5491111111111@s.whatsapp.net' }]); + service.client = { signalRepository: { lidMapping: { getPNsForLIDs } } }; + return { service, prismaRepository, getPNsForLIDs }; +}; + +describe('messaging-history.set with @lid messages', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('persists and emits history messages with remoteJidAlt resolved and remoteJid preserved', async () => { + const { service, prismaRepository, getPNsForLIDs } = buildService(); + + await service.messageHandle['messaging-history.set']({ + chats: [{ id: '111@lid' }], + contacts: [], + messages: [ + { + key: { id: 'HIST-1', remoteJid: '111@lid', fromMe: false }, + message: { conversation: 'hi' }, + messageTimestamp: 1700000000, + pushName: 'Someone', + }, + ], + }); + + expect(getPNsForLIDs).toHaveBeenCalledWith(['111@lid']); + const [{ data }] = prismaRepository.message.createMany.mock.calls[0]; + expect(data[0].key).toMatchObject({ remoteJid: '111@lid', remoteJidAlt: '5491111111111@s.whatsapp.net' }); + const [, emitted] = service.sendDataWebhook.mock.calls.find(([event]) => event === Events.MESSAGES_SET); + expect(emitted[0].key.remoteJidAlt).toBe('5491111111111@s.whatsapp.net'); + expect(prismaRepository.chat.createMany.mock.calls[0][0].data[0].remoteJid).toBe('111@lid'); + }); +}); diff --git a/test/utils/resolveLidRemoteJidAlt.test.ts b/test/utils/resolveLidRemoteJidAlt.test.ts new file mode 100644 index 0000000000..3a3f7297ba --- /dev/null +++ b/test/utils/resolveLidRemoteJidAlt.test.ts @@ -0,0 +1,96 @@ +import { resolveLidRemoteJidAlt } from '@utils/resolveLidRemoteJidAlt'; +import { describe, expect, it, vi } from 'vitest'; + +const message = (remoteJid: string, remoteJidAlt?: string) => + ({ key: { id: `id-${remoteJid}`, remoteJid, fromMe: false, ...(remoteJidAlt ? { remoteJidAlt } : {}) } }) as any; + +describe('resolveLidRemoteJidAlt', () => { + it('fills remoteJidAlt from a payload contact carrying lid and phone number', async () => { + const messages = [message('111@lid')]; + const lookup = vi.fn(async () => []); + + await resolveLidRemoteJidAlt(messages, [{ id: '111@lid', phoneNumber: '5491111111111@s.whatsapp.net' }], lookup); + + expect(messages[0].key).toMatchObject({ remoteJid: '111@lid', remoteJidAlt: '5491111111111@s.whatsapp.net' }); + expect(lookup).not.toHaveBeenCalled(); + }); + + it('replaces a remoteJidAlt that is not a phone jid', async () => { + const messages = [message('555@lid', '555@lid')]; + + await resolveLidRemoteJidAlt(messages, [{ id: '555@lid', phoneNumber: '5495555555555@s.whatsapp.net' }]); + + expect(messages[0].key).toMatchObject({ remoteJid: '555@lid', remoteJidAlt: '5495555555555@s.whatsapp.net' }); + }); + + it('fills remoteJidAlt from a contact whose id is the phone jid and lid is set', async () => { + const messages = [message('222@lid')]; + + await resolveLidRemoteJidAlt(messages, [{ id: '5492222222222@s.whatsapp.net', lid: '222@lid' }]); + + expect(messages[0].key.remoteJidAlt).toBe('5492222222222@s.whatsapp.net'); + }); + + it('looks up the remaining distinct lids once and normalizes the phone jid', async () => { + const messages = [message('333@lid'), message('333@lid'), message('444@lid')]; + const lookup = vi.fn(async () => [ + { lid: '333@lid', pn: '5493333333333:12@s.whatsapp.net' }, + { lid: '444@lid', pn: '5494444444444@s.whatsapp.net' }, + ]); + + await resolveLidRemoteJidAlt(messages, [], lookup); + + expect(lookup).toHaveBeenCalledTimes(1); + expect(lookup).toHaveBeenCalledWith(['333@lid', '444@lid']); + expect(messages.map((m) => m.key.remoteJidAlt)).toEqual([ + '5493333333333@s.whatsapp.net', + '5493333333333@s.whatsapp.net', + '5494444444444@s.whatsapp.net', + ]); + }); + + it('leaves remoteJid untouched and skips groups, phone jids and already resolved messages', async () => { + const messages = [ + message('555@lid', '5495555555555@s.whatsapp.net'), + message('123@g.us'), + message('5496666666666@s.whatsapp.net'), + ]; + const lookup = vi.fn(async () => [{ lid: '555@lid', pn: '5490000000000@s.whatsapp.net' }]); + + await resolveLidRemoteJidAlt(messages, [], lookup); + + expect(lookup).not.toHaveBeenCalled(); + expect(messages.map((m) => [m.key.remoteJid, m.key.remoteJidAlt])).toEqual([ + ['555@lid', '5495555555555@s.whatsapp.net'], + ['123@g.us', undefined], + ['5496666666666@s.whatsapp.net', undefined], + ]); + }); + + it('ignores lookup results that are not phone jids', async () => { + const messages = [message('777@lid')]; + + await resolveLidRemoteJidAlt(messages, [], async () => [{ lid: '777@lid', pn: '888@lid' }]); + + expect(messages[0].key.remoteJidAlt).toBeUndefined(); + }); + + it('reports lookup failures without throwing', async () => { + const messages = [message('999@lid')]; + const onLookupError = vi.fn(); + + await expect( + resolveLidRemoteJidAlt( + messages, + [], + async () => { + throw new Error('boom'); + }, + onLookupError, + ), + ).resolves.toBeUndefined(); + + expect(onLookupError).toHaveBeenCalledTimes(1); + expect(messages[0].key.remoteJidAlt).toBeUndefined(); + }); +}); From acf5a4c56240cd5fb015dc4c899b3113697dfec0 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 01:30:07 -0300 Subject: [PATCH 78/96] chore(scripts): add one-off backfill for @lid key.remoteJidAlt Fills key.remoteJidAlt on stored @lid messages from phone JIDs already known for the same lid in the same instance. Never changes key.remoteJid; ambiguous lids are skipped. Inspired by upstream EvolutionAPI/evolution-api#2663 by @fidelis05. --- scripts/backfill-lid-remote-jid-alt/README.md | 47 +++++++++++++++++++ scripts/backfill-lid-remote-jid-alt/apply.sql | 35 ++++++++++++++ .../backfill-lid-remote-jid-alt/dry-run.sql | 37 +++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 scripts/backfill-lid-remote-jid-alt/README.md create mode 100644 scripts/backfill-lid-remote-jid-alt/apply.sql create mode 100644 scripts/backfill-lid-remote-jid-alt/dry-run.sql diff --git a/scripts/backfill-lid-remote-jid-alt/README.md b/scripts/backfill-lid-remote-jid-alt/README.md new file mode 100644 index 0000000000..23db5c0f11 --- /dev/null +++ b/scripts/backfill-lid-remote-jid-alt/README.md @@ -0,0 +1,47 @@ +# Backfill `key.remoteJidAlt` for `@lid` messages + +One-off, idempotent, operator-run PostgreSQL script. Inspired by upstream EvolutionAPI/evolution-api#2663 by @fidelis05. + +## Purpose + +Older `"Message"` rows (history-sync shaped) have `key.remoteJid = "@lid"` and no `key.remoteJidAlt`. +Live messages from the same contact usually carry `key.remoteJidAlt = "@s.whatsapp.net"`. This script +copies that phone JID onto the older rows of the **same `instanceId`** and the same `key.remoteJid`. + +Guarantees: + +- It **never changes `key.remoteJid`**. Only `key.remoteJidAlt` is written. +- It writes only when `key.remoteJidAlt` is missing, empty or not a phone JID, and only with a value ending in `@s.whatsapp.net`. +- Mappings never cross instances. +- A lid that maps to more than one distinct phone within an instance is skipped and counted. +- Re-running it updates 0 rows. + +PostgreSQL only. + +## Steps + +1. Back up the table first: + + ```bash + pg_dump "$DATABASE_CONNECTION_URI" -t '"Message"' -Fc -f message-backup.dump + ``` + +2. Dry run (read-only). Reports candidate rows, fixable rows, rows with a non-phone `remoteJidAlt` + (these are replaced when fixable), ambiguous lids, and the top 10 instances by fixable rows: + + ```bash + psql "$DATABASE_CONNECTION_URI" -f scripts/backfill-lid-remote-jid-alt/dry-run.sql + ``` + +3. Apply (single transaction, prints `updated_rows` and `ambiguous_lids_skipped`): + + ```bash + psql "$DATABASE_CONNECTION_URI" -f scripts/backfill-lid-remote-jid-alt/apply.sql + ``` + +4. Verify: run the dry run again. `fixable_rows` must be 0; running `apply.sql` again must print `updated_rows = 0`. + +## Rollback + +Restore the backup taken in step 1 (for example with `pg_restore --clean -t Message -d "$DATABASE_CONNECTION_URI" +message-backup.dump`). diff --git a/scripts/backfill-lid-remote-jid-alt/apply.sql b/scripts/backfill-lid-remote-jid-alt/apply.sql new file mode 100644 index 0000000000..615b7ae0da --- /dev/null +++ b/scripts/backfill-lid-remote-jid-alt/apply.sql @@ -0,0 +1,35 @@ +\set ON_ERROR_STOP on +BEGIN; + +WITH lid_phone_map AS ( + SELECT "instanceId", key->>'remoteJid' AS lid, min(key->>'remoteJidAlt') AS phone_jid + FROM "Message" + WHERE key->>'remoteJid' LIKE '%@lid' + AND key->>'remoteJidAlt' LIKE '%@s.whatsapp.net' + GROUP BY "instanceId", key->>'remoteJid' + HAVING count(DISTINCT key->>'remoteJidAlt') = 1 +), +updated AS ( + UPDATE "Message" m + SET key = jsonb_set(m.key, '{remoteJidAlt}', to_jsonb(p.phone_jid), true) + FROM lid_phone_map p + WHERE p."instanceId" = m."instanceId" + AND p.lid = m.key->>'remoteJid' + AND m.key->>'remoteJid' LIKE '%@lid' + AND coalesce(m.key->>'remoteJidAlt', '') NOT LIKE '%@s.whatsapp.net' + AND p.phone_jid LIKE '%@s.whatsapp.net' + RETURNING m.id +) +SELECT count(*) AS updated_rows FROM updated; + +WITH ambiguous AS ( + SELECT "instanceId", key->>'remoteJid' AS lid + FROM "Message" + WHERE key->>'remoteJid' LIKE '%@lid' + AND key->>'remoteJidAlt' LIKE '%@s.whatsapp.net' + GROUP BY "instanceId", key->>'remoteJid' + HAVING count(DISTINCT key->>'remoteJidAlt') > 1 +) +SELECT count(*) AS ambiguous_lids_skipped FROM ambiguous; + +COMMIT; diff --git a/scripts/backfill-lid-remote-jid-alt/dry-run.sql b/scripts/backfill-lid-remote-jid-alt/dry-run.sql new file mode 100644 index 0000000000..d2e32ec4e2 --- /dev/null +++ b/scripts/backfill-lid-remote-jid-alt/dry-run.sql @@ -0,0 +1,37 @@ +WITH lid_phone_map AS ( + SELECT "instanceId", key->>'remoteJid' AS lid, count(DISTINCT key->>'remoteJidAlt') AS phone_count + FROM "Message" + WHERE key->>'remoteJid' LIKE '%@lid' + AND key->>'remoteJidAlt' LIKE '%@s.whatsapp.net' + GROUP BY "instanceId", key->>'remoteJid' +), +candidates AS ( + SELECT m."instanceId", key->>'remoteJid' AS lid, coalesce(key->>'remoteJidAlt', '') = '' AS alt_missing + FROM "Message" m + WHERE key->>'remoteJid' LIKE '%@lid' + AND coalesce(key->>'remoteJidAlt', '') NOT LIKE '%@s.whatsapp.net' +) +SELECT + (SELECT count(*) FROM candidates) AS candidate_rows, + (SELECT count(*) FROM candidates c JOIN lid_phone_map p USING ("instanceId", lid) + WHERE p.phone_count = 1) AS fixable_rows, + (SELECT count(*) FROM candidates WHERE NOT alt_missing) AS non_phone_alt_rows, + (SELECT count(*) FROM lid_phone_map WHERE phone_count > 1) AS ambiguous_lids, + (SELECT count(*) FROM candidates c JOIN lid_phone_map p USING ("instanceId", lid) + WHERE p.phone_count > 1) AS ambiguous_rows_skipped; + +WITH lid_phone_map AS ( + SELECT "instanceId", key->>'remoteJid' AS lid, count(DISTINCT key->>'remoteJidAlt') AS phone_count + FROM "Message" + WHERE key->>'remoteJid' LIKE '%@lid' + AND key->>'remoteJidAlt' LIKE '%@s.whatsapp.net' + GROUP BY "instanceId", key->>'remoteJid' +) +SELECT m."instanceId", count(*) AS fixable_rows +FROM "Message" m +JOIN lid_phone_map p ON p."instanceId" = m."instanceId" AND p.lid = m.key->>'remoteJid' +WHERE p.phone_count = 1 + AND coalesce(m.key->>'remoteJidAlt', '') NOT LIKE '%@s.whatsapp.net' +GROUP BY m."instanceId" +ORDER BY fixable_rows DESC +LIMIT 10; From dbb65f1efa58d567f8506b9233ca817e3073e3fb Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 01:32:42 -0300 Subject: [PATCH 79/96] fix(baileys): skip payload contacts with conflicting lids --- src/utils/resolveLidRemoteJidAlt.ts | 8 +++++--- test/utils/resolveLidRemoteJidAlt.test.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/utils/resolveLidRemoteJidAlt.ts b/src/utils/resolveLidRemoteJidAlt.ts index cee04132c3..8b23e37f40 100644 --- a/src/utils/resolveLidRemoteJidAlt.ts +++ b/src/utils/resolveLidRemoteJidAlt.ts @@ -10,10 +10,12 @@ const toPhoneJid = (jid?: string | null): string | undefined => { const contactLidToPn = (contacts: Contact[]): Map => { const pairs = new Map(); for (const contact of contacts) { - const lid = [contact.lid, contact.id].find((jid) => jid && isLidUser(jid)); + const lids = new Set( + [contact.lid, contact.id].filter((jid) => jid && isLidUser(jid)).map((jid) => jidNormalizedUser(jid)), + ); const pn = toPhoneJid(contact.phoneNumber) ?? toPhoneJid(contact.id); - if (lid && pn) { - pairs.set(jidNormalizedUser(lid), pn); + if (lids.size === 1 && pn) { + pairs.set([...lids][0], pn); } } return pairs; diff --git a/test/utils/resolveLidRemoteJidAlt.test.ts b/test/utils/resolveLidRemoteJidAlt.test.ts index 3a3f7297ba..28afdc1418 100644 --- a/test/utils/resolveLidRemoteJidAlt.test.ts +++ b/test/utils/resolveLidRemoteJidAlt.test.ts @@ -23,6 +23,16 @@ describe('resolveLidRemoteJidAlt', () => { expect(messages[0].key).toMatchObject({ remoteJid: '555@lid', remoteJidAlt: '5495555555555@s.whatsapp.net' }); }); + it('ignores a contact whose id and lid are different lids', async () => { + const messages = [message('666@lid'), message('777@lid')]; + + await resolveLidRemoteJidAlt(messages, [ + { id: '666@lid', lid: '777@lid', phoneNumber: '5496666666666@s.whatsapp.net' }, + ]); + + expect(messages.map((m) => m.key.remoteJidAlt)).toEqual([undefined, undefined]); + }); + it('fills remoteJidAlt from a contact whose id is the phone jid and lid is set', async () => { const messages = [message('222@lid')]; From edf05128dc89f1381a0dd87c384633e26f7dace3 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 01:39:06 -0300 Subject: [PATCH 80/96] chore(scripts): back up rows before the @lid backfill and add rollback Replaces the table-level pg_restore rollback with a backup table and rollback.sql, and adds lock and statement timeouts. --- scripts/backfill-lid-remote-jid-alt/README.md | 26 ++++++++++++-- scripts/backfill-lid-remote-jid-alt/apply.sql | 34 ++++++++++++++----- .../backfill-lid-remote-jid-alt/rollback.sql | 17 ++++++++++ 3 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 scripts/backfill-lid-remote-jid-alt/rollback.sql diff --git a/scripts/backfill-lid-remote-jid-alt/README.md b/scripts/backfill-lid-remote-jid-alt/README.md index 23db5c0f11..917d9d0765 100644 --- a/scripts/backfill-lid-remote-jid-alt/README.md +++ b/scripts/backfill-lid-remote-jid-alt/README.md @@ -33,7 +33,11 @@ PostgreSQL only. psql "$DATABASE_CONNECTION_URI" -f scripts/backfill-lid-remote-jid-alt/dry-run.sql ``` -3. Apply (single transaction, prints `updated_rows` and `ambiguous_lids_skipped`): +3. Apply (single transaction, prints `updated_rows` and `ambiguous_lids_skipped`). Before updating, it copies + the original `(id, key)` of every row it is about to change into `"_lid_remote_jid_alt_backup"` (created if + missing; on re-runs the first saved original wins). It runs with `lock_timeout = '5s'` and + `statement_timeout = '10min'` so it fails fast instead of blocking live writes; on a timeout nothing is + committed and it can simply be re-run: ```bash psql "$DATABASE_CONNECTION_URI" -f scripts/backfill-lid-remote-jid-alt/apply.sql @@ -43,5 +47,21 @@ PostgreSQL only. ## Rollback -Restore the backup taken in step 1 (for example with `pg_restore --clean -t Message -d "$DATABASE_CONNECTION_URI" -message-backup.dump`). +Precise rollback from the backup table (single transaction: restores `key` for every backed-up id, prints +`restored_rows`, then drops `"_lid_remote_jid_alt_backup"`): + +```bash +psql "$DATABASE_CONNECTION_URI" -f scripts/backfill-lid-remote-jid-alt/rollback.sql +``` + +Rollback replaces the whole `key` of each backed-up row, so any change made to those keys after `apply.sql` +(for example a status update) is lost. Roll back soon after applying, or not at all. + +The full `pg_dump` from step 1 remains the last-resort backup; restore individual rows from it rather than +dropping and recreating `"Message"`. + +Once the backfill is verified and no rollback is needed, drop the backup table manually: + +```sql +DROP TABLE "_lid_remote_jid_alt_backup"; +``` diff --git a/scripts/backfill-lid-remote-jid-alt/apply.sql b/scripts/backfill-lid-remote-jid-alt/apply.sql index 615b7ae0da..07d5cd5732 100644 --- a/scripts/backfill-lid-remote-jid-alt/apply.sql +++ b/scripts/backfill-lid-remote-jid-alt/apply.sql @@ -1,6 +1,15 @@ \set ON_ERROR_STOP on BEGIN; +SET LOCAL lock_timeout = '5s'; +SET LOCAL statement_timeout = '10min'; +CREATE TABLE IF NOT EXISTS "_lid_remote_jid_alt_backup" ( + id text PRIMARY KEY, + key jsonb NOT NULL, + backed_up_at timestamp DEFAULT now() +); + +CREATE TEMP TABLE lid_backfill_targets ON COMMIT DROP AS WITH lid_phone_map AS ( SELECT "instanceId", key->>'remoteJid' AS lid, min(key->>'remoteJidAlt') AS phone_jid FROM "Message" @@ -8,16 +17,23 @@ WITH lid_phone_map AS ( AND key->>'remoteJidAlt' LIKE '%@s.whatsapp.net' GROUP BY "instanceId", key->>'remoteJid' HAVING count(DISTINCT key->>'remoteJidAlt') = 1 -), -updated AS ( +) +SELECT m.id, m.key AS original_key, p.phone_jid +FROM "Message" m +JOIN lid_phone_map p ON p."instanceId" = m."instanceId" AND p.lid = m.key->>'remoteJid' +WHERE m.key->>'remoteJid' LIKE '%@lid' + AND coalesce(m.key->>'remoteJidAlt', '') NOT LIKE '%@s.whatsapp.net' + AND p.phone_jid LIKE '%@s.whatsapp.net'; + +INSERT INTO "_lid_remote_jid_alt_backup" (id, key) +SELECT id, original_key FROM lid_backfill_targets +ON CONFLICT (id) DO NOTHING; + +WITH updated AS ( UPDATE "Message" m - SET key = jsonb_set(m.key, '{remoteJidAlt}', to_jsonb(p.phone_jid), true) - FROM lid_phone_map p - WHERE p."instanceId" = m."instanceId" - AND p.lid = m.key->>'remoteJid' - AND m.key->>'remoteJid' LIKE '%@lid' - AND coalesce(m.key->>'remoteJidAlt', '') NOT LIKE '%@s.whatsapp.net' - AND p.phone_jid LIKE '%@s.whatsapp.net' + SET key = jsonb_set(m.key, '{remoteJidAlt}', to_jsonb(t.phone_jid), true) + FROM lid_backfill_targets t + WHERE t.id = m.id RETURNING m.id ) SELECT count(*) AS updated_rows FROM updated; diff --git a/scripts/backfill-lid-remote-jid-alt/rollback.sql b/scripts/backfill-lid-remote-jid-alt/rollback.sql new file mode 100644 index 0000000000..2dd4ff322e --- /dev/null +++ b/scripts/backfill-lid-remote-jid-alt/rollback.sql @@ -0,0 +1,17 @@ +\set ON_ERROR_STOP on +BEGIN; +SET LOCAL lock_timeout = '5s'; +SET LOCAL statement_timeout = '10min'; + +WITH restored AS ( + UPDATE "Message" m + SET key = b.key + FROM "_lid_remote_jid_alt_backup" b + WHERE b.id = m.id + RETURNING m.id +) +SELECT count(*) AS restored_rows FROM restored; + +DROP TABLE "_lid_remote_jid_alt_backup"; + +COMMIT; From 3a0e91a6c1702ae308beea59f0362db597e2d72d Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 01:44:02 -0300 Subject: [PATCH 81/96] chore(scripts): skip rows changed during the @lid backfill and back up only updated rows --- scripts/backfill-lid-remote-jid-alt/README.md | 4 ++-- scripts/backfill-lid-remote-jid-alt/apply.sql | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/backfill-lid-remote-jid-alt/README.md b/scripts/backfill-lid-remote-jid-alt/README.md index 917d9d0765..1c2b0ff36c 100644 --- a/scripts/backfill-lid-remote-jid-alt/README.md +++ b/scripts/backfill-lid-remote-jid-alt/README.md @@ -33,7 +33,7 @@ PostgreSQL only. psql "$DATABASE_CONNECTION_URI" -f scripts/backfill-lid-remote-jid-alt/dry-run.sql ``` -3. Apply (single transaction, prints `updated_rows` and `ambiguous_lids_skipped`). Before updating, it copies +3. Apply (single transaction, prints `updated_rows`, `changed_meanwhile_rows_skipped` and `ambiguous_lids_skipped`). Before updating, it copies the original `(id, key)` of every row it is about to change into `"_lid_remote_jid_alt_backup"` (created if missing; on re-runs the first saved original wins). It runs with `lock_timeout = '5s'` and `statement_timeout = '10min'` so it fails fast instead of blocking live writes; on a timeout nothing is @@ -43,7 +43,7 @@ PostgreSQL only. psql "$DATABASE_CONNECTION_URI" -f scripts/backfill-lid-remote-jid-alt/apply.sql ``` -4. Verify: run the dry run again. `fixable_rows` must be 0; running `apply.sql` again must print `updated_rows = 0`. +4. Verify: run the dry run again. `fixable_rows` must be 0; running `apply.sql` again must print `updated_rows = 0`. Rows changed by Evolution while the script ran are skipped (`changed_meanwhile_rows_skipped`); run it again to pick them up. ## Rollback diff --git a/scripts/backfill-lid-remote-jid-alt/apply.sql b/scripts/backfill-lid-remote-jid-alt/apply.sql index 07d5cd5732..c021c99614 100644 --- a/scripts/backfill-lid-remote-jid-alt/apply.sql +++ b/scripts/backfill-lid-remote-jid-alt/apply.sql @@ -25,18 +25,25 @@ WHERE m.key->>'remoteJid' LIKE '%@lid' AND coalesce(m.key->>'remoteJidAlt', '') NOT LIKE '%@s.whatsapp.net' AND p.phone_jid LIKE '%@s.whatsapp.net'; -INSERT INTO "_lid_remote_jid_alt_backup" (id, key) -SELECT id, original_key FROM lid_backfill_targets -ON CONFLICT (id) DO NOTHING; - WITH updated AS ( UPDATE "Message" m SET key = jsonb_set(m.key, '{remoteJidAlt}', to_jsonb(t.phone_jid), true) FROM lid_backfill_targets t WHERE t.id = m.id + AND m.key = t.original_key RETURNING m.id +), +backed_up AS ( + INSERT INTO "_lid_remote_jid_alt_backup" (id, key) + SELECT t.id, t.original_key + FROM lid_backfill_targets t + JOIN updated u ON u.id = t.id + ON CONFLICT (id) DO NOTHING + RETURNING id ) -SELECT count(*) AS updated_rows FROM updated; +SELECT + (SELECT count(*) FROM updated) AS updated_rows, + (SELECT count(*) FROM lid_backfill_targets) - (SELECT count(*) FROM updated) AS changed_meanwhile_rows_skipped; WITH ambiguous AS ( SELECT "instanceId", key->>'remoteJid' AS lid From c2c081152bba12d92217499f2dc61702141db7b5 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 01:45:23 -0300 Subject: [PATCH 82/96] docs(scripts): note row locks held by the @lid backfill --- scripts/backfill-lid-remote-jid-alt/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/backfill-lid-remote-jid-alt/README.md b/scripts/backfill-lid-remote-jid-alt/README.md index 1c2b0ff36c..b4c14d253e 100644 --- a/scripts/backfill-lid-remote-jid-alt/README.md +++ b/scripts/backfill-lid-remote-jid-alt/README.md @@ -37,7 +37,9 @@ PostgreSQL only. the original `(id, key)` of every row it is about to change into `"_lid_remote_jid_alt_backup"` (created if missing; on re-runs the first saved original wins). It runs with `lock_timeout = '5s'` and `statement_timeout = '10min'` so it fails fast instead of blocking live writes; on a timeout nothing is - committed and it can simply be re-run: + committed and it can simply be re-run. Updated rows stay locked until the transaction commits, so live status + updates on those rows wait for it; run it during low traffic. Measured candidates are about 20k rows per + database, which commits in seconds: ```bash psql "$DATABASE_CONNECTION_URI" -f scripts/backfill-lid-remote-jid-alt/apply.sql From b9991042147e4036a3089c5940b13d5b3d74b49c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Varela?= <114447249+MatiasxVarela@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:30:37 -0300 Subject: [PATCH 83/96] feat(baileys): backfill remoteJidAlt when a lid mapping is learned (#34) * feat(baileys): backfill remoteJidAlt on stored lid messages When an instance learns a LID->phone pair, via the Baileys lid-mapping.update event or a live messages.upsert carrying a @lid remoteJid with a phone remoteJidAlt, older Message rows of that instance keyed by the lid get key.remoteJidAlt filled with the phone JID. key.remoteJid is never rewritten and existing phone remoteJidAlt values are kept. The update is a parameterized jsonb_set, runs at most once per (instance, lid) per process via a bounded set, is fire-and-forget, logs failures at warn and is skipped when message saving is disabled. 1:1 chats only. Inspired by upstream EvolutionAPI/evolution-api#2663 by @fidelis05. * fix(baileys): retry failed lid backfills, run them one at a time, postgres only * test(baileys): cover lid backfill scheduling guards and lid-mapping dispatch * test(baileys): cover the messages.upsert lid backfill trigger * test(utils): use fictional phone and lid fixtures * test(utils): replace real phone numbers with fictional fixtures --- .../whatsapp/whatsapp.baileys.service.ts | 17 +++ src/utils/lidRemoteJidAltBackfill.ts | 52 +++++++ .../scheduleLidRemoteJidAltBackfill.test.ts | 140 ++++++++++++++++++ test/utils/createJid.test.ts | 6 +- test/utils/lidRemoteJidAltBackfill.test.ts | 104 +++++++++++++ test/utils/onWhatsappCache.test.ts | 4 +- test/utils/resolvePhoneRemoteJid.test.ts | 20 +-- 7 files changed, 328 insertions(+), 15 deletions(-) create mode 100644 src/utils/lidRemoteJidAltBackfill.ts create mode 100644 test/api/integrations/channel/whatsapp/scheduleLidRemoteJidAltBackfill.test.ts create mode 100644 test/utils/lidRemoteJidAltBackfill.test.ts diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index d021e6a91a..9838a68ff7 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -83,6 +83,7 @@ import { Instance, Message } from '@prisma/client'; import { createJid } from '@utils/createJid'; import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; import { headerAsString } from '@utils/headerAsString'; +import { LidRemoteJidAltBackfill } from '@utils/lidRemoteJidAltBackfill'; import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent'; import { MESSAGE_DEDUPE_TTL_SECONDS, messageDedupeKey, MessageDedupeKeyInput } from '@utils/messageDedupe'; import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; @@ -255,6 +256,15 @@ export class BaileysStartupService extends ChannelStartupService { private endSession = false; private logBaileys = this.configService.get('LOG').BAILEYS; private eventProcessingQueue: Promise = Promise.resolve(); + private readonly lidRemoteJidAltBackfill = new LidRemoteJidAltBackfill(this.prismaRepository, this.logger); + + private scheduleLidRemoteJidAltBackfill(lid?: string, phoneJid?: string) { + const database = this.configService.get('DATABASE'); + if (database.PROVIDER !== 'postgresql') return; + if (!database.SAVE_DATA.NEW_MESSAGE && !database.SAVE_DATA.HISTORIC) return; + + void this.lidRemoteJidAltBackfill.schedule(this.instanceId, lid, phoneJid); + } // Cache TTL constants (in seconds) private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing @@ -1179,6 +1189,8 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.warn(`Message ignored with messageStubParameters: ${JSON.stringify(received, null, 2)}`); continue; } + + this.scheduleLidRemoteJidAltBackfill(received.key?.remoteJid, received.key?.remoteJidAlt); if (received.message?.conversation || received.message?.extendedTextMessage?.text) { const text = received.message?.conversation || received.message?.extendedTextMessage?.text; @@ -1998,6 +2010,11 @@ export class BaileysStartupService extends ChannelStartupService { await this.messageHandle['messaging-history.set'](payload); } + if (events['lid-mapping.update']) { + const { lid, pn } = events['lid-mapping.update']; + this.scheduleLidRemoteJidAltBackfill(lid, pn); + } + if (events['messages.upsert']) { const payload = events['messages.upsert']; diff --git a/src/utils/lidRemoteJidAltBackfill.ts b/src/utils/lidRemoteJidAltBackfill.ts new file mode 100644 index 0000000000..ac00320181 --- /dev/null +++ b/src/utils/lidRemoteJidAltBackfill.ts @@ -0,0 +1,52 @@ +import { PrismaRepository } from '@api/repository/repository.service'; +import { Logger } from '@config/logger.config'; +import { jidNormalizedUser } from 'baileys'; + +const MAX_TRACKED_LIDS = 10_000; +const LID_SUFFIX = '@lid'; +const PHONE_SUFFIX = '@s.whatsapp.net'; + +export class LidRemoteJidAltBackfill { + private readonly backfilledLids = new Set(); + private backfillQueue: Promise = Promise.resolve(); + + constructor( + private readonly prismaRepository: PrismaRepository, + private readonly logger: Logger, + ) {} + + public schedule(instanceId: string, lid?: string, phoneJid?: string): Promise | undefined { + if (!instanceId || !lid || !phoneJid) return undefined; + + const normalizedLid = jidNormalizedUser(lid); + const normalizedPhoneJid = jidNormalizedUser(phoneJid); + if (!normalizedLid.endsWith(LID_SUFFIX) || !normalizedPhoneJid.endsWith(PHONE_SUFFIX)) return undefined; + + const trackingKey = `${instanceId}:${normalizedLid}`; + if (this.backfilledLids.has(trackingKey)) return undefined; + if (this.backfilledLids.size >= MAX_TRACKED_LIDS) this.backfilledLids.clear(); + this.backfilledLids.add(trackingKey); + + const queuedBackfill = this.backfillQueue.then(() => + this.backfill(trackingKey, instanceId, normalizedLid, normalizedPhoneJid), + ); + this.backfillQueue = queuedBackfill; + return queuedBackfill; + } + + private async backfill(trackingKey: string, instanceId: string, lid: string, phoneJid: string): Promise { + try { + const updatedRows = await this.prismaRepository.$executeRaw` + UPDATE "Message" + SET "key" = jsonb_set("key"::jsonb, '{remoteJidAlt}', to_jsonb(${phoneJid}::text), true) + WHERE "instanceId" = ${instanceId} + AND "key"->>'remoteJid' = ${lid} + AND COALESCE("key"->>'remoteJidAlt', '') NOT LIKE ${`%${PHONE_SUFFIX}`} + `; + if (updatedRows > 0) this.logger.verbose(`Backfilled remoteJidAlt on ${updatedRows} messages for ${lid}`); + } catch (error) { + this.backfilledLids.delete(trackingKey); + this.logger.warn(`Failed to backfill remoteJidAlt for ${lid}: ${error?.message ?? error}`); + } + } +} diff --git a/test/api/integrations/channel/whatsapp/scheduleLidRemoteJidAltBackfill.test.ts b/test/api/integrations/channel/whatsapp/scheduleLidRemoteJidAltBackfill.test.ts new file mode 100644 index 0000000000..5afc705bab --- /dev/null +++ b/test/api/integrations/channel/whatsapp/scheduleLidRemoteJidAltBackfill.test.ts @@ -0,0 +1,140 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@api/server.module', () => ({ + chatbotController: { emit: vi.fn(async () => undefined) }, + waMonitor: {}, +})); + +vi.mock('@utils/onWhatsappCache', () => ({ + getOnWhatsappCache: vi.fn(async () => []), + saveOnWhatsappCache: vi.fn(async () => undefined), +})); + +vi.mock('@utils/sendTelemetry', () => ({ sendTelemetry: vi.fn() })); + +vi.mock('@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper', () => ({ + chatwootImport: { getRepositoryMessagesCache: vi.fn(() => null), setRepositoryMessagesCache: vi.fn() }, +})); + +const LID = '123456789@lid'; +const PHONE_JID = '5491123456789@s.whatsapp.net'; + +const buildService = (provider: string, saveData: { NEW_MESSAGE: boolean; HISTORIC: boolean }) => { + const config: Record = { + LOG: { BAILEYS: 'error' }, + DATABASE: { PROVIDER: provider, SAVE_DATA: { ...saveData, CHATS: false, CONTACTS: false } }, + CHATWOOT: { ENABLED: false }, + OPENAI: { ENABLED: false }, + S3: { ENABLE: false }, + }; + const configService = { get: vi.fn((key: string) => config[key]) } as any; + const prismaRepository = { $executeRaw: vi.fn(async () => 0) } as any; + const service = new BaileysStartupService( + configService, + { emit: vi.fn() } as any, + prismaRepository, + {} as any, + {} as any, + {} as any, + {} as any, + ) as any; + service.instance.id = 'instance-id'; + service.instance.name = 'instance-name'; + const schedule = vi.spyOn(service.lidRemoteJidAltBackfill, 'schedule').mockReturnValue(undefined); + return { service, schedule, prismaRepository }; +}; + +const deliverUpsert = async (service: any, key: Record) => { + await service.messageHandle['messages.upsert']( + { messages: [{ key: { id: 'MSG-1', fromMe: false, ...key }, messageTimestamp: 1700000000 }], type: 'notify' }, + {}, + ); +}; + +const runRealSchedule = (service: any, schedule: any) => { + const backfills: Array | undefined> = []; + schedule.mockRestore(); + const spy = vi.spyOn(service.lidRemoteJidAltBackfill, 'schedule'); + const original = service.lidRemoteJidAltBackfill.constructor.prototype.schedule; + spy.mockImplementation((...args: unknown[]) => { + const backfill = original.apply(service.lidRemoteJidAltBackfill, args); + backfills.push(backfill); + return backfill; + }); + return { spy, settled: () => Promise.all(backfills) }; +}; + +const dispatch = async (service: any, events: Record) => { + let processEvents: (events: Record) => Promise; + service.client = { ev: { process: vi.fn((handler) => (processEvents = handler)) } }; + service.findSettings = vi.fn(async () => ({})); + service.eventHandler(); + await processEvents(events); + await service.eventProcessingQueue; +}; + +describe('scheduleLidRemoteJidAltBackfill', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('does not schedule a backfill on mysql', () => { + const { service, schedule } = buildService('mysql', { NEW_MESSAGE: true, HISTORIC: true }); + + service.scheduleLidRemoteJidAltBackfill(LID, PHONE_JID); + + expect(schedule).not.toHaveBeenCalled(); + }); + + it('does not schedule a backfill on postgresql when messages are not saved', () => { + const { service, schedule } = buildService('postgresql', { NEW_MESSAGE: false, HISTORIC: false }); + + service.scheduleLidRemoteJidAltBackfill(LID, PHONE_JID); + + expect(schedule).not.toHaveBeenCalled(); + }); + + it.each([ + { NEW_MESSAGE: true, HISTORIC: false }, + { NEW_MESSAGE: false, HISTORIC: true }, + ])('schedules a backfill on postgresql with save flags %o', (saveData) => { + const { service, schedule } = buildService('postgresql', saveData); + + service.scheduleLidRemoteJidAltBackfill(LID, PHONE_JID); + + expect(schedule).toHaveBeenCalledWith('instance-id', LID, PHONE_JID); + }); + + it('schedules a backfill with the lid and pn of a lid-mapping.update event', async () => { + const { service, schedule } = buildService('postgresql', { NEW_MESSAGE: true, HISTORIC: false }); + + await dispatch(service, { 'lid-mapping.update': { lid: LID, pn: PHONE_JID } }); + + expect(schedule).toHaveBeenCalledWith('instance-id', LID, PHONE_JID); + }); + + it('schedules a backfill with the lid and phone of a 1:1 messages.upsert', async () => { + const { service, schedule, prismaRepository } = buildService('postgresql', { NEW_MESSAGE: true, HISTORIC: false }); + const { spy, settled } = runRealSchedule(service, schedule); + + await deliverUpsert(service, { remoteJid: '111@lid', remoteJidAlt: '5491111@s.whatsapp.net' }); + await settled(); + + expect(spy).toHaveBeenCalledWith('instance-id', '111@lid', '5491111@s.whatsapp.net'); + expect(prismaRepository.$executeRaw).toHaveBeenCalledTimes(1); + }); + + it.each([ + { name: 'a phone remoteJid', key: { remoteJid: PHONE_JID, remoteJidAlt: LID } }, + { name: 'a group remoteJid', key: { remoteJid: '120363000000000000@g.us', participant: LID } }, + ])('does not backfill a messages.upsert with $name', async ({ key }) => { + const { service, schedule, prismaRepository } = buildService('postgresql', { NEW_MESSAGE: true, HISTORIC: false }); + const { settled } = runRealSchedule(service, schedule); + + await deliverUpsert(service, key); + await settled(); + + expect(prismaRepository.$executeRaw).not.toHaveBeenCalled(); + }); +}); diff --git a/test/utils/createJid.test.ts b/test/utils/createJid.test.ts index 7d02dea252..10e58e8adc 100644 --- a/test/utils/createJid.test.ts +++ b/test/utils/createJid.test.ts @@ -44,8 +44,8 @@ describe('createJid', () => { }); it('keeps the mobile 9 in 13 digit Argentine numbers', () => { - expect(createJid('5492616107398')).toBe('5492616107398@s.whatsapp.net'); - expect(createJid('+54 9 261 610-7398')).toBe('5492616107398@s.whatsapp.net'); + expect(createJid('5492615550101')).toBe('5492615550101@s.whatsapp.net'); + expect(createJid('+54 9 261 555-0101')).toBe('5492615550101@s.whatsapp.net'); }); it.each([ @@ -79,7 +79,7 @@ describe('createJid', () => { }); it('keeps 12 digit Argentine numbers unchanged', () => { - expect(createJid('542616107398')).toBe('542616107398@s.whatsapp.net'); + expect(createJid('542615550101')).toBe('542615550101@s.whatsapp.net'); }); it('keeps the Brazilian ninth digit when the area code is below 31', () => { diff --git a/test/utils/lidRemoteJidAltBackfill.test.ts b/test/utils/lidRemoteJidAltBackfill.test.ts new file mode 100644 index 0000000000..0d5d422ad1 --- /dev/null +++ b/test/utils/lidRemoteJidAltBackfill.test.ts @@ -0,0 +1,104 @@ +import { LidRemoteJidAltBackfill } from '@utils/lidRemoteJidAltBackfill'; +import { describe, expect, it, vi } from 'vitest'; + +const LID = '100000000000001@lid'; +const PHONE = '5492615550102@s.whatsapp.net'; + +function makeBackfill(executeRaw = vi.fn().mockResolvedValue(3)) { + const prisma = { $executeRaw: executeRaw } as any; + const logger = { warn: vi.fn(), verbose: vi.fn() } as any; + return { backfill: new LidRemoteJidAltBackfill(prisma, logger), executeRaw, logger }; +} + +describe('LidRemoteJidAltBackfill', () => { + it('backfills stored messages when a LID->phone pair is learned', async () => { + const { backfill, executeRaw } = makeBackfill(); + await backfill.schedule('instance-1', LID, PHONE); + expect(executeRaw).toHaveBeenCalledTimes(1); + }); + + it('updates only this instance rows for the lid with bound parameters', async () => { + const { backfill, executeRaw } = makeBackfill(); + await backfill.schedule('instance-1', LID, PHONE); + expect(executeRaw).toHaveBeenCalledTimes(1); + const [strings, ...values] = executeRaw.mock.calls[0]; + const sql = strings.join('?'); + expect(sql).toContain('jsonb_set'); + expect(sql).toContain('"instanceId" = ?'); + expect(sql).not.toContain(LID); + expect(sql).not.toContain(PHONE); + expect(values).toEqual(expect.arrayContaining(['instance-1', LID, PHONE])); + }); + + it('normalizes device suffixes before backfilling', async () => { + const { backfill, executeRaw } = makeBackfill(); + await backfill.schedule('instance-1', '100000000000001:12@lid', '5492615550102:3@s.whatsapp.net'); + const values = executeRaw.mock.calls[0].slice(1); + expect(values).toEqual(expect.arrayContaining([LID, PHONE])); + }); + + it('runs at most once per instance and lid', async () => { + const { backfill, executeRaw } = makeBackfill(); + await backfill.schedule('instance-1', LID, PHONE); + expect(backfill.schedule('instance-1', LID, PHONE)).toBeUndefined(); + await backfill.schedule('instance-2', LID, PHONE); + expect(executeRaw).toHaveBeenCalledTimes(2); + }); + + it.each([ + ['missing lid', undefined, PHONE], + ['missing phone', LID, undefined], + ['phone that is not a user JID', LID, '123@lid'], + ['lid that is not a lid', '5492615550102@s.whatsapp.net', PHONE], + ['group participant', '120363000000000000@g.us', PHONE], + ])('skips a %s', (_label, lid, phone) => { + const { backfill, executeRaw } = makeBackfill(); + expect(backfill.schedule('instance-1', lid, phone)).toBeUndefined(); + expect(executeRaw).not.toHaveBeenCalled(); + }); + + it('logs a warning and resolves when the update fails', async () => { + const { backfill, logger } = makeBackfill(vi.fn().mockRejectedValue(new Error('db down'))); + await expect(backfill.schedule('instance-1', LID, PHONE)).resolves.toBeUndefined(); + expect(logger.warn).toHaveBeenCalledTimes(1); + }); + + it('retries a lid whose previous update failed', async () => { + const executeRaw = vi.fn().mockRejectedValueOnce(new Error('db down')).mockResolvedValue(1); + const { backfill } = makeBackfill(executeRaw); + await backfill.schedule('instance-1', LID, PHONE); + await backfill.schedule('instance-1', LID, PHONE); + expect(executeRaw).toHaveBeenCalledTimes(2); + }); + + it('runs one update at a time and keeps going after a failure', async () => { + let running = 0; + let maxRunning = 0; + const executeRaw = vi.fn().mockImplementation(async () => { + running++; + maxRunning = Math.max(maxRunning, running); + await new Promise((resolve) => setTimeout(resolve, 5)); + running--; + if (executeRaw.mock.calls.length === 1) throw new Error('db down'); + return 1; + }); + const { backfill } = makeBackfill(executeRaw); + await Promise.all([ + backfill.schedule('instance-1', LID, PHONE), + backfill.schedule('instance-1', '1@lid', PHONE), + backfill.schedule('instance-1', '2@lid', PHONE), + ]); + expect(executeRaw).toHaveBeenCalledTimes(3); + expect(maxRunning).toBe(1); + }); + + it('clears the dedupe set once it grows past its bound', async () => { + const { backfill, executeRaw } = makeBackfill(); + await backfill.schedule('instance-1', LID, PHONE); + for (let index = 0; index <= 10_000; index++) { + backfill.schedule('instance-1', `${index}@lid`, PHONE); + } + await backfill.schedule('instance-1', LID, PHONE); + expect(executeRaw.mock.calls.filter((call) => call.includes(LID))).toHaveLength(2); + }); +}); diff --git a/test/utils/onWhatsappCache.test.ts b/test/utils/onWhatsappCache.test.ts index 51cd7e25d6..b8ab6afde8 100644 --- a/test/utils/onWhatsappCache.test.ts +++ b/test/utils/onWhatsappCache.test.ts @@ -157,9 +157,9 @@ describe('getOnWhatsappCache', () => { }); it('queries Argentine numbers with and without the 9 prefix', async () => { - await getOnWhatsappCache(['542616107398@s.whatsapp.net']); + await getOnWhatsappCache(['542615550101@s.whatsapp.net']); - expect(queriedJids()).toEqual(['5492616107398@s.whatsapp.net', '542616107398@s.whatsapp.net']); + expect(queriedJids()).toEqual(['5492615550101@s.whatsapp.net', '542615550101@s.whatsapp.net']); }); it('queries Mexican numbers with and without the 1 prefix', async () => { diff --git a/test/utils/resolvePhoneRemoteJid.test.ts b/test/utils/resolvePhoneRemoteJid.test.ts index 148bee0c3a..8082c03b5d 100644 --- a/test/utils/resolvePhoneRemoteJid.test.ts +++ b/test/utils/resolvePhoneRemoteJid.test.ts @@ -4,22 +4,22 @@ import { describe, expect, it } from 'vitest'; describe('resolvePhoneRemoteJid', () => { it('replaces a @lid remoteJid with the phone JID from remoteJidAlt', () => { expect( - resolvePhoneRemoteJid({ remoteJid: '14714994171909@lid', remoteJidAlt: '5492616841080@s.whatsapp.net' }), - ).toBe('5492616841080@s.whatsapp.net'); + resolvePhoneRemoteJid({ remoteJid: '100000000000001@lid', remoteJidAlt: '5492615550102@s.whatsapp.net' }), + ).toBe('5492615550102@s.whatsapp.net'); }); it('keeps a @lid remoteJid when remoteJidAlt is missing', () => { - expect(resolvePhoneRemoteJid({ remoteJid: '14714994171909@lid' })).toBe('14714994171909@lid'); + expect(resolvePhoneRemoteJid({ remoteJid: '100000000000001@lid' })).toBe('100000000000001@lid'); }); it('keeps a @lid remoteJid when remoteJidAlt is empty', () => { - expect(resolvePhoneRemoteJid({ remoteJid: '14714994171909@lid', remoteJidAlt: '' })).toBe('14714994171909@lid'); + expect(resolvePhoneRemoteJid({ remoteJid: '100000000000001@lid', remoteJidAlt: '' })).toBe('100000000000001@lid'); }); it('keeps a phone remoteJid even when remoteJidAlt is present', () => { expect( - resolvePhoneRemoteJid({ remoteJid: '5492616841080@s.whatsapp.net', remoteJidAlt: '14714994171909@lid' }), - ).toBe('5492616841080@s.whatsapp.net'); + resolvePhoneRemoteJid({ remoteJid: '5492615550102@s.whatsapp.net', remoteJidAlt: '100000000000001@lid' }), + ).toBe('5492615550102@s.whatsapp.net'); }); it('keeps a group remoteJid', () => { @@ -29,18 +29,18 @@ describe('resolvePhoneRemoteJid', () => { }); it('KNOWN BUG: replaces a remoteJid that contains @lid outside the suffix (current behavior)', () => { - expect(resolvePhoneRemoteJid({ remoteJid: 'x@lid.example', remoteJidAlt: '5492616841080@s.whatsapp.net' })).toBe( - '5492616841080@s.whatsapp.net', + expect(resolvePhoneRemoteJid({ remoteJid: 'x@lid.example', remoteJidAlt: '5492615550102@s.whatsapp.net' })).toBe( + '5492615550102@s.whatsapp.net', ); }); it.fails('keeps a remoteJid that contains @lid outside the suffix', () => { - expect(resolvePhoneRemoteJid({ remoteJid: 'x@lid.example', remoteJidAlt: '5492616841080@s.whatsapp.net' })).toBe( + expect(resolvePhoneRemoteJid({ remoteJid: 'x@lid.example', remoteJidAlt: '5492615550102@s.whatsapp.net' })).toBe( 'x@lid.example', ); }); it('returns undefined when there is no remoteJid', () => { - expect(resolvePhoneRemoteJid({ remoteJidAlt: '5492616841080@s.whatsapp.net' })).toBeUndefined(); + expect(resolvePhoneRemoteJid({ remoteJidAlt: '5492615550102@s.whatsapp.net' })).toBeUndefined(); }); }); From 85d0b1e58d0442c53bc57cd9492657d5ca4491ae Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 12:12:01 -0300 Subject: [PATCH 84/96] fix(baileys): advance the status of sent messages from acks Sent messages stayed PENDING because messages.update only wrote Message.status for received messages. The status now only moves forward, ERROR is always kept unless the message is DELETED or EDITED, and those stay final. Webhooks and MessageUpdate rows are unchanged. --- CHANGELOG.md | 6 + .../whatsapp/whatsapp.baileys.service.ts | 11 ++ src/utils/advanceMessageStatus.ts | 19 +++ .../whatsapp/messagesUpdateOwnStatus.test.ts | 134 ++++++++++++++++++ test/utils/advanceMessageStatus.test.ts | 39 +++++ 5 files changed, 209 insertions(+) create mode 100644 src/utils/advanceMessageStatus.ts create mode 100644 test/api/integrations/channel/whatsapp/messagesUpdateOwnStatus.test.ts create mode 100644 test/utils/advanceMessageStatus.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 80998311c2..481e13a018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # evolution-in-purple +## Unreleased + +### Fixes + +* **Sent message status**: messages sent by the instance now advance `Message.status` from WhatsApp acks instead of staying `PENDING`, so `findChats` `lastMessageStatus` and `findMessages` reflect the real status. Applies to 1:1 chats; Baileys delivers no per-message status for groups. Backward acks are ignored and `DELETED`/`EDITED` are never overwritten + ## 0.1.3 (2026-10-24) ### Security diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 9838a68ff7..70c143a079 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -80,6 +80,7 @@ import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import { Boom } from '@hapi/boom'; import { createId as cuid } from '@paralleldrive/cuid2'; import { Instance, Message } from '@prisma/client'; +import { nextMessageStatus } from '@utils/advanceMessageStatus'; import { createJid } from '@utils/createJid'; import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; import { headerAsString } from '@utils/headerAsString'; @@ -1795,6 +1796,16 @@ export class BaileysStartupService extends ChannelStartupService { ); } } + + if (key.fromMe) { + const nextStatus = nextMessageStatus(findMessage.status, status[update.status]); + if (nextStatus !== undefined) { + await this.prismaRepository.message.update({ + where: { id: findMessage.id }, + data: { status: nextStatus }, + }); + } + } } this.sendDataWebhook(Events.MESSAGES_UPDATE, message); diff --git a/src/utils/advanceMessageStatus.ts b/src/utils/advanceMessageStatus.ts new file mode 100644 index 0000000000..b0b6b400db --- /dev/null +++ b/src/utils/advanceMessageStatus.ts @@ -0,0 +1,19 @@ +import { status } from '@utils/renderStatus'; + +const FINAL_STATUSES = ['DELETED', 'EDITED']; +const ERROR_STATUS = status[0]; + +const statusRank = (value: string | null | undefined): number => { + const entry = Object.entries(status).find(([, name]) => name === value); + return entry === undefined ? -1 : Number(entry[0]); +}; + +export const nextMessageStatus = (current: string | null | undefined, incoming: string): string | undefined => { + if (current !== null && current !== undefined && FINAL_STATUSES.includes(current)) { + return undefined; + } + if (incoming === ERROR_STATUS) { + return incoming; + } + return statusRank(incoming) > statusRank(current) ? incoming : undefined; +}; diff --git a/test/api/integrations/channel/whatsapp/messagesUpdateOwnStatus.test.ts b/test/api/integrations/channel/whatsapp/messagesUpdateOwnStatus.test.ts new file mode 100644 index 0000000000..ece65a30cc --- /dev/null +++ b/test/api/integrations/channel/whatsapp/messagesUpdateOwnStatus.test.ts @@ -0,0 +1,134 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { Events } from '@api/types/wa.types'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@api/server.module', () => ({ + chatbotController: { emit: vi.fn(async () => undefined) }, + waMonitor: {}, +})); + +vi.mock('@utils/onWhatsappCache', () => ({ + getOnWhatsappCache: vi.fn(async () => []), + saveOnWhatsappCache: vi.fn(async () => undefined), +})); + +vi.mock('@utils/sendTelemetry', () => ({ sendTelemetry: vi.fn() })); + +vi.mock('@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper', () => ({ + chatwootImport: { getRepositoryMessagesCache: vi.fn(() => null), setRepositoryMessagesCache: vi.fn() }, +})); + +const buildCache = () => { + const store = new Map(); + return { + store, + get: vi.fn(async (key: string) => store.get(key)), + set: vi.fn(async (key: string, value: unknown) => { + store.set(key, value); + }), + }; +}; + +const buildService = (baileysCache = buildCache()) => { + const config: Record = { + LOG: { BAILEYS: 'error' }, + DATABASE: { SAVE_DATA: { NEW_MESSAGE: true, HISTORIC: true, MESSAGE_UPDATE: true, CHATS: false, CONTACTS: false } }, + CHATWOOT: { ENABLED: false }, + OPENAI: { ENABLED: false }, + S3: { ENABLE: false }, + }; + const configService = { get: vi.fn((key: string) => config[key]) } as any; + const prismaRepository = { + chat: { findFirst: vi.fn(async () => null), findMany: vi.fn(async () => []), createMany: vi.fn(async () => ({})) }, + message: { + create: vi.fn(async ({ data }) => ({ id: 'row-id', ...data })), + createMany: vi.fn(async () => ({})), + findMany: vi.fn(async () => []), + update: vi.fn(async () => ({})), + }, + messageUpdate: { create: vi.fn(async () => ({})) }, + contact: { findFirst: vi.fn(async () => null) }, + } as any; + const service = new BaileysStartupService( + configService, + { emit: vi.fn() } as any, + prismaRepository, + {} as any, + {} as any, + baileysCache as any, + {} as any, + ) as any; + service.instance.id = 'instance-id'; + service.instance.name = 'instance-name'; + service.sendDataWebhook = vi.fn(); + service.profilePicture = vi.fn(async () => ({ profilePictureUrl: null })); + service.updateMessagesReadedByTimestamp = vi.fn(async () => undefined); + service.updateChatUnreadMessages = vi.fn(async () => undefined); + service.client = { readMessages: vi.fn() }; + service.contactHandle = { 'contacts.upsert': vi.fn(async () => undefined) }; + return { service, prismaRepository, baileysCache }; +}; + +const storedMessage = (id: string, fromMe: boolean, status: string) => ({ + id: `row-${id}`, + key: { id, remoteJid: '5491111111111@s.whatsapp.net', fromMe }, + status, + messageTimestamp: 1700000000, +}); + +const updateWebhooks = (service: any) => + service.sendDataWebhook.mock.calls.filter(([event]) => event === Events.MESSAGES_UPDATE).map(([, data]) => data); + +const runUpdate = async (fromMe: boolean, storedStatus: string, incomingStatus: number, remoteJid?: string) => { + const { service, prismaRepository } = buildService(); + prismaRepository.$queryRaw = vi.fn(async () => [storedMessage('MSG-1', fromMe, storedStatus)]); + await service.messageHandle['messages.update']( + [{ key: { id: 'MSG-1', fromMe, remoteJid }, update: { status: incomingStatus } }], + {}, + ); + return { service, prismaRepository }; +}; + +describe('messages.update status of own messages', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('writes the ack status of a sent message by row id', async () => { + const { prismaRepository } = await runUpdate(true, 'PENDING', 3, '5491111111111@s.whatsapp.net'); + + expect(prismaRepository.message.update).toHaveBeenCalledWith({ + where: { id: 'row-MSG-1' }, + data: { status: 'DELIVERY_ACK' }, + }); + }); + + it('writes the ack status of a sent message without a remote jid in the key', async () => { + const { prismaRepository } = await runUpdate(true, 'SERVER_ACK', 4); + + expect(prismaRepository.message.update).toHaveBeenCalledWith({ + where: { id: 'row-MSG-1' }, + data: { status: 'READ' }, + }); + }); + + it('skips a backward ack but still emits the webhook and records the update', async () => { + const { service, prismaRepository } = await runUpdate(true, 'READ', 3, '5491111111111@s.whatsapp.net'); + + expect(prismaRepository.message.update).not.toHaveBeenCalled(); + expect(updateWebhooks(service)).toHaveLength(1); + expect(updateWebhooks(service)[0]).not.toHaveProperty('key'); + expect(prismaRepository.messageUpdate.create).toHaveBeenCalledTimes(1); + }); + + it('keeps the received message path marking the chat as read', async () => { + const { service, prismaRepository } = await runUpdate(false, 'DELIVERY_ACK', 4, '5491111111111@s.whatsapp.net'); + + expect(service.updateMessagesReadedByTimestamp).toHaveBeenCalledWith('5491111111111@s.whatsapp.net', 1700000000); + expect(prismaRepository.message.update).toHaveBeenCalledWith({ + where: { id: 'row-MSG-1' }, + data: { status: 'READ' }, + }); + expect(prismaRepository.messageUpdate.create).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/utils/advanceMessageStatus.test.ts b/test/utils/advanceMessageStatus.test.ts new file mode 100644 index 0000000000..970074f1cf --- /dev/null +++ b/test/utils/advanceMessageStatus.test.ts @@ -0,0 +1,39 @@ +import { nextMessageStatus } from '@utils/advanceMessageStatus'; +import { describe, expect, it } from 'vitest'; + +describe('nextMessageStatus', () => { + it('writes the first ack when there is no current status', () => { + expect(nextMessageStatus(undefined, 'SERVER_ACK')).toBe('SERVER_ACK'); + expect(nextMessageStatus(null, 'DELIVERY_ACK')).toBe('DELIVERY_ACK'); + }); + + it('treats an unknown current status as the lowest rank', () => { + expect(nextMessageStatus('UNKNOWN', 'PENDING')).toBe('PENDING'); + }); + + it('advances to a higher status', () => { + expect(nextMessageStatus('PENDING', 'SERVER_ACK')).toBe('SERVER_ACK'); + expect(nextMessageStatus('READ', 'PLAYED')).toBe('PLAYED'); + }); + + it('skips a backward ack', () => { + expect(nextMessageStatus('READ', 'DELIVERY_ACK')).toBeUndefined(); + }); + + it('skips an equal status', () => { + expect(nextMessageStatus('DELIVERY_ACK', 'DELIVERY_ACK')).toBeUndefined(); + }); + + it('writes ERROR over non-final statuses', () => { + expect(nextMessageStatus('PENDING', 'ERROR')).toBe('ERROR'); + expect(nextMessageStatus('SERVER_ACK', 'ERROR')).toBe('ERROR'); + }); + + it('never overwrites DELETED or EDITED', () => { + for (const current of ['DELETED', 'EDITED']) { + for (const incoming of ['ERROR', 'PENDING', 'SERVER_ACK', 'DELIVERY_ACK', 'READ', 'PLAYED']) { + expect(nextMessageStatus(current, incoming)).toBeUndefined(); + } + } + }); +}); From 826c325d32b5966a2a9f89cbbd7aab6d6d9abf2d Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 12:14:15 -0300 Subject: [PATCH 85/96] fix(baileys): ignore ERROR after delivery and keep the webhook when the status write fails --- .../channel/whatsapp/whatsapp.baileys.service.ts | 14 ++++++++++---- src/utils/advanceMessageStatus.ts | 3 ++- .../whatsapp/messagesUpdateOwnStatus.test.ts | 16 ++++++++++++++++ test/utils/advanceMessageStatus.test.ts | 6 ++++++ 4 files changed, 34 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 70c143a079..a822676879 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1800,10 +1800,16 @@ export class BaileysStartupService extends ChannelStartupService { if (key.fromMe) { const nextStatus = nextMessageStatus(findMessage.status, status[update.status]); if (nextStatus !== undefined) { - await this.prismaRepository.message.update({ - where: { id: findMessage.id }, - data: { status: nextStatus }, - }); + try { + await this.prismaRepository.message.update({ + where: { id: findMessage.id }, + data: { status: nextStatus }, + }); + } catch (error) { + this.logger.warn( + `Failed to update status of sent message ${findMessage.id}: ${error?.message ?? error}`, + ); + } } } } diff --git a/src/utils/advanceMessageStatus.ts b/src/utils/advanceMessageStatus.ts index b0b6b400db..1efe96630d 100644 --- a/src/utils/advanceMessageStatus.ts +++ b/src/utils/advanceMessageStatus.ts @@ -2,6 +2,7 @@ import { status } from '@utils/renderStatus'; const FINAL_STATUSES = ['DELETED', 'EDITED']; const ERROR_STATUS = status[0]; +const DELIVERY_ACK_STATUS = status[3]; const statusRank = (value: string | null | undefined): number => { const entry = Object.entries(status).find(([, name]) => name === value); @@ -13,7 +14,7 @@ export const nextMessageStatus = (current: string | null | undefined, incoming: return undefined; } if (incoming === ERROR_STATUS) { - return incoming; + return statusRank(current) < statusRank(DELIVERY_ACK_STATUS) ? incoming : undefined; } return statusRank(incoming) > statusRank(current) ? incoming : undefined; }; diff --git a/test/api/integrations/channel/whatsapp/messagesUpdateOwnStatus.test.ts b/test/api/integrations/channel/whatsapp/messagesUpdateOwnStatus.test.ts index ece65a30cc..ee8ee29827 100644 --- a/test/api/integrations/channel/whatsapp/messagesUpdateOwnStatus.test.ts +++ b/test/api/integrations/channel/whatsapp/messagesUpdateOwnStatus.test.ts @@ -121,6 +121,22 @@ describe('messages.update status of own messages', () => { expect(prismaRepository.messageUpdate.create).toHaveBeenCalledTimes(1); }); + it('still emits the webhook and records the update when the status write fails', async () => { + const { service, prismaRepository } = buildService(); + prismaRepository.$queryRaw = vi.fn(async () => [storedMessage('MSG-1', true, 'PENDING')]); + prismaRepository.message.update = vi.fn(async () => { + throw new Error('db down'); + }); + + await service.messageHandle['messages.update']( + [{ key: { id: 'MSG-1', fromMe: true, remoteJid: '5491111111111@s.whatsapp.net' }, update: { status: 3 } }], + {}, + ); + + expect(updateWebhooks(service)).toHaveLength(1); + expect(prismaRepository.messageUpdate.create).toHaveBeenCalledTimes(1); + }); + it('keeps the received message path marking the chat as read', async () => { const { service, prismaRepository } = await runUpdate(false, 'DELIVERY_ACK', 4, '5491111111111@s.whatsapp.net'); diff --git a/test/utils/advanceMessageStatus.test.ts b/test/utils/advanceMessageStatus.test.ts index 970074f1cf..c9703985e7 100644 --- a/test/utils/advanceMessageStatus.test.ts +++ b/test/utils/advanceMessageStatus.test.ts @@ -29,6 +29,12 @@ describe('nextMessageStatus', () => { expect(nextMessageStatus('SERVER_ACK', 'ERROR')).toBe('ERROR'); }); + it('does not write ERROR after the message was delivered', () => { + for (const current of ['DELIVERY_ACK', 'READ', 'PLAYED']) { + expect(nextMessageStatus(current, 'ERROR')).toBeUndefined(); + } + }); + it('never overwrites DELETED or EDITED', () => { for (const current of ['DELETED', 'EDITED']) { for (const incoming of ['ERROR', 'PENDING', 'SERVER_ACK', 'DELIVERY_ACK', 'READ', 'PLAYED']) { From bc3871468976adb94c25d63c52d86d465afbd3c1 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 12:18:33 -0300 Subject: [PATCH 86/96] fix(baileys): advance sent message status with a conditional update --- .../whatsapp/whatsapp.baileys.service.ts | 9 ++++++--- src/utils/advanceMessageStatus.ts | 3 +++ .../whatsapp/messagesUpdateOwnStatus.test.ts | 17 +++++++++++------ test/utils/advanceMessageStatus.test.ts | 8 +++++++- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index a822676879..8302ff4937 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -80,7 +80,7 @@ import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import { Boom } from '@hapi/boom'; import { createId as cuid } from '@paralleldrive/cuid2'; import { Instance, Message } from '@prisma/client'; -import { nextMessageStatus } from '@utils/advanceMessageStatus'; +import { nextMessageStatus, statusesAdvanceableTo } from '@utils/advanceMessageStatus'; import { createJid } from '@utils/createJid'; import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; import { headerAsString } from '@utils/headerAsString'; @@ -1801,8 +1801,11 @@ export class BaileysStartupService extends ChannelStartupService { const nextStatus = nextMessageStatus(findMessage.status, status[update.status]); if (nextStatus !== undefined) { try { - await this.prismaRepository.message.update({ - where: { id: findMessage.id }, + await this.prismaRepository.message.updateMany({ + where: { + id: findMessage.id, + OR: [{ status: null }, { status: { in: statusesAdvanceableTo(nextStatus) } }], + }, data: { status: nextStatus }, }); } catch (error) { diff --git a/src/utils/advanceMessageStatus.ts b/src/utils/advanceMessageStatus.ts index 1efe96630d..cedfbff9bd 100644 --- a/src/utils/advanceMessageStatus.ts +++ b/src/utils/advanceMessageStatus.ts @@ -18,3 +18,6 @@ export const nextMessageStatus = (current: string | null | undefined, incoming: } return statusRank(incoming) > statusRank(current) ? incoming : undefined; }; + +export const statusesAdvanceableTo = (incoming: string): string[] => + Object.values(status).filter((current) => current !== incoming && nextMessageStatus(current, incoming) === incoming); diff --git a/test/api/integrations/channel/whatsapp/messagesUpdateOwnStatus.test.ts b/test/api/integrations/channel/whatsapp/messagesUpdateOwnStatus.test.ts index ee8ee29827..4f74f45403 100644 --- a/test/api/integrations/channel/whatsapp/messagesUpdateOwnStatus.test.ts +++ b/test/api/integrations/channel/whatsapp/messagesUpdateOwnStatus.test.ts @@ -45,6 +45,7 @@ const buildService = (baileysCache = buildCache()) => { createMany: vi.fn(async () => ({})), findMany: vi.fn(async () => []), update: vi.fn(async () => ({})), + updateMany: vi.fn(async () => ({ count: 1 })), }, messageUpdate: { create: vi.fn(async () => ({})) }, contact: { findFirst: vi.fn(async () => null) }, @@ -97,8 +98,8 @@ describe('messages.update status of own messages', () => { it('writes the ack status of a sent message by row id', async () => { const { prismaRepository } = await runUpdate(true, 'PENDING', 3, '5491111111111@s.whatsapp.net'); - expect(prismaRepository.message.update).toHaveBeenCalledWith({ - where: { id: 'row-MSG-1' }, + expect(prismaRepository.message.updateMany).toHaveBeenCalledWith({ + where: { id: 'row-MSG-1', OR: [{ status: null }, { status: { in: ['ERROR', 'PENDING', 'SERVER_ACK'] } }] }, data: { status: 'DELIVERY_ACK' }, }); }); @@ -106,8 +107,11 @@ describe('messages.update status of own messages', () => { it('writes the ack status of a sent message without a remote jid in the key', async () => { const { prismaRepository } = await runUpdate(true, 'SERVER_ACK', 4); - expect(prismaRepository.message.update).toHaveBeenCalledWith({ - where: { id: 'row-MSG-1' }, + expect(prismaRepository.message.updateMany).toHaveBeenCalledWith({ + where: { + id: 'row-MSG-1', + OR: [{ status: null }, { status: { in: ['ERROR', 'PENDING', 'SERVER_ACK', 'DELIVERY_ACK'] } }], + }, data: { status: 'READ' }, }); }); @@ -115,7 +119,7 @@ describe('messages.update status of own messages', () => { it('skips a backward ack but still emits the webhook and records the update', async () => { const { service, prismaRepository } = await runUpdate(true, 'READ', 3, '5491111111111@s.whatsapp.net'); - expect(prismaRepository.message.update).not.toHaveBeenCalled(); + expect(prismaRepository.message.updateMany).not.toHaveBeenCalled(); expect(updateWebhooks(service)).toHaveLength(1); expect(updateWebhooks(service)[0]).not.toHaveProperty('key'); expect(prismaRepository.messageUpdate.create).toHaveBeenCalledTimes(1); @@ -124,7 +128,7 @@ describe('messages.update status of own messages', () => { it('still emits the webhook and records the update when the status write fails', async () => { const { service, prismaRepository } = buildService(); prismaRepository.$queryRaw = vi.fn(async () => [storedMessage('MSG-1', true, 'PENDING')]); - prismaRepository.message.update = vi.fn(async () => { + prismaRepository.message.updateMany = vi.fn(async () => { throw new Error('db down'); }); @@ -145,6 +149,7 @@ describe('messages.update status of own messages', () => { where: { id: 'row-MSG-1' }, data: { status: 'READ' }, }); + expect(prismaRepository.message.updateMany).not.toHaveBeenCalled(); expect(prismaRepository.messageUpdate.create).toHaveBeenCalledTimes(1); }); }); diff --git a/test/utils/advanceMessageStatus.test.ts b/test/utils/advanceMessageStatus.test.ts index c9703985e7..b1cbdad444 100644 --- a/test/utils/advanceMessageStatus.test.ts +++ b/test/utils/advanceMessageStatus.test.ts @@ -1,4 +1,4 @@ -import { nextMessageStatus } from '@utils/advanceMessageStatus'; +import { nextMessageStatus, statusesAdvanceableTo } from '@utils/advanceMessageStatus'; import { describe, expect, it } from 'vitest'; describe('nextMessageStatus', () => { @@ -42,4 +42,10 @@ describe('nextMessageStatus', () => { } } }); + + it('lists the stored statuses an incoming status may replace', () => { + expect(statusesAdvanceableTo('DELIVERY_ACK')).toEqual(['ERROR', 'PENDING', 'SERVER_ACK']); + expect(statusesAdvanceableTo('ERROR')).toEqual(['PENDING', 'SERVER_ACK']); + expect(statusesAdvanceableTo('PENDING')).toEqual(['ERROR']); + }); }); From 95e691932450050497d74a0dc0ff846e99384d49 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 12:20:13 -0300 Subject: [PATCH 87/96] fix(utils): never overwrite an unknown stored message status --- src/utils/advanceMessageStatus.ts | 3 ++- test/utils/advanceMessageStatus.test.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/advanceMessageStatus.ts b/src/utils/advanceMessageStatus.ts index cedfbff9bd..da8e14308f 100644 --- a/src/utils/advanceMessageStatus.ts +++ b/src/utils/advanceMessageStatus.ts @@ -10,7 +10,8 @@ const statusRank = (value: string | null | undefined): number => { }; export const nextMessageStatus = (current: string | null | undefined, incoming: string): string | undefined => { - if (current !== null && current !== undefined && FINAL_STATUSES.includes(current)) { + const hasCurrent = current !== null && current !== undefined; + if (hasCurrent && (FINAL_STATUSES.includes(current) || statusRank(current) === -1)) { return undefined; } if (incoming === ERROR_STATUS) { diff --git a/test/utils/advanceMessageStatus.test.ts b/test/utils/advanceMessageStatus.test.ts index b1cbdad444..d0db0afb8b 100644 --- a/test/utils/advanceMessageStatus.test.ts +++ b/test/utils/advanceMessageStatus.test.ts @@ -7,8 +7,9 @@ describe('nextMessageStatus', () => { expect(nextMessageStatus(null, 'DELIVERY_ACK')).toBe('DELIVERY_ACK'); }); - it('treats an unknown current status as the lowest rank', () => { - expect(nextMessageStatus('UNKNOWN', 'PENDING')).toBe('PENDING'); + it('never overwrites an unknown stored status', () => { + expect(nextMessageStatus('UNKNOWN', 'READ')).toBeUndefined(); + expect(nextMessageStatus('UNKNOWN', 'ERROR')).toBeUndefined(); }); it('advances to a higher status', () => { From 42eed011888a782066ad6799ead9d8588f815d62 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 14:01:14 -0300 Subject: [PATCH 88/96] fix(monitor): close unpaired instances when the QR code limit is reached logout() throws for sockets that were never paired, which skipped closing the socket, resetting the QR count and setting the state to close, so instances stayed connecting and /instance/connect returned an expired QR. The state is now always closed and persisted. --- CHANGELOG.md | 1 + src/api/services/monitor.service.ts | 24 +++- test/api/services/monitorNoConnection.test.ts | 123 ++++++++++++++++++ 3 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 test/api/services/monitorNoConnection.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 481e13a018..981e72778b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes * **Sent message status**: messages sent by the instance now advance `Message.status` from WhatsApp acks instead of staying `PENDING`, so `findChats` `lastMessageStatus` and `findMessages` reflect the real status. Applies to 1:1 chats; Baileys delivers no per-message status for groups. Backward acks are ignored and `DELETED`/`EDITED` are never overwritten +* **QR code limit**: unpaired instances whose QR code limit is reached now end in `close` (memory and database) instead of staying `connecting`, so requesting the QR code again starts a fresh connection ## 0.1.3 (2026-10-24) diff --git a/src/api/services/monitor.service.ts b/src/api/services/monitor.service.ts index 438530b57e..7cd120fcd2 100644 --- a/src/api/services/monitor.service.ts +++ b/src/api/services/monitor.service.ts @@ -7,6 +7,7 @@ import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSess import { Logger } from '@config/logger.config'; import { INSTANCE_DIR, STORE_DIR } from '@config/path.config'; import { NotFoundException } from '@exceptions'; +import { DisconnectReason } from 'baileys'; import { execFileSync } from 'child_process'; import EventEmitter2 from 'eventemitter2'; import { rmSync } from 'fs'; @@ -427,13 +428,28 @@ export class WAMonitoringService { private noConnection() { this.eventEmitter.on('no.connection', async (instanceName) => { + const waInstance = this.waInstances[instanceName]; + if (!waInstance) { + return; + } + try { - await this.waInstances[instanceName]?.client?.logout('Log out instance: ' + instanceName); + try { + await waInstance.client?.logout('Log out instance: ' + instanceName); + } catch (error) { + this.logger.verbose(`Instance "${instanceName}" - logout skipped: ${error?.message ?? error}`); + } - this.waInstances[instanceName]?.client?.ws?.close(); + waInstance.instance.qrcode = { count: 0 }; + waInstance.stateConnection = { state: 'close', statusReason: DisconnectReason.connectionClosed }; - this.waInstances[instanceName].instance.qrcode = { count: 0 }; - this.waInstances[instanceName].stateConnection.state = 'close'; + waInstance.client?.ws?.close(); + waInstance.client?.end?.(undefined); + + await this.prismaRepository.instance.update({ + where: { id: waInstance.instanceId }, + data: { connectionStatus: 'close' }, + }); } catch (error) { this.logger.error({ localError: 'noConnection', diff --git a/test/api/services/monitorNoConnection.test.ts b/test/api/services/monitorNoConnection.test.ts new file mode 100644 index 0000000000..b1d551bbb4 --- /dev/null +++ b/test/api/services/monitorNoConnection.test.ts @@ -0,0 +1,123 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { WAMonitoringService } from '@api/services/monitor.service'; +import { Boom } from '@hapi/boom'; +import { DisconnectReason } from 'baileys'; +import EventEmitter2 from 'eventemitter2'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@api/server.module', () => ({ channelController: {} })); + +const QRCODE_LIMIT = 3; + +const config: Record = { + DATABASE: { SAVE_DATA: { INSTANCE: true } }, + CACHE: { REDIS: { ENABLED: false }, LOCAL: { ENABLED: false } }, + PROVIDER: { ENABLED: false }, + CHATWOOT: { ENABLED: false }, + QRCODE: { LIMIT: QRCODE_LIMIT, COLOR: '#000000' }, +}; + +const buildMonitor = () => { + const eventEmitter = new EventEmitter2(); + const prismaRepository = { instance: { update: vi.fn(async () => ({})) } }; + const configService = { get: vi.fn((key: string) => config[key]) }; + const monitor = new WAMonitoringService( + eventEmitter, + configService as any, + prismaRepository as any, + {} as any, + {} as any, + {} as any, + {} as any, + ); + return { monitor, eventEmitter, prismaRepository, configService }; +}; + +const buildService = (eventEmitter: EventEmitter2, prismaRepository: any, configService: any) => { + const service = Object.create(BaileysStartupService.prototype) as any; + service.client = { logout: vi.fn(async () => undefined), ws: { close: vi.fn() }, end: vi.fn() }; + service.configService = configService; + service.prismaRepository = prismaRepository; + service.instance = { + id: 'instance-id', + name: 'instance-name', + wuid: undefined, + qrcode: { count: QRCODE_LIMIT, code: 'stale-qr', base64: 'stale-base64' }, + }; + service.eventEmitter = eventEmitter; + service.sendDataWebhook = vi.fn(); + service.connectToWhatsapp = vi.fn(); + service.getProfileName = vi.fn(async () => undefined); + service.localChatwoot = { enabled: false }; + service.stateConnection = { state: 'connecting' }; + service.endSession = false; + return service; +}; + +const flushHandlers = () => new Promise((resolve) => setTimeout(resolve, 0)); + +describe('WAMonitoringService no.connection', () => { + let monitor: WAMonitoringService; + let eventEmitter: EventEmitter2; + let prismaRepository: any; + let service: any; + + beforeEach(() => { + const built = buildMonitor(); + monitor = built.monitor; + eventEmitter = built.eventEmitter; + prismaRepository = built.prismaRepository; + service = buildService(eventEmitter, prismaRepository, built.configService); + monitor.waInstances['instance-name'] = service; + }); + + const expectClosed = () => { + expect(service.client.ws.close).toHaveBeenCalled(); + expect(service.client.end).toHaveBeenCalled(); + expect(service.instance.qrcode).toEqual({ count: 0 }); + expect(service.stateConnection).toEqual({ state: 'close', statusReason: DisconnectReason.connectionClosed }); + expect(prismaRepository.instance.update).toHaveBeenCalledWith({ + where: { id: 'instance-id' }, + data: { connectionStatus: 'close' }, + }); + }; + + it('closes the instance when the unpaired socket rejects logout', async () => { + service.client.logout.mockRejectedValue(new Error('Connection Closed')); + + await eventEmitter.emitAsync('no.connection', 'instance-name'); + + expectClosed(); + }); + + it('closes the instance when logout succeeds', async () => { + await eventEmitter.emitAsync('no.connection', 'instance-name'); + + expect(service.client.logout).toHaveBeenCalled(); + expectClosed(); + }); + + it('ignores an unknown instance name', async () => { + await expect(eventEmitter.emitAsync('no.connection', 'missing-instance')).resolves.toBeDefined(); + + expect(prismaRepository.instance.update).not.toHaveBeenCalled(); + }); + + it('does not reconnect after the QR limit closes the socket', async () => { + service.client.logout.mockRejectedValue(new Error('Connection Closed')); + + await service.connectionUpdate({ qr: 'next-qr' }); + await flushHandlers(); + + await service.connectionUpdate({ + connection: 'close', + lastDisconnect: { + error: new Boom('closed', { statusCode: DisconnectReason.connectionClosed }), + date: new Date(), + }, + }); + + expectClosed(); + expect(service.connectToWhatsapp).not.toHaveBeenCalled(); + }); +}); From 433170b6a459ece5a2733a8044a0ce1e5c697b94 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 15:30:09 -0300 Subject: [PATCH 89/96] fix(baileys): do not resend messages deleted for everyone on retry receipts getMessage now returns nothing for deleted messages, incoming revokes mark the stored message DELETED, and deleteMessage only looks up rows of its own instance. Inspired by upstream EvolutionAPI/evolution-api#2751 by @sossei. --- CHANGELOG.md | 1 + .../whatsapp/whatsapp.baileys.service.ts | 31 ++++- .../channel/whatsapp/getMessage.test.ts | 49 ++++++- .../whatsapp/messagesUpdateRevoke.test.ts | 122 ++++++++++++++++++ 4 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 test/api/integrations/channel/whatsapp/messagesUpdateRevoke.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 481e13a018..9b8a7171b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes * **Sent message status**: messages sent by the instance now advance `Message.status` from WhatsApp acks instead of staying `PENDING`, so `findChats` `lastMessageStatus` and `findMessages` reflect the real status. Applies to 1:1 chats; Baileys delivers no per-message status for groups. Backward acks are ignored and `DELETED`/`EDITED` are never overwritten +* **Deleted messages resent on retry**: messages deleted for everyone are no longer resent when a device sends a retry receipt, so deleted content no longer reappears. Incoming revokes now mark the stored message `DELETED` and record a `DELETED` message update; the `MESSAGES_DELETE` webhook payload is unchanged ## 0.1.3 (2026-10-24) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 8302ff4937..3d9aa1fb0c 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -578,6 +578,10 @@ export class BaileysStartupService extends ChannelStartupService { if (!webMessageInfo[0]) { return undefined; } + if (await this.isDeletedMessage(webMessageInfo[0] as any, key)) { + this.logger.verbose(`Skipping retry resend of deleted message: ${key.id}`); + return undefined; + } if (webMessageInfo[0].message?.pollCreationMessage) { const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret; @@ -599,6 +603,16 @@ export class BaileysStartupService extends ChannelStartupService { } } + private async isDeletedMessage(row: { status?: string; key?: { deleted?: boolean } }, key: proto.IMessageKey) { + if (row.status === 'DELETED' || row.key?.deleted === true) { + return true; + } + const deletedUpdate = await this.prismaRepository.messageUpdate.findFirst({ + where: { instanceId: this.instanceId, keyId: key.id, status: 'DELETED' }, + }); + return !!deletedUpdate; + } + private async defineAuthState() { const db = this.configService.get('DATABASE'); const cache = this.configService.get('CACHE'); @@ -1754,6 +1768,21 @@ export class BaileysStartupService extends ChannelStartupService { if (update.message === null && update.status === undefined) { this.sendDataWebhook(Events.MESSAGES_DELETE, { ...key, status: 'DELETED' }); + message.status = 'DELETED'; + + if (findMessage?.id) { + try { + await this.prismaRepository.message.update({ + where: { id: findMessage.id }, + data: { status: 'DELETED' }, + }); + } catch (error) { + this.logger.warn( + `Failed to mark revoked message ${findMessage.id} as deleted: ${error?.message ?? error}`, + ); + } + } + if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) await this.prismaRepository.messageUpdate.create({ data: message }); @@ -3897,7 +3926,7 @@ export class BaileysStartupService extends ChannelStartupService { if (messageId) { const isLogicalDeleted = configService.get('DATABASE').DELETE_DATA.LOGICAL_MESSAGE_DELETE; let message = await this.prismaRepository.message.findFirst({ - where: { key: { path: ['id'], equals: messageId } }, + where: { instanceId: this.instanceId, key: { path: ['id'], equals: messageId } }, }); if (isLogicalDeleted) { if (!message) return response; diff --git a/test/api/integrations/channel/whatsapp/getMessage.test.ts b/test/api/integrations/channel/whatsapp/getMessage.test.ts index ffc9a69286..6fbc9f0978 100644 --- a/test/api/integrations/channel/whatsapp/getMessage.test.ts +++ b/test/api/integrations/channel/whatsapp/getMessage.test.ts @@ -4,7 +4,8 @@ import { describe, expect, it, vi } from 'vitest'; const buildService = (queryRaw: () => Promise) => { const service = Object.create(BaileysStartupService.prototype) as any; service.instance = { id: 'instance-id', name: 'instance-name' }; - service.prismaRepository = { $queryRaw: vi.fn(queryRaw) }; + service.prismaRepository = { $queryRaw: vi.fn(queryRaw), messageUpdate: { findFirst: vi.fn(async () => null) } }; + service.logger = { verbose: vi.fn(), error: vi.fn() }; return service; }; @@ -47,3 +48,49 @@ describe('BaileysStartupService.getMessage', () => { expect(Buffer.from(result.messageContextInfo.messageSecret).toString()).toBe('secret'); }); }); + +describe('BaileysStartupService.getMessage for deleted messages', () => { + const buildDeletedAwareService = (rows: unknown[], deletedUpdate: unknown = null) => { + const service = buildService(async () => rows); + service.prismaRepository.messageUpdate.findFirst = vi.fn(async () => deletedUpdate); + return service; + }; + + it('returns undefined for a row with status DELETED', async () => { + const service = buildDeletedAwareService([{ key, status: 'DELETED', message: { conversation: 'secret' } }]); + + await expect(service.getMessage(key)).resolves.toBeUndefined(); + }); + + it('returns undefined for a row whose key is flagged deleted', async () => { + const service = buildDeletedAwareService([ + { key: { ...key, deleted: true }, status: 'SERVER_ACK', message: { conversation: 'secret' } }, + ]); + + await expect(service.getMessage(key)).resolves.toBeUndefined(); + }); + + it('returns undefined when a DELETED update exists for the instance', async () => { + const service = buildDeletedAwareService([{ key, status: 'SERVER_ACK', message: { conversation: 'secret' } }], { + id: 'update-id', + }); + + await expect(service.getMessage(key)).resolves.toBeUndefined(); + expect(service.prismaRepository.messageUpdate.findFirst).toHaveBeenCalledWith({ + where: { instanceId: 'instance-id', keyId: 'MESSAGE-ID', status: 'DELETED' }, + }); + }); + + it('keeps returning the full row of a deleted message', async () => { + const stored = { key, status: 'DELETED', message: { conversation: 'secret' } }; + const service = buildDeletedAwareService([stored], { id: 'update-id' }); + + await expect(service.getMessage(key, true)).resolves.toEqual(stored); + }); + + it('returns the content of a live message', async () => { + const service = buildDeletedAwareService([{ key, status: 'READ', message: { conversation: 'hello' } }]); + + await expect(service.getMessage(key)).resolves.toEqual({ conversation: 'hello' }); + }); +}); diff --git a/test/api/integrations/channel/whatsapp/messagesUpdateRevoke.test.ts b/test/api/integrations/channel/whatsapp/messagesUpdateRevoke.test.ts new file mode 100644 index 0000000000..a902c747bb --- /dev/null +++ b/test/api/integrations/channel/whatsapp/messagesUpdateRevoke.test.ts @@ -0,0 +1,122 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { Events } from '@api/types/wa.types'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@api/server.module', () => ({ + chatbotController: { emit: vi.fn(async () => undefined) }, + waMonitor: {}, +})); + +vi.mock('@utils/onWhatsappCache', () => ({ + getOnWhatsappCache: vi.fn(async () => []), + saveOnWhatsappCache: vi.fn(async () => undefined), +})); + +vi.mock('@utils/sendTelemetry', () => ({ sendTelemetry: vi.fn() })); + +vi.mock('@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper', () => ({ + chatwootImport: { getRepositoryMessagesCache: vi.fn(() => null), setRepositoryMessagesCache: vi.fn() }, +})); + +const buildCache = () => { + const store = new Map(); + return { + store, + get: vi.fn(async (key: string) => store.get(key)), + set: vi.fn(async (key: string, value: unknown) => { + store.set(key, value); + }), + }; +}; + +const buildService = (baileysCache = buildCache()) => { + const config: Record = { + LOG: { BAILEYS: 'error' }, + DATABASE: { SAVE_DATA: { NEW_MESSAGE: true, HISTORIC: true, MESSAGE_UPDATE: true, CHATS: false, CONTACTS: false } }, + CHATWOOT: { ENABLED: false }, + OPENAI: { ENABLED: false }, + S3: { ENABLE: false }, + }; + const configService = { get: vi.fn((key: string) => config[key]) } as any; + const prismaRepository = { + chat: { findFirst: vi.fn(async () => null), findMany: vi.fn(async () => []), createMany: vi.fn(async () => ({})) }, + message: { + create: vi.fn(async ({ data }) => ({ id: 'row-id', ...data })), + createMany: vi.fn(async () => ({})), + findMany: vi.fn(async () => []), + update: vi.fn(async () => ({})), + updateMany: vi.fn(async () => ({ count: 1 })), + }, + messageUpdate: { create: vi.fn(async () => ({})) }, + contact: { findFirst: vi.fn(async () => null) }, + } as any; + const service = new BaileysStartupService( + configService, + { emit: vi.fn() } as any, + prismaRepository, + {} as any, + {} as any, + baileysCache as any, + {} as any, + ) as any; + service.instance.id = 'instance-id'; + service.instance.name = 'instance-name'; + service.sendDataWebhook = vi.fn(); + service.profilePicture = vi.fn(async () => ({ profilePictureUrl: null })); + service.updateMessagesReadedByTimestamp = vi.fn(async () => undefined); + service.updateChatUnreadMessages = vi.fn(async () => undefined); + service.client = { readMessages: vi.fn() }; + service.contactHandle = { 'contacts.upsert': vi.fn(async () => undefined) }; + return { service, prismaRepository, baileysCache }; +}; + +const remoteJid = '5492615550101@s.whatsapp.net'; + +const runRevoke = async () => { + const { service, prismaRepository } = buildService(); + prismaRepository.$queryRaw = vi.fn(async () => [ + { id: 'row-MSG-1', key: { id: 'MSG-1', remoteJid, fromMe: false }, status: 'DELIVERY_ACK' }, + ]); + await service.messageHandle['messages.update']( + [{ key: { id: 'MSG-1', fromMe: false, remoteJid }, update: { message: null } }], + {}, + ); + return { service, prismaRepository }; +}; + +describe('messages.update revoke', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('records the revoke as a DELETED message update', async () => { + const { prismaRepository } = await runRevoke(); + + expect(prismaRepository.messageUpdate.create).toHaveBeenCalledWith({ + data: expect.objectContaining({ + keyId: 'MSG-1', + messageId: 'row-MSG-1', + status: 'DELETED', + instanceId: 'instance-id', + }), + }); + }); + + it('marks the stored message row as DELETED', async () => { + const { prismaRepository } = await runRevoke(); + + expect(prismaRepository.message.update).toHaveBeenCalledWith({ + where: { id: 'row-MSG-1' }, + data: { status: 'DELETED' }, + }); + }); + + it('keeps the MESSAGES_DELETE webhook payload unchanged', async () => { + const { service } = await runRevoke(); + + const deleteWebhooks = service.sendDataWebhook.mock.calls.filter(([event]) => event === Events.MESSAGES_DELETE); + expect(deleteWebhooks).toEqual([ + [Events.MESSAGES_DELETE, { id: 'MSG-1', fromMe: false, remoteJid, status: 'DELETED' }], + ]); + }); +}); From e84e59028359eaa1dbf3f8b24ad07bb9c3533e83 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 16:06:41 -0300 Subject: [PATCH 90/96] perf(prisma): index message lookups by key id and remote jid Adds the two indexes stenfar had created by hand as migrations with the same names, using CREATE INDEX CONCURRENTLY IF NOT EXISTS so they are a no-op where they exist and do not block writes where they are built. --- CHANGELOG.md | 4 ++++ .../migration.sql | 1 + .../migration.sql | 1 + 3 files changed, 6 insertions(+) create mode 100644 prisma/postgresql-migrations/20261024120000_add_message_instance_key_id_index/migration.sql create mode 100644 prisma/postgresql-migrations/20261024120100_add_message_instance_remotejid_fromme_index/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 3663477541..270e922136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ * **QR code limit**: unpaired instances whose QR code limit is reached now end in `close` (memory and database) instead of staying `connecting`, so requesting the QR code again starts a fresh connection * **Deleted messages resent on retry**: messages deleted for everyone are no longer resent when a device sends a retry receipt, so deleted content no longer reappears. Incoming revokes now mark the stored message `DELETED` and record a `DELETED` message update; the `MESSAGES_DELETE` webhook payload is unchanged +### Performance + +* **Message lookup indexes**: PostgreSQL migrations add `idx_message_instance_key_id` (`instanceId`, `key->>'id'`) and `idx_message_instance_remotejid_fromme` (`instanceId`, `key->>'remoteJid'`, `key->>'fromMe'`) so message updates and read receipts no longer scan every message of the instance. They are created `CONCURRENTLY IF NOT EXISTS`: writes are not blocked and databases that already have them are left untouched + ## 0.1.3 (2026-10-24) ### Security diff --git a/prisma/postgresql-migrations/20261024120000_add_message_instance_key_id_index/migration.sql b/prisma/postgresql-migrations/20261024120000_add_message_instance_key_id_index/migration.sql new file mode 100644 index 0000000000..69e8591d8f --- /dev/null +++ b/prisma/postgresql-migrations/20261024120000_add_message_instance_key_id_index/migration.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_message_instance_key_id" ON "Message" USING btree ("instanceId", (("key" ->> 'id'::text))); diff --git a/prisma/postgresql-migrations/20261024120100_add_message_instance_remotejid_fromme_index/migration.sql b/prisma/postgresql-migrations/20261024120100_add_message_instance_remotejid_fromme_index/migration.sql new file mode 100644 index 0000000000..709bbe10f1 --- /dev/null +++ b/prisma/postgresql-migrations/20261024120100_add_message_instance_remotejid_fromme_index/migration.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_message_instance_remotejid_fromme" ON "Message" USING btree ("instanceId", (("key" ->> 'remoteJid'::text)), ((("key" ->> 'fromMe'::text))::boolean)); From 5409ff05c192fe20b0cb8cbdb2632858342be28e Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 16:06:56 -0300 Subject: [PATCH 91/96] fix(baileys): stop printing message payloads and base64 media to stdout Replaces the unconditional console.log calls in the Baileys service with logger calls that carry only ids, counts and types, including logger calls that passed whole messages with base64 media. --- CHANGELOG.md | 1 + .../whatsapp/whatsapp.baileys.service.ts | 46 ++++--- .../whatsapp/noPayloadConsoleLog.test.ts | 125 ++++++++++++++++++ 3 files changed, 150 insertions(+), 22 deletions(-) create mode 100644 test/api/integrations/channel/whatsapp/noPayloadConsoleLog.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3663477541..501307399b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * **Sent message status**: messages sent by the instance now advance `Message.status` from WhatsApp acks instead of staying `PENDING`, so `findChats` `lastMessageStatus` and `findMessages` reflect the real status. Applies to 1:1 chats; Baileys delivers no per-message status for groups. Backward acks are ignored and `DELETED`/`EDITED` are never overwritten * **QR code limit**: unpaired instances whose QR code limit is reached now end in `close` (memory and database) instead of staying `connecting`, so requesting the QR code again starts a fresh connection * **Deleted messages resent on retry**: messages deleted for everyone are no longer resent when a device sends a retry receipt, so deleted content no longer reappears. Incoming revokes now mark the stored message `DELETED` and record a `DELETED` message update; the `MESSAGES_DELETE` webhook payload is unchanged +* **Payload logging**: the Baileys channel no longer writes full message payloads (including media base64) to stdout on every received message, message update, call or history sync; these logs now go through the logger at `VERBOSE`/`DEBUG` level and include only ids and counts. Webhook payloads are unchanged ## 0.1.3 (2026-10-24) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 3d9aa1fb0c..91dc12abe6 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -764,13 +764,13 @@ export class BaileysStartupService extends ChannelStartupService { this.eventHandler(); this.client.ws.on('CB:call', (packet) => { - console.log('CB:call', packet); + this.logger.verbose(`CB:call received: ${packet?.attrs?.id ?? 'unknown'}`); const payload = { event: 'CB:call', packet: packet }; this.sendDataWebhook(Events.CALL, payload, true, ['websocket']); }); this.client.ws.on('CB:ack,class:call', (packet) => { - console.log('CB:ack,class:call', packet); + this.logger.verbose(`CB:ack,class:call received: ${packet?.attrs?.id ?? 'unknown'}`); const payload = { event: 'CB:ack,class:call', packet: packet }; this.sendDataWebhook(Events.CALL, payload, true, ['websocket']); }); @@ -1026,9 +1026,9 @@ export class BaileysStartupService extends ChannelStartupService { }) => { try { if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) { - console.log('received on-demand history sync, messages=', messages); + this.logger.verbose(`Received on-demand history sync, messages: ${messages.length}`); } - console.log( + this.logger.verbose( `recv ${chats.length} chats, ${contacts.length} contacts, ${messages.length} msgs (is latest: ${isLatest}, progress: ${progress}%), type: ${syncType}`, ); @@ -1212,14 +1212,16 @@ export class BaileysStartupService extends ChannelStartupService { if (text == 'requestPlaceholder' && !requestId) { const messageId = await this.client.requestPlaceholderResend(received.key); - console.log('requested placeholder resync, id=', messageId); + this.logger.verbose(`Requested placeholder resync, id: ${messageId}`); } else if (requestId) { - console.log('Message received from phone, id=', requestId, received); + this.logger.verbose( + `Message received from phone, request id: ${requestId}, message id: ${received.key?.id}`, + ); } if (text == 'onDemandHistSync') { const messageId = await this.client.fetchMessageHistory(50, received.key, received.messageTimestamp!); - console.log('requested on-demand sync, id=', messageId); + this.logger.verbose(`Requested on-demand sync, id: ${messageId}`); } } @@ -1305,7 +1307,7 @@ export class BaileysStartupService extends ChannelStartupService { data: { name: received.pushName }, }); } catch { - console.log(`Chat insert record ignored: ${received.key.remoteJid} - ${this.instanceId}`); + this.logger.verbose(`Chat insert record ignored: ${received.key.remoteJid} - ${this.instanceId}`); } } } @@ -1581,15 +1583,13 @@ export class BaileysStartupService extends ChannelStartupService { } } - this.logger.verbose(messageRaw); + this.logger.verbose(`Message received: ${messageRaw.key?.id} (${messageRaw.messageType})`); sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`); const phoneRemoteJid = resolvePhoneRemoteJid(messageRaw.key); if (phoneRemoteJid !== messageRaw.key.remoteJid) { messageRaw.key.remoteJid = phoneRemoteJid; } - console.log(messageRaw); - this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); await this.markMessageSeen(received.key); @@ -1668,7 +1668,7 @@ export class BaileysStartupService extends ChannelStartupService { }, 'messages.update': async (args: { update: Partial; key: WAMessageKey }[], settings: any) => { - this.logger.verbose(`Update messages ${JSON.stringify(args, undefined, 2)}`); + this.logger.verbose(`Update messages: ${args.length}`); const readChatToUpdate: Record = {}; // {remoteJid: true} @@ -1682,7 +1682,7 @@ export class BaileysStartupService extends ChannelStartupService { const cached = await this.baileysCache.get(updateKey); const secondsSinceEpoch = Math.floor(Date.now() / 1000); - console.log('CACHE:', { cached, updateKey, messageTimestamp: update.messageTimestamp, secondsSinceEpoch }); + this.logger.debug(`Message update cache check: ${updateKey}, cached: ${cached}`); if ( (update.messageTimestamp && update.messageTimestamp === cached) || @@ -1866,7 +1866,9 @@ export class BaileysStartupService extends ChannelStartupService { try { await this.prismaRepository.chat.update({ where: { id: existingChat.id }, data: chatToInsert }); } catch { - console.log(`Chat insert record ignored: ${chatToInsert.remoteJid} - ${chatToInsert.instanceId}`); + this.logger.verbose( + `Chat insert record ignored: ${chatToInsert.remoteJid} - ${chatToInsert.instanceId}`, + ); } } } @@ -2692,7 +2694,7 @@ export class BaileysStartupService extends ChannelStartupService { } } - this.logger.verbose(messageSent); + this.logger.verbose(`Message sent: ${messageSent?.key?.id} (${messageRaw.messageType})`); this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); @@ -3303,7 +3305,7 @@ export class BaileysStartupService extends ChannelStartupService { }); outputAudioStream.on('error', (error) => { - console.log('error', error); + this.logger.error(['Error in output audio stream', error?.message]); reject(error); }); @@ -3345,8 +3347,8 @@ export class BaileysStartupService extends ChannelStartupService { '0', ]) .pipe(outputAudioStream, { end: true }) - .on('error', function (error) { - console.log('error', error); + .on('error', (error) => { + this.logger.error(['Error in ffmpeg audio conversion', error?.message]); reject(error); }); }); @@ -4459,7 +4461,7 @@ export class BaileysStartupService extends ChannelStartupService { if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) { if (await groupMetadataCache?.has(groupJid)) { - console.log(`Cache request for group: ${groupJid}`); + this.logger.verbose(`Cache request for group: ${groupJid}`); const meta = await groupMetadataCache.get(groupJid); if (Date.now() - meta.timestamp > 3600000) { @@ -4469,7 +4471,7 @@ export class BaileysStartupService extends ChannelStartupService { return meta.data; } - console.log(`Cache request for group: ${groupJid} - not found`); + this.logger.verbose(`Cache request for group: ${groupJid} - not found`); return await this.updateGroupMetadataCache(groupJid); } @@ -5010,7 +5012,7 @@ export class BaileysStartupService extends ChannelStartupService { } public async baileysSendNode(stanza: any) { - console.log('stanza', JSON.stringify(stanza)); + this.logger.verbose(`Sending stanza: ${stanza?.tag}`); const response = await this.client.sendNode(stanza); return response; @@ -5094,7 +5096,7 @@ export class BaileysStartupService extends ChannelStartupService { catalog: productsCatalog, }; } catch (error) { - console.log(error); + this.logger.error(['Error fetching business profile', error?.message]); return { wuid: jid, name: null, isBusiness: false }; } } diff --git a/test/api/integrations/channel/whatsapp/noPayloadConsoleLog.test.ts b/test/api/integrations/channel/whatsapp/noPayloadConsoleLog.test.ts new file mode 100644 index 0000000000..54085ed50e --- /dev/null +++ b/test/api/integrations/channel/whatsapp/noPayloadConsoleLog.test.ts @@ -0,0 +1,125 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { Events } from '@api/types/wa.types'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('baileys', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + downloadMediaMessage: vi.fn(async () => Buffer.from('fictional-image-bytes')), + }; +}); + +vi.mock('@api/server.module', () => ({ + chatbotController: { emit: vi.fn(async () => undefined) }, + waMonitor: {}, +})); + +vi.mock('@utils/onWhatsappCache', () => ({ + getOnWhatsappCache: vi.fn(async () => []), + saveOnWhatsappCache: vi.fn(async () => undefined), +})); + +vi.mock('@utils/sendTelemetry', () => ({ sendTelemetry: vi.fn() })); + +vi.mock('@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper', () => ({ + chatwootImport: { getRepositoryMessagesCache: vi.fn(() => null), setRepositoryMessagesCache: vi.fn() }, +})); + +const buildCache = () => { + const store = new Map(); + return { + store, + get: vi.fn(async (key: string) => store.get(key)), + set: vi.fn(async (key: string, value: unknown) => { + store.set(key, value); + }), + }; +}; +const buildService = () => { + const config: Record = { + LOG: { BAILEYS: 'error' }, + DATABASE: { SAVE_DATA: { NEW_MESSAGE: true, HISTORIC: true, MESSAGE_UPDATE: true, CHATS: false, CONTACTS: false } }, + CHATWOOT: { ENABLED: false }, + OPENAI: { ENABLED: false }, + S3: { ENABLE: false }, + }; + const configService = { get: vi.fn((key: string) => config[key]) } as any; + const prismaRepository = { + chat: { findFirst: vi.fn(async () => null), findMany: vi.fn(async () => []), createMany: vi.fn(async () => ({})) }, + message: { + create: vi.fn(async ({ data }) => ({ id: 'row-id', ...data })), + createMany: vi.fn(async () => ({})), + findMany: vi.fn(async () => []), + update: vi.fn(async () => ({})), + updateMany: vi.fn(async () => ({ count: 1 })), + }, + messageUpdate: { create: vi.fn(async () => ({})) }, + contact: { findFirst: vi.fn(async () => null) }, + $queryRaw: vi.fn(async () => []), + } as any; + const service = new BaileysStartupService( + configService, + { emit: vi.fn() } as any, + prismaRepository, + {} as any, + {} as any, + buildCache() as any, + {} as any, + ) as any; + service.instance.id = 'instance-id'; + service.instance.name = 'instance-name'; + service.localWebhook = { enabled: true, webhookBase64: true }; + service.sendDataWebhook = vi.fn(); + service.profilePicture = vi.fn(async () => ({ profilePictureUrl: null })); + service.updateMessagesReadedByTimestamp = vi.fn(async () => undefined); + service.updateChatUnreadMessages = vi.fn(async () => undefined); + service.client = { readMessages: vi.fn(), updateMediaMessage: vi.fn() }; + service.contactHandle = { 'contacts.upsert': vi.fn(async () => undefined) }; + return service; +}; + +const MEDIA_BASE64 = Buffer.from('fictional-image-bytes').toString('base64'); + +const imageMessage = { + key: { id: 'MSG-IMG-1', fromMe: false, remoteJid: '5492615550101@s.whatsapp.net' }, + message: { imageMessage: { mimetype: 'image/jpeg', caption: 'photo', url: 'https://example.invalid/media' } }, + messageTimestamp: 1700000000, + pushName: 'Someone', +}; + +const loggerCallArgs = (service: any) => + Object.values(service.logger) + .filter((member: any) => typeof member === 'function' && 'mock' in member) + .flatMap((spy: any) => spy.mock.calls.flat()); + +const serialize = (value: unknown) => (typeof value === 'string' ? value : (JSON.stringify(value) ?? '')); + +describe('baileys message handlers do not dump payloads to console', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('never calls console.log nor logs media base64 on messages.upsert and messages.update', async () => { + const service = buildService(); + const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined); + for (const level of ['log', 'info', 'warn', 'error', 'verbose', 'debug']) { + vi.spyOn(service.logger, level).mockImplementation(() => undefined); + } + + await service.messageHandle['messages.upsert']({ messages: [imageMessage], type: 'notify' }, {}); + await service.messageHandle['messages.update']( + [{ key: { id: 'MSG-IMG-1', fromMe: false, remoteJid: '5492615550101@s.whatsapp.net' }, update: { status: 3 } }], + {}, + ); + + const upsertPayload = service.sendDataWebhook.mock.calls.find(([event]) => event === Events.MESSAGES_UPSERT)?.[1]; + expect(upsertPayload?.message?.base64).toBe(MEDIA_BASE64); + expect(consoleLog).not.toHaveBeenCalled(); + expect(loggerCallArgs(service).map(serialize).join('\n')).not.toContain(MEDIA_BASE64); + }); +}); From e14ae1be11bf5ba7279aa4ba9db96b7767419b84 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 16:09:10 -0300 Subject: [PATCH 92/96] perf(prisma): build the message indexes inside the migration transaction --- CHANGELOG.md | 2 +- .../migration.sql | 2 +- .../migration.sql | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 270e922136..cc10bf15ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Performance -* **Message lookup indexes**: PostgreSQL migrations add `idx_message_instance_key_id` (`instanceId`, `key->>'id'`) and `idx_message_instance_remotejid_fromme` (`instanceId`, `key->>'remoteJid'`, `key->>'fromMe'`) so message updates and read receipts no longer scan every message of the instance. They are created `CONCURRENTLY IF NOT EXISTS`: writes are not blocked and databases that already have them are left untouched +* **Message lookup indexes**: PostgreSQL migrations add `idx_message_instance_key_id` (`instanceId`, `key->>'id'`) and `idx_message_instance_remotejid_fromme` (`instanceId`, `key->>'remoteJid'`, `key->>'fromMe'`) so message updates and read receipts no longer scan every message of the instance. They are created `IF NOT EXISTS` inside the migration transaction, which runs before the server starts, so databases that already have them are left untouched and a failed build leaves no invalid index ## 0.1.3 (2026-10-24) diff --git a/prisma/postgresql-migrations/20261024120000_add_message_instance_key_id_index/migration.sql b/prisma/postgresql-migrations/20261024120000_add_message_instance_key_id_index/migration.sql index 69e8591d8f..95c4d39538 100644 --- a/prisma/postgresql-migrations/20261024120000_add_message_instance_key_id_index/migration.sql +++ b/prisma/postgresql-migrations/20261024120000_add_message_instance_key_id_index/migration.sql @@ -1 +1 @@ -CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_message_instance_key_id" ON "Message" USING btree ("instanceId", (("key" ->> 'id'::text))); +CREATE INDEX IF NOT EXISTS "idx_message_instance_key_id" ON "Message" USING btree ("instanceId", (("key" ->> 'id'::text))); diff --git a/prisma/postgresql-migrations/20261024120100_add_message_instance_remotejid_fromme_index/migration.sql b/prisma/postgresql-migrations/20261024120100_add_message_instance_remotejid_fromme_index/migration.sql index 709bbe10f1..ed58c2917a 100644 --- a/prisma/postgresql-migrations/20261024120100_add_message_instance_remotejid_fromme_index/migration.sql +++ b/prisma/postgresql-migrations/20261024120100_add_message_instance_remotejid_fromme_index/migration.sql @@ -1 +1 @@ -CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_message_instance_remotejid_fromme" ON "Message" USING btree ("instanceId", (("key" ->> 'remoteJid'::text)), ((("key" ->> 'fromMe'::text))::boolean)); +CREATE INDEX IF NOT EXISTS "idx_message_instance_remotejid_fromme" ON "Message" USING btree ("instanceId", (("key" ->> 'remoteJid'::text)), ((("key" ->> 'fromMe'::text))::boolean)); From 5e98cee80cb8528614bb1c9854b54dd786a453e2 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 16:09:10 -0300 Subject: [PATCH 93/96] fix(baileys): scope unread counts and message edits to the instance updateChatUnreadMessages and the edit lookup matched rows of any instance with the same remote jid or key id, so another instance's chat or message could be updated. The edit guards now throw explicitly. --- CHANGELOG.md | 1 + .../whatsapp/whatsapp.baileys.service.ts | 8 +- .../channel/whatsapp/instanceScoping.test.ts | 109 ++++++++++++++++++ 3 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 test/api/integrations/channel/whatsapp/instanceScoping.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3663477541..b7e434c76f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * **Sent message status**: messages sent by the instance now advance `Message.status` from WhatsApp acks instead of staying `PENDING`, so `findChats` `lastMessageStatus` and `findMessages` reflect the real status. Applies to 1:1 chats; Baileys delivers no per-message status for groups. Backward acks are ignored and `DELETED`/`EDITED` are never overwritten * **QR code limit**: unpaired instances whose QR code limit is reached now end in `close` (memory and database) instead of staying `connecting`, so requesting the QR code again starts a fresh connection * **Deleted messages resent on retry**: messages deleted for everyone are no longer resent when a device sends a retry receipt, so deleted content no longer reappears. Incoming revokes now mark the stored message `DELETED` and record a `DELETED` message update; the `MESSAGES_DELETE` webhook payload is unchanged +* **Instance-scoped chat and edit lookups**: recalculating a chat's unread count and editing a sent message now only look up rows of the current instance, so another instance's chat or message with the same `remoteJid` or message id is no longer updated ## 0.1.3 (2026-10-24) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 3d9aa1fb0c..f0d0e78780 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -4346,15 +4346,15 @@ export class BaileysStartupService extends ChannelStartupService { const messageId = messageSent.message?.protocolMessage?.key?.id; if (messageId && this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { let message = await this.prismaRepository.message.findFirst({ - where: { key: { path: ['id'], equals: messageId } }, + where: { instanceId: this.instanceId, key: { path: ['id'], equals: messageId } }, }); if (!message) throw new NotFoundException('Message not found'); if (!(message.key.valueOf() as any).fromMe) { - new BadRequestException('You cannot edit others messages'); + throw new BadRequestException('You cannot edit others messages'); } if ((message.key.valueOf() as any)?.deleted) { - new BadRequestException('You cannot edit deleted messages'); + throw new BadRequestException('You cannot edit deleted messages'); } if (oldMessage.messageType === 'conversation' || oldMessage.messageType === 'extendedTextMessage') { @@ -4908,7 +4908,7 @@ export class BaileysStartupService extends ChannelStartupService { private async updateChatUnreadMessages(remoteJid: string): Promise { const [chat, unreadMessages] = await Promise.all([ - this.prismaRepository.chat.findFirst({ where: { remoteJid } }), + this.prismaRepository.chat.findFirst({ where: { instanceId: this.instanceId, remoteJid } }), // Use raw SQL to avoid JSON path issues this.prismaRepository.$queryRaw` SELECT COUNT(*)::int as count FROM "Message" diff --git a/test/api/integrations/channel/whatsapp/instanceScoping.test.ts b/test/api/integrations/channel/whatsapp/instanceScoping.test.ts new file mode 100644 index 0000000000..ca6e702df0 --- /dev/null +++ b/test/api/integrations/channel/whatsapp/instanceScoping.test.ts @@ -0,0 +1,109 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('@api/server.module', () => ({ + chatbotController: { emit: vi.fn(async () => undefined) }, + waMonitor: {}, +})); + +vi.mock('@utils/onWhatsappCache', () => ({ + getOnWhatsappCache: vi.fn(async () => []), + saveOnWhatsappCache: vi.fn(async () => undefined), +})); + +vi.mock('@utils/sendTelemetry', () => ({ sendTelemetry: vi.fn() })); + +vi.mock('@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper', () => ({ + chatwootImport: { getRepositoryMessagesCache: vi.fn(() => null), setRepositoryMessagesCache: vi.fn() }, +})); + +const remoteJid = '5492615550101@s.whatsapp.net'; + +const buildService = () => { + const config: Record = { + LOG: { BAILEYS: 'error' }, + DATABASE: { SAVE_DATA: { NEW_MESSAGE: true, HISTORIC: true, MESSAGE_UPDATE: true, CHATS: false, CONTACTS: false } }, + CHATWOOT: { ENABLED: false }, + OPENAI: { ENABLED: false }, + S3: { ENABLE: false }, + }; + const configService = { get: vi.fn((key: string) => config[key]) } as any; + const prismaRepository = { + chat: { findFirst: vi.fn(async () => null), update: vi.fn(async () => ({})) }, + message: { findFirst: vi.fn(async () => null), update: vi.fn(async ({ data }) => ({ id: 'row-id', ...data })) }, + messageUpdate: { create: vi.fn(async () => ({})) }, + $queryRaw: vi.fn(async () => [{ count: 0 }]), + } as any; + const service = new BaileysStartupService( + configService, + { emit: vi.fn() } as any, + prismaRepository, + {} as any, + {} as any, + { get: vi.fn(), set: vi.fn() } as any, + {} as any, + ) as any; + service.instance.id = 'instance-id'; + service.instance.name = 'instance-name'; + service.sendDataWebhook = vi.fn(); + return { service, prismaRepository }; +}; + +describe('updateChatUnreadMessages instance scoping', () => { + it('looks up the chat within the current instance', async () => { + const { service, prismaRepository } = buildService(); + + await service.updateChatUnreadMessages(remoteJid); + + expect(prismaRepository.chat.findFirst).toHaveBeenCalledWith({ + where: { instanceId: 'instance-id', remoteJid }, + }); + }); +}); + +describe('updateMessage instance scoping and guards', () => { + const buildEdit = (storedKey: Record) => { + const { service, prismaRepository } = buildService(); + service.formatUpdateMessage = vi.fn(async () => ({ text: 'edited' })); + service.getMessage = vi.fn(async () => ({ + key: { id: 'MSG-1', remoteJid, fromMe: true }, + messageType: 'conversation', + message: { conversation: 'original' }, + messageTimestamp: 1, + })); + service.client = { + sendMessage: vi.fn(async () => ({ + key: { id: 'EDIT-1', remoteJid, fromMe: true }, + message: { protocolMessage: { key: { id: 'MSG-1', remoteJid, fromMe: true } } }, + })), + }; + prismaRepository.message.findFirst = vi.fn(async () => ({ id: 'row-MSG-1', key: storedKey })); + const data = { number: '5492615550101', text: 'edited', key: { id: 'MSG-1', remoteJid, fromMe: true } }; + return { service, prismaRepository, data }; + }; + + it('looks up the edited message within the current instance', async () => { + const { service, prismaRepository, data } = buildEdit({ id: 'MSG-1', remoteJid, fromMe: true }); + + await service.updateMessage(data); + + expect(prismaRepository.message.findFirst).toHaveBeenCalledWith({ + where: { instanceId: 'instance-id', key: { path: ['id'], equals: 'MSG-1' } }, + }); + expect(prismaRepository.message.update).toHaveBeenCalledTimes(1); + }); + + it('rejects editing a message not sent by the instance and does not update it', async () => { + const { service, prismaRepository, data } = buildEdit({ id: 'MSG-1', remoteJid, fromMe: false }); + + await expect(service.updateMessage(data)).rejects.toThrow('You cannot edit others messages'); + expect(prismaRepository.message.update).not.toHaveBeenCalled(); + }); + + it('rejects editing a deleted message and does not update it', async () => { + const { service, prismaRepository, data } = buildEdit({ id: 'MSG-1', remoteJid, fromMe: true, deleted: true }); + + await expect(service.updateMessage(data)).rejects.toThrow('You cannot edit deleted messages'); + expect(prismaRepository.message.update).not.toHaveBeenCalled(); + }); +}); From 9f95ca184f8614a8bef5f808e4b81900298f3e97 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Sat, 24 Oct 2026 16:09:10 -0300 Subject: [PATCH 94/96] fix(baileys): isolate errors per message, update and event type One failing message in messages.upsert or messages.update dropped the rest of the batch and their webhooks, and early returns in presence and label handling skipped later event types of the same Baileys batch. --- CHANGELOG.md | 1 + .../whatsapp/whatsapp.baileys.service.ts | 482 ++++++++++-------- .../whatsapp/eventErrorIsolation.test.ts | 186 +++++++ 3 files changed, 452 insertions(+), 217 deletions(-) create mode 100644 test/api/integrations/channel/whatsapp/eventErrorIsolation.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3663477541..205d60112b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * **Sent message status**: messages sent by the instance now advance `Message.status` from WhatsApp acks instead of staying `PENDING`, so `findChats` `lastMessageStatus` and `findMessages` reflect the real status. Applies to 1:1 chats; Baileys delivers no per-message status for groups. Backward acks are ignored and `DELETED`/`EDITED` are never overwritten * **QR code limit**: unpaired instances whose QR code limit is reached now end in `close` (memory and database) instead of staying `connecting`, so requesting the QR code again starts a fresh connection * **Deleted messages resent on retry**: messages deleted for everyone are no longer resent when a device sends a retry receipt, so deleted content no longer reappears. Incoming revokes now mark the stored message `DELETED` and record a `DELETED` message update; the `MESSAGES_DELETE` webhook payload is unchanged +* **Event error isolation**: a failure while processing one message of a `messages.upsert` or `messages.update` batch no longer drops the rest of the batch and their webhooks, and a failing or ignored event type (for example a group `presence.update` with `groupsIgnore`, or a label event) no longer skips the other event types delivered with it. Webhook payloads and event order are unchanged ## 0.1.3 (2026-10-24) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 3d9aa1fb0c..6a3b934626 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1185,8 +1185,8 @@ export class BaileysStartupService extends ChannelStartupService { { messages, type, requestId }: { messages: WAMessage[]; type: MessageUpsertType; requestId?: string }, settings: any, ) => { - try { - for (const received of messages) { + for (const received of messages) { + try { if ( received?.messageStubParameters?.some?.((param) => [ @@ -1661,9 +1661,9 @@ export class BaileysStartupService extends ChannelStartupService { update: contactRaw, create: contactRaw, }); + } catch (error) { + this.logger.error(`Failed to process upserted message ${received?.key?.id}: ${error?.message ?? error}`); } - } catch (error) { - this.logger.error(error); } }, @@ -1673,203 +1673,207 @@ export class BaileysStartupService extends ChannelStartupService { const readChatToUpdate: Record = {}; // {remoteJid: true} for await (const { key, update } of args) { - if (settings?.groupsIgnore && key.remoteJid?.includes('@g.us')) { - continue; - } + try { + if (settings?.groupsIgnore && key.remoteJid?.includes('@g.us')) { + continue; + } - const updateKey = `${this.instance.id}_${key.id}_${update.status}`; + const updateKey = `${this.instance.id}_${key.id}_${update.status}`; - const cached = await this.baileysCache.get(updateKey); + const cached = await this.baileysCache.get(updateKey); - const secondsSinceEpoch = Math.floor(Date.now() / 1000); - console.log('CACHE:', { cached, updateKey, messageTimestamp: update.messageTimestamp, secondsSinceEpoch }); + const secondsSinceEpoch = Math.floor(Date.now() / 1000); + console.log('CACHE:', { cached, updateKey, messageTimestamp: update.messageTimestamp, secondsSinceEpoch }); - if ( - (update.messageTimestamp && update.messageTimestamp === cached) || - (!update.messageTimestamp && secondsSinceEpoch === cached) - ) { - this.logger.info(`Update Message duplicated ignored [avoid deadlock]: ${updateKey}`); - continue; - } + if ( + (update.messageTimestamp && update.messageTimestamp === cached) || + (!update.messageTimestamp && secondsSinceEpoch === cached) + ) { + this.logger.info(`Update Message duplicated ignored [avoid deadlock]: ${updateKey}`); + continue; + } - if (update.messageTimestamp) { - await this.baileysCache.set(updateKey, update.messageTimestamp, 30 * 60); - } else { - await this.baileysCache.set(updateKey, secondsSinceEpoch, 30 * 60); - } + if (update.messageTimestamp) { + await this.baileysCache.set(updateKey, update.messageTimestamp, 30 * 60); + } else { + await this.baileysCache.set(updateKey, secondsSinceEpoch, 30 * 60); + } - if (status[update.status] === 'READ' && key.fromMe) { - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { - this.chatwootService.eventWhatsapp( - 'messages.read', - { instanceName: this.instance.name, instanceId: this.instanceId }, - { key: key }, - ); + if (status[update.status] === 'READ' && key.fromMe) { + if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { + this.chatwootService.eventWhatsapp( + 'messages.read', + { instanceName: this.instance.name, instanceId: this.instanceId }, + { key: key }, + ); + } } - } - if (key.remoteJid !== 'status@broadcast' && key.id !== undefined) { - let pollUpdates: any; + if (key.remoteJid !== 'status@broadcast' && key.id !== undefined) { + let pollUpdates: any; - if (update.pollUpdates) { - const pollCreation = await this.getMessage(key); + if (update.pollUpdates) { + const pollCreation = await this.getMessage(key); - if (pollCreation) { - pollUpdates = getAggregateVotesInPollMessage({ - message: pollCreation as proto.IMessage, - pollUpdates: update.pollUpdates, - }); + if (pollCreation) { + pollUpdates = getAggregateVotesInPollMessage({ + message: pollCreation as proto.IMessage, + pollUpdates: update.pollUpdates, + }); + } } - } - const message: any = { - keyId: key.id, - remoteJid: key?.remoteJid, - fromMe: key.fromMe, - participant: key?.participant, - status: status[update.status] ?? 'SERVER_ACK', - pollUpdates, - instanceId: this.instanceId, - }; + const message: any = { + keyId: key.id, + remoteJid: key?.remoteJid, + fromMe: key.fromMe, + participant: key?.participant, + status: status[update.status] ?? 'SERVER_ACK', + pollUpdates, + instanceId: this.instanceId, + }; - if (update.message) { - message.message = update.message; - } + if (update.message) { + message.message = update.message; + } - let findMessage: any; - const configDatabaseData = this.configService.get('DATABASE').SAVE_DATA; - if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) { - // Use raw SQL to avoid JSON path issues - const protocolMapKey = `protocol_${key.id}`; - const originalMessageId = (await this.baileysCache.get(protocolMapKey)) as string; + let findMessage: any; + const configDatabaseData = this.configService.get('DATABASE').SAVE_DATA; + if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) { + // Use raw SQL to avoid JSON path issues + const protocolMapKey = `protocol_${key.id}`; + const originalMessageId = (await this.baileysCache.get(protocolMapKey)) as string; - if (originalMessageId) { - message.keyId = originalMessageId; - } + if (originalMessageId) { + message.keyId = originalMessageId; + } - const searchId = originalMessageId || key.id; + const searchId = originalMessageId || key.id; - const messages = (await this.prismaRepository.$queryRaw` - SELECT * FROM "Message" - WHERE "instanceId" = ${this.instanceId} - AND "key"->>'id' = ${searchId} - LIMIT 1 - `) as any[]; - findMessage = messages[0] || null; + const messages = (await this.prismaRepository.$queryRaw` + SELECT * FROM "Message" + WHERE "instanceId" = ${this.instanceId} + AND "key"->>'id' = ${searchId} + LIMIT 1 + `) as any[]; + findMessage = messages[0] || null; - if (!findMessage?.id) { - this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`); - continue; + if (!findMessage?.id) { + this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`); + continue; + } + message.messageId = findMessage.id; + message.remoteJid = message.remoteJid ?? findMessage.key?.remoteJid; } - message.messageId = findMessage.id; - message.remoteJid = message.remoteJid ?? findMessage.key?.remoteJid; - } - if (update.message === null && update.status === undefined) { - this.sendDataWebhook(Events.MESSAGES_DELETE, { ...key, status: 'DELETED' }); + if (update.message === null && update.status === undefined) { + this.sendDataWebhook(Events.MESSAGES_DELETE, { ...key, status: 'DELETED' }); - message.status = 'DELETED'; + message.status = 'DELETED'; - if (findMessage?.id) { - try { - await this.prismaRepository.message.update({ - where: { id: findMessage.id }, - data: { status: 'DELETED' }, - }); - } catch (error) { - this.logger.warn( - `Failed to mark revoked message ${findMessage.id} as deleted: ${error?.message ?? error}`, - ); + if (findMessage?.id) { + try { + await this.prismaRepository.message.update({ + where: { id: findMessage.id }, + data: { status: 'DELETED' }, + }); + } catch (error) { + this.logger.warn( + `Failed to mark revoked message ${findMessage.id} as deleted: ${error?.message ?? error}`, + ); + } } - } - if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) - await this.prismaRepository.messageUpdate.create({ data: message }); + if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) + await this.prismaRepository.messageUpdate.create({ data: message }); - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { - this.chatwootService.eventWhatsapp( - Events.MESSAGES_DELETE, - { instanceName: this.instance.name, instanceId: this.instanceId }, - { key: key }, - ); + if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { + this.chatwootService.eventWhatsapp( + Events.MESSAGES_DELETE, + { instanceName: this.instance.name, instanceId: this.instanceId }, + { key: key }, + ); + } + + continue; } - continue; - } + if (findMessage && update.status !== undefined && status[update.status] !== findMessage.status) { + if (!key.fromMe && key.remoteJid) { + readChatToUpdate[key.remoteJid] = true; - if (findMessage && update.status !== undefined && status[update.status] !== findMessage.status) { - if (!key.fromMe && key.remoteJid) { - readChatToUpdate[key.remoteJid] = true; + const { remoteJid } = key; + const timestamp = findMessage.messageTimestamp; + const fromMe = key.fromMe.toString(); + const messageKey = `${remoteJid}_${timestamp}_${fromMe}`; - const { remoteJid } = key; - const timestamp = findMessage.messageTimestamp; - const fromMe = key.fromMe.toString(); - const messageKey = `${remoteJid}_${timestamp}_${fromMe}`; + const cachedTimestamp = await this.baileysCache.get(messageKey); - const cachedTimestamp = await this.baileysCache.get(messageKey); + if (!cachedTimestamp) { + if (status[update.status] === status[4]) { + this.logger.log(`Update as read in message.update ${remoteJid} - ${timestamp}`); + await this.updateMessagesReadedByTimestamp(remoteJid, timestamp); + await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS); + } - if (!cachedTimestamp) { - if (status[update.status] === status[4]) { - this.logger.log(`Update as read in message.update ${remoteJid} - ${timestamp}`); - await this.updateMessagesReadedByTimestamp(remoteJid, timestamp); - await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS); + await this.prismaRepository.message.update({ + where: { id: findMessage.id }, + data: { status: status[update.status] }, + }); + } else { + this.logger.info( + `Update readed messages duplicated ignored in message.update [avoid deadlock]: ${messageKey}`, + ); } - - await this.prismaRepository.message.update({ - where: { id: findMessage.id }, - data: { status: status[update.status] }, - }); - } else { - this.logger.info( - `Update readed messages duplicated ignored in message.update [avoid deadlock]: ${messageKey}`, - ); } - } - if (key.fromMe) { - const nextStatus = nextMessageStatus(findMessage.status, status[update.status]); - if (nextStatus !== undefined) { - try { - await this.prismaRepository.message.updateMany({ - where: { - id: findMessage.id, - OR: [{ status: null }, { status: { in: statusesAdvanceableTo(nextStatus) } }], - }, - data: { status: nextStatus }, - }); - } catch (error) { - this.logger.warn( - `Failed to update status of sent message ${findMessage.id}: ${error?.message ?? error}`, - ); + if (key.fromMe) { + const nextStatus = nextMessageStatus(findMessage.status, status[update.status]); + if (nextStatus !== undefined) { + try { + await this.prismaRepository.message.updateMany({ + where: { + id: findMessage.id, + OR: [{ status: null }, { status: { in: statusesAdvanceableTo(nextStatus) } }], + }, + data: { status: nextStatus }, + }); + } catch (error) { + this.logger.warn( + `Failed to update status of sent message ${findMessage.id}: ${error?.message ?? error}`, + ); + } } } } - } - this.sendDataWebhook(Events.MESSAGES_UPDATE, message); + this.sendDataWebhook(Events.MESSAGES_UPDATE, message); - if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { message: _msg, ...messageData } = message; - await this.prismaRepository.messageUpdate.create({ data: messageData }); - } + if (this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { message: _msg, ...messageData } = message; + await this.prismaRepository.messageUpdate.create({ data: messageData }); + } - const existingChat = await this.prismaRepository.chat.findFirst({ - where: { instanceId: this.instanceId, remoteJid: message.remoteJid }, - }); + const existingChat = await this.prismaRepository.chat.findFirst({ + where: { instanceId: this.instanceId, remoteJid: message.remoteJid }, + }); - if (existingChat) { - const chatToInsert = { remoteJid: message.remoteJid, instanceId: this.instanceId, unreadMessages: 0 }; + if (existingChat) { + const chatToInsert = { remoteJid: message.remoteJid, instanceId: this.instanceId, unreadMessages: 0 }; - this.sendDataWebhook(Events.CHATS_UPSERT, [chatToInsert]); - if (this.configService.get('DATABASE').SAVE_DATA.CHATS) { - try { - await this.prismaRepository.chat.update({ where: { id: existingChat.id }, data: chatToInsert }); - } catch { - console.log(`Chat insert record ignored: ${chatToInsert.remoteJid} - ${chatToInsert.instanceId}`); + this.sendDataWebhook(Events.CHATS_UPSERT, [chatToInsert]); + if (this.configService.get('DATABASE').SAVE_DATA.CHATS) { + try { + await this.prismaRepository.chat.update({ where: { id: existingChat.id }, data: chatToInsert }); + } catch { + console.log(`Chat insert record ignored: ${chatToInsert.remoteJid} - ${chatToInsert.instanceId}`); + } } } } + } catch (error) { + this.logger.error(`Failed to process message update ${key?.id}: ${error?.message ?? error}`); } } @@ -2019,6 +2023,14 @@ export class BaileysStartupService extends ChannelStartupService { }, }; + private async runEventStep(eventName: string, step: () => Promise) { + try { + await step(); + } catch (error) { + this.logger.error(`Failed to process ${eventName} event: ${error?.message ?? error}`); + } + } + private eventHandler() { this.client.ev.process(async (events) => { this.eventProcessingQueue = this.eventProcessingQueue.then(async () => { @@ -2028,133 +2040,169 @@ export class BaileysStartupService extends ChannelStartupService { const settings = await this.findSettings(); if (events.call) { - const call = events.call[0]; + await this.runEventStep('call', async () => { + const call = events.call[0]; - if (settings?.rejectCall && call.status == 'offer') { - this.client.rejectCall(call.id, call.from); - } - - if (settings?.msgCall?.trim().length > 0 && call.status == 'offer') { - if (call.from.endsWith('@lid')) { - call.from = await this.client.signalRepository.lidMapping.getPNForLID(call.from as string); + if (settings?.rejectCall && call.status == 'offer') { + this.client.rejectCall(call.id, call.from); } - const msg = await this.client.sendMessage(call.from, { text: settings.msgCall }); - this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify' }); - } + if (settings?.msgCall?.trim().length > 0 && call.status == 'offer') { + if (call.from.endsWith('@lid')) { + call.from = await this.client.signalRepository.lidMapping.getPNForLID(call.from as string); + } + const msg = await this.client.sendMessage(call.from, { text: settings.msgCall }); + + this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify' }); + } - this.sendDataWebhook(Events.CALL, call); + this.sendDataWebhook(Events.CALL, call); + }); } if (events['connection.update']) { - this.connectionUpdate(events['connection.update']); + await this.runEventStep('connection.update', async () => { + this.connectionUpdate(events['connection.update']); + }); } if (events['creds.update']) { - this.instance.authState.saveCreds(); + await this.runEventStep('creds.update', async () => { + this.instance.authState.saveCreds(); + }); } if (events['messaging-history.set']) { - const payload = events['messaging-history.set']; - await this.messageHandle['messaging-history.set'](payload); + await this.runEventStep('messaging-history.set', async () => { + const payload = events['messaging-history.set']; + await this.messageHandle['messaging-history.set'](payload); + }); } if (events['lid-mapping.update']) { - const { lid, pn } = events['lid-mapping.update']; - this.scheduleLidRemoteJidAltBackfill(lid, pn); + await this.runEventStep('lid-mapping.update', async () => { + const { lid, pn } = events['lid-mapping.update']; + this.scheduleLidRemoteJidAltBackfill(lid, pn); + }); } if (events['messages.upsert']) { - const payload = events['messages.upsert']; + await this.runEventStep('messages.upsert', async () => { + const payload = events['messages.upsert']; - // this.messageProcessor.processMessage(payload, settings); - await this.messageHandle['messages.upsert'](payload, settings); + // this.messageProcessor.processMessage(payload, settings); + await this.messageHandle['messages.upsert'](payload, settings); + }); } if (events['messages.update']) { - const payload = events['messages.update']; - await this.messageHandle['messages.update'](payload, settings); + await this.runEventStep('messages.update', async () => { + const payload = events['messages.update']; + await this.messageHandle['messages.update'](payload, settings); + }); } if (events['message-receipt.update']) { - const payload = events['message-receipt.update'] as MessageUserReceiptUpdate[]; - const remotesJidMap: Record = {}; + await this.runEventStep('message-receipt.update', async () => { + const payload = events['message-receipt.update'] as MessageUserReceiptUpdate[]; + const remotesJidMap: Record = {}; - for (const event of payload) { - if (typeof event.key.remoteJid === 'string' && typeof event.receipt.readTimestamp === 'number') { - remotesJidMap[event.key.remoteJid] = event.receipt.readTimestamp; + for (const event of payload) { + if (typeof event.key.remoteJid === 'string' && typeof event.receipt.readTimestamp === 'number') { + remotesJidMap[event.key.remoteJid] = event.receipt.readTimestamp; + } } - } - await Promise.all( - Object.keys(remotesJidMap).map(async (remoteJid) => - this.updateMessagesReadedByTimestamp(remoteJid, remotesJidMap[remoteJid]), - ), - ); + await Promise.all( + Object.keys(remotesJidMap).map(async (remoteJid) => + this.updateMessagesReadedByTimestamp(remoteJid, remotesJidMap[remoteJid]), + ), + ); + }); } if (events['presence.update']) { - const payload = events['presence.update']; + await this.runEventStep('presence.update', async () => { + const payload = events['presence.update']; - if (settings?.groupsIgnore && payload.id.includes('@g.us')) { - return; - } + const isIgnoredGroupPresence = settings?.groupsIgnore && payload.id.includes('@g.us'); - this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); + if (!isIgnoredGroupPresence) { + this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); + } + }); } if (!settings?.groupsIgnore) { if (events['groups.upsert']) { - const payload = events['groups.upsert']; - this.groupHandler['groups.upsert'](payload); + await this.runEventStep('groups.upsert', async () => { + const payload = events['groups.upsert']; + this.groupHandler['groups.upsert'](payload); + }); } if (events['groups.update']) { - const payload = events['groups.update']; - this.groupHandler['groups.update'](payload); + await this.runEventStep('groups.update', async () => { + const payload = events['groups.update']; + this.groupHandler['groups.update'](payload); + }); } if (events['group-participants.update']) { - const payload = events['group-participants.update'] as any; - this.groupHandler['group-participants.update'](payload); + await this.runEventStep('group-participants.update', async () => { + const payload = events['group-participants.update'] as any; + this.groupHandler['group-participants.update'](payload); + }); } } if (events['chats.upsert']) { - const payload = events['chats.upsert']; - this.chatHandle['chats.upsert'](payload); + await this.runEventStep('chats.upsert', async () => { + const payload = events['chats.upsert']; + this.chatHandle['chats.upsert'](payload); + }); } if (events['chats.update']) { - const payload = events['chats.update']; - this.chatHandle['chats.update'](payload); + await this.runEventStep('chats.update', async () => { + const payload = events['chats.update']; + this.chatHandle['chats.update'](payload); + }); } if (events['chats.delete']) { - const payload = events['chats.delete']; - this.chatHandle['chats.delete'](payload); + await this.runEventStep('chats.delete', async () => { + const payload = events['chats.delete']; + this.chatHandle['chats.delete'](payload); + }); } if (events['contacts.upsert']) { - const payload = events['contacts.upsert']; - this.contactHandle['contacts.upsert'](payload); + await this.runEventStep('contacts.upsert', async () => { + const payload = events['contacts.upsert']; + this.contactHandle['contacts.upsert'](payload); + }); } if (events['contacts.update']) { - const payload = events['contacts.update']; - this.contactHandle['contacts.update'](payload); + await this.runEventStep('contacts.update', async () => { + const payload = events['contacts.update']; + this.contactHandle['contacts.update'](payload); + }); } if (events[Events.LABELS_ASSOCIATION]) { - const payload = events[Events.LABELS_ASSOCIATION]; - this.labelHandle[Events.LABELS_ASSOCIATION](payload, database); - return; + await this.runEventStep(Events.LABELS_ASSOCIATION, async () => { + const payload = events[Events.LABELS_ASSOCIATION]; + this.labelHandle[Events.LABELS_ASSOCIATION](payload, database); + }); } if (events[Events.LABELS_EDIT]) { - const payload = events[Events.LABELS_EDIT]; - this.labelHandle[Events.LABELS_EDIT](payload); - return; + await this.runEventStep(Events.LABELS_EDIT, async () => { + const payload = events[Events.LABELS_EDIT]; + this.labelHandle[Events.LABELS_EDIT](payload); + }); } } } catch (error) { diff --git a/test/api/integrations/channel/whatsapp/eventErrorIsolation.test.ts b/test/api/integrations/channel/whatsapp/eventErrorIsolation.test.ts new file mode 100644 index 0000000000..0ec44096ee --- /dev/null +++ b/test/api/integrations/channel/whatsapp/eventErrorIsolation.test.ts @@ -0,0 +1,186 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { Events } from '@api/types/wa.types'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@api/server.module', () => ({ + chatbotController: { emit: vi.fn(async () => undefined) }, + waMonitor: {}, +})); + +vi.mock('@utils/onWhatsappCache', () => ({ + getOnWhatsappCache: vi.fn(async () => []), + saveOnWhatsappCache: vi.fn(async () => undefined), +})); + +vi.mock('@utils/sendTelemetry', () => ({ sendTelemetry: vi.fn() })); + +vi.mock('@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper', () => ({ + chatwootImport: { getRepositoryMessagesCache: vi.fn(() => null), setRepositoryMessagesCache: vi.fn() }, +})); + +const buildCache = () => { + const store = new Map(); + return { + store, + get: vi.fn(async (key: string) => store.get(key)), + set: vi.fn(async (key: string, value: unknown) => { + store.set(key, value); + }), + }; +}; + +const buildService = () => { + const config: Record = { + LOG: { BAILEYS: 'error' }, + DATABASE: { SAVE_DATA: { NEW_MESSAGE: true, HISTORIC: true, MESSAGE_UPDATE: true, CHATS: false, CONTACTS: false } }, + CHATWOOT: { ENABLED: false }, + OPENAI: { ENABLED: false }, + S3: { ENABLE: false }, + }; + const configService = { get: vi.fn((key: string) => config[key]) } as any; + const prismaRepository = { + chat: { findFirst: vi.fn(async () => null), findMany: vi.fn(async () => []), createMany: vi.fn(async () => ({})) }, + message: { + create: vi.fn(async ({ data }) => ({ id: 'row-id', ...data })), + createMany: vi.fn(async () => ({})), + findMany: vi.fn(async () => []), + update: vi.fn(async () => ({})), + updateMany: vi.fn(async () => ({ count: 1 })), + }, + messageUpdate: { create: vi.fn(async () => ({})) }, + contact: { findFirst: vi.fn(async () => null) }, + } as any; + const service = new BaileysStartupService( + configService, + { emit: vi.fn() } as any, + prismaRepository, + {} as any, + {} as any, + buildCache() as any, + {} as any, + ) as any; + service.instance.id = 'instance-id'; + service.instance.name = 'instance-name'; + service.sendDataWebhook = vi.fn(); + service.profilePicture = vi.fn(async () => ({ profilePictureUrl: null })); + service.updateMessagesReadedByTimestamp = vi.fn(async () => undefined); + service.updateChatUnreadMessages = vi.fn(async () => undefined); + service.client = { readMessages: vi.fn() }; + service.contactHandle = { 'contacts.upsert': vi.fn(async () => undefined) }; + return { service, prismaRepository }; +}; + +const remoteJid = '5492615550101@s.whatsapp.net'; +const groupJid = '120363000000000001@g.us'; + +const message = (id: string) => ({ + key: { id, fromMe: false, remoteJid }, + message: { conversation: 'hello' }, + messageTimestamp: 1700000000, + pushName: 'Someone', +}); + +const webhookCalls = (service: any, event: Events) => + service.sendDataWebhook.mock.calls.filter(([sent]) => sent === event); + +const processEvents = async (service: any, events: Record, settings: Record) => { + let processCallback: (events: Record) => Promise; + service.client = { + ev: { + process: vi.fn((callback) => { + processCallback = callback; + }), + }, + }; + service.findSettings = vi.fn(async () => settings); + service.chatHandle = { + 'chats.upsert': vi.fn(async () => undefined), + 'chats.update': vi.fn(async () => undefined), + }; + service.labelHandle = { + [Events.LABELS_EDIT]: vi.fn(async () => undefined), + [Events.LABELS_ASSOCIATION]: vi.fn(async () => undefined), + }; + service.eventHandler(); + await processCallback(events); + await service.eventProcessingQueue; +}; + +describe('event error isolation', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('emits the remaining upserts of a batch when one message fails to store', async () => { + const { service, prismaRepository } = buildService(); + prismaRepository.message.create + .mockImplementationOnce(async ({ data }) => ({ id: 'row-1', ...data })) + .mockRejectedValueOnce(new Error('database unavailable')); + + await service.messageHandle['messages.upsert']( + { messages: [message('MSG-1'), message('MSG-2'), message('MSG-3')], type: 'notify' }, + {}, + ); + + const emittedIds = webhookCalls(service, Events.MESSAGES_UPSERT).map(([, data]) => data.key.id); + expect(emittedIds).toEqual(['MSG-1', 'MSG-3']); + }); + + it('handles the remaining updates of a batch when one update fails', async () => { + const { service, prismaRepository } = buildService(); + prismaRepository.$queryRaw = vi + .fn() + .mockRejectedValueOnce(new Error('database unavailable')) + .mockResolvedValueOnce([ + { id: 'row-MSG-2', key: { id: 'MSG-2', remoteJid, fromMe: false }, status: 'DELIVERY_ACK' }, + ]); + + await service.messageHandle['messages.update']( + [ + { key: { id: 'MSG-1', fromMe: false, remoteJid }, update: { message: null } }, + { key: { id: 'MSG-2', fromMe: false, remoteJid }, update: { message: null } }, + ], + {}, + ); + + const deletedIds = webhookCalls(service, Events.MESSAGES_DELETE).map(([, data]) => data.id); + expect(deletedIds).toEqual(['MSG-2']); + }); + + it('runs later event types after an ignored group presence update', async () => { + const { service } = buildService(); + const chats = [{ id: remoteJid }]; + + await processEvents( + service, + { 'presence.update': { id: groupJid, presences: {} }, 'chats.upsert': chats }, + { groupsIgnore: true }, + ); + + expect(webhookCalls(service, Events.PRESENCE_UPDATE)).toHaveLength(0); + expect(service.chatHandle['chats.upsert']).toHaveBeenCalledWith(chats); + }); + + it('runs the labels edit handler after a labels association', async () => { + const { service } = buildService(); + const association = { type: 'add', association: { chatId: remoteJid, labelId: '1' } }; + const label = { id: '1', name: 'Lead' }; + + await processEvents(service, { [Events.LABELS_ASSOCIATION]: association, [Events.LABELS_EDIT]: label }, {}); + + expect(service.labelHandle[Events.LABELS_ASSOCIATION]).toHaveBeenCalled(); + expect(service.labelHandle[Events.LABELS_EDIT]).toHaveBeenCalledWith(label); + }); + + it('runs later event types when an earlier event type throws', async () => { + const { service } = buildService(); + const label = { id: '1', name: 'Lead' }; + service.messageHandle['messages.update'] = vi.fn(async () => { + throw new Error('update failed'); + }); + + await processEvents(service, { 'messages.update': [], [Events.LABELS_EDIT]: label }, {}); + + expect(service.labelHandle[Events.LABELS_EDIT]).toHaveBeenCalledWith(label); + }); +}); From ed9d5d8a0071e98f6955732a5e1ee371bf03bbc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Varela?= <114447249+MatiasxVarela@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:44:34 -0300 Subject: [PATCH 95/96] fix(chat): find the last message by JSON path in archiveChat and markChatUnread (#43) getLastMessage filtered the Message.key JSON column with { remoteJid }, which Prisma rejects as an unknown argument, so archiveChat and markChatUnread failed with 500 whenever the caller sent only `chat`. It also compared the raw number against stored JIDs. - Filter with { path: ['remoteJid'], equals } on the JID built by createJid, keeping @lid addressing intact. - archiveChat accepts chats without stored messages by sending an empty lastMessages list instead of failing. - The fallback messageTimestamp is unix seconds instead of milliseconds. Response shapes are unchanged. Credit: based on the fix by Razario in we-digital/wdgtl-evo-api (commit 2e045cb2, "Fix WA mute units, archive JID lookup, and chat-update spam"). Co-authored-by: matiasxvarela --- .../whatsapp/whatsapp.baileys.service.ts | 38 ++--- .../whatsapp/chatModifyLastMessage.test.ts | 150 ++++++++++++++++++ 2 files changed, 166 insertions(+), 22 deletions(-) create mode 100644 test/api/integrations/channel/whatsapp/chatModifyLastMessage.test.ts diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 345f9786ee..3b0676488c 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3889,27 +3889,23 @@ export class BaileysStartupService extends ChannelStartupService { } public async getLastMessage(number: string) { - const where: any = { key: { remoteJid: number }, instanceId: this.instance.id }; + const lastMessage = await this.findLastMessageOrNone(number); - const messages = await this.prismaRepository.message.findMany({ - where, - orderBy: { messageTimestamp: 'desc' }, - take: 1, - }); - - if (messages.length === 0) { + if (!lastMessage) { throw new NotFoundException('Messages not found'); } - let lastMessage = messages.pop(); + return lastMessage; + } - for (const message of messages) { - if (message.messageTimestamp >= lastMessage.messageTimestamp) { - lastMessage = message; - } - } + private async findLastMessageOrNone(number: string): Promise { + const messages = await this.prismaRepository.message.findMany({ + where: { key: { path: ['remoteJid'], equals: createJid(number) }, instanceId: this.instance.id }, + orderBy: { messageTimestamp: 'desc' }, + take: 1, + }); - return lastMessage as unknown as LastMessage; + return messages[0] as unknown as LastMessage | undefined; } public async archiveChat(data: ArchiveChatDto) { @@ -3918,18 +3914,16 @@ export class BaileysStartupService extends ChannelStartupService { let number = data.chat; if (!last_message && number) { - last_message = await this.getLastMessage(number); + last_message = await this.findLastMessageOrNone(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; } - if (!last_message || Object.keys(last_message).length === 0) { - throw new NotFoundException('Last message not found'); - } + const lastMessages = last_message && Object.keys(last_message).length > 0 ? [last_message] : []; - await this.client.chatModify({ archive: data.archive, lastMessages: [last_message] }, createJid(number)); + await this.client.chatModify({ archive: data.archive, lastMessages }, createJid(number)); return { chatId: number, archived: true }; } catch (error) { @@ -3949,7 +3943,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; } diff --git a/test/api/integrations/channel/whatsapp/chatModifyLastMessage.test.ts b/test/api/integrations/channel/whatsapp/chatModifyLastMessage.test.ts new file mode 100644 index 0000000000..0a4eacbcc6 --- /dev/null +++ b/test/api/integrations/channel/whatsapp/chatModifyLastMessage.test.ts @@ -0,0 +1,150 @@ +import { BaileysStartupService } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service'; +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('@api/server.module', () => ({ + chatbotController: { emit: vi.fn(async () => undefined) }, + waMonitor: {}, +})); + +vi.mock('@utils/onWhatsappCache', () => ({ + getOnWhatsappCache: vi.fn(async () => []), + saveOnWhatsappCache: vi.fn(async () => undefined), +})); + +vi.mock('@utils/sendTelemetry', () => ({ sendTelemetry: vi.fn() })); + +vi.mock('@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper', () => ({ + chatwootImport: { getRepositoryMessagesCache: vi.fn(() => null), setRepositoryMessagesCache: vi.fn() }, +})); + +const phoneNumber = '5492615550101'; +const phoneJid = '5492615550101@s.whatsapp.net'; +const lidJid = '123456789012345@lid'; + +const buildService = (storedMessages: unknown[]) => { + const config: Record = { + LOG: { BAILEYS: 'error' }, + CHATWOOT: { ENABLED: false }, + OPENAI: { ENABLED: false }, + S3: { ENABLE: false }, + }; + const configService = { get: vi.fn((key: string) => config[key]) } as any; + const prismaRepository = { + message: { findMany: vi.fn(async () => storedMessages) }, + } as any; + const service = new BaileysStartupService( + configService, + { emit: vi.fn() } as any, + prismaRepository, + {} as any, + {} as any, + { get: vi.fn(), set: vi.fn() } as any, + {} as any, + ) as any; + service.instance.id = 'instance-id'; + service.client = { chatModify: vi.fn(async () => undefined) }; + return { service, prismaRepository }; +}; + +const storedMessage = (remoteJid: string) => ({ + key: { id: 'MSG-1', remoteJid, fromMe: false }, + messageTimestamp: 1700000000, +}); + +describe('archiveChat by chat number', () => { + it('looks up the last message by the JSON remoteJid path of the normalized JID within the instance', async () => { + const { service, prismaRepository } = buildService([storedMessage(phoneJid)]); + + await service.archiveChat({ chat: phoneNumber, archive: true }); + + expect(prismaRepository.message.findMany).toHaveBeenCalledWith({ + where: { key: { path: ['remoteJid'], equals: phoneJid }, instanceId: 'instance-id' }, + orderBy: { messageTimestamp: 'desc' }, + take: 1, + }); + }); + + it('archives with the stored last message and keeps the response shape', async () => { + const { service } = buildService([storedMessage(phoneJid)]); + + const response = await service.archiveChat({ chat: phoneNumber, archive: true }); + + expect(service.client.chatModify).toHaveBeenCalledWith( + { archive: true, lastMessages: [storedMessage(phoneJid)] }, + phoneJid, + ); + expect(response).toEqual({ chatId: phoneNumber, archived: true }); + }); + + it('preserves @lid addressing in the lookup and the chat modification', async () => { + const { service, prismaRepository } = buildService([storedMessage(lidJid)]); + + await service.archiveChat({ chat: lidJid, archive: true }); + + expect(prismaRepository.message.findMany.mock.calls[0][0].where.key).toEqual({ + path: ['remoteJid'], + equals: lidJid, + }); + expect(service.client.chatModify.mock.calls[0][1]).toBe(lidJid); + }); + + it('archives a chat without stored messages using an empty lastMessages list', async () => { + const { service } = buildService([]); + + const response = await service.archiveChat({ chat: phoneNumber, archive: true }); + + expect(service.client.chatModify).toHaveBeenCalledWith({ archive: true, lastMessages: [] }, phoneJid); + expect(response).toEqual({ chatId: phoneNumber, archived: true }); + }); +}); + +describe('archiveChat with an explicit lastMessage', () => { + it('uses the provided message without querying storage and keeps the response shape', async () => { + const { service, prismaRepository } = buildService([]); + const lastMessage = storedMessage(phoneJid); + + const response = await service.archiveChat({ lastMessage, archive: true }); + + expect(prismaRepository.message.findMany).not.toHaveBeenCalled(); + expect(service.client.chatModify).toHaveBeenCalledWith({ archive: true, lastMessages: [lastMessage] }, phoneJid); + expect(response).toEqual({ chatId: phoneJid, archived: true }); + }); + + it('fills a missing messageTimestamp with unix seconds', async () => { + const { service } = buildService([]); + const lastMessage = { key: { id: 'MSG-1', remoteJid: phoneJid, fromMe: false } } as any; + const nowSeconds = Math.floor(Date.now() / 1000); + + await service.archiveChat({ lastMessage, archive: true }); + + expect(lastMessage.messageTimestamp).toBeGreaterThanOrEqual(nowSeconds); + expect(lastMessage.messageTimestamp).toBeLessThan(nowSeconds + 60); + }); +}); + +describe('markChatUnread by chat number', () => { + it('looks up the last message by the JSON remoteJid path and marks the chat unread', async () => { + const { service, prismaRepository } = buildService([storedMessage(phoneJid)]); + + const response = await service.markChatUnread({ chat: phoneNumber }); + + expect(prismaRepository.message.findMany.mock.calls[0][0].where).toEqual({ + key: { path: ['remoteJid'], equals: phoneJid }, + instanceId: 'instance-id', + }); + expect(service.client.chatModify).toHaveBeenCalledWith( + { markRead: false, lastMessages: [storedMessage(phoneJid)] }, + phoneJid, + ); + expect(response).toEqual({ chatId: phoneNumber, markedChatUnread: true }); + }); + + it('still fails when the chat has no stored messages', async () => { + const { service } = buildService([]); + + await expect(service.markChatUnread({ chat: phoneNumber })).rejects.toMatchObject({ + message: expect.anything(), + }); + expect(service.client.chatModify).not.toHaveBeenCalled(); + }); +}); From 0482cbd200b366e1e0a1949579fe4603eadbb837 Mon Sep 17 00:00:00 2001 From: matiasxvarela Date: Thu, 5 Nov 2026 10:20:43 -0300 Subject: [PATCH 96/96] docs(changelog): release v0.1.4 --- CHANGELOG.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27cbcc5805..3c5d515690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,26 @@ # evolution-in-purple -## Unreleased +## 0.1.4 (2026-11-05) ### Fixes -* **Sent message status**: messages sent by the instance now advance `Message.status` from WhatsApp acks instead of staying `PENDING`, so `findChats` `lastMessageStatus` and `findMessages` reflect the real status. Applies to 1:1 chats; Baileys delivers no per-message status for groups. Backward acks are ignored and `DELETED`/`EDITED` are never overwritten -* **QR code limit**: unpaired instances whose QR code limit is reached now end in `close` (memory and database) instead of staying `connecting`, so requesting the QR code again starts a fresh connection -* **Deleted messages resent on retry**: messages deleted for everyone are no longer resent when a device sends a retry receipt, so deleted content no longer reappears. Incoming revokes now mark the stored message `DELETED` and record a `DELETED` message update; the `MESSAGES_DELETE` webhook payload is unchanged -* **Instance-scoped chat and edit lookups**: recalculating a chat's unread count and editing a sent message now only look up rows of the current instance, so another instance's chat or message with the same `remoteJid` or message id is no longer updated -* **Payload logging**: the Baileys channel no longer writes full message payloads (including media base64) to stdout on every received message, message update, call or history sync; these logs now go through the logger at `VERBOSE`/`DEBUG` level and include only ids and counts. Webhook payloads are unchanged -* **Event error isolation**: a failure while processing one message of a `messages.upsert` or `messages.update` batch no longer drops the rest of the batch and their webhooks, and a failing or ignored event type (for example a group `presence.update` with `groupsIgnore`, or a label event) no longer skips the other event types delivered with it. Webhook payloads and event order are unchanged +* **Sent message status**: messages sent by the instance now advance `Message.status` from WhatsApp acks instead of staying `PENDING`, so `findChats` `lastMessageStatus` and `findMessages` reflect the real status. Applies to 1:1 chats; Baileys delivers no per-message status for groups. Backward acks are ignored and `DELETED`/`EDITED` are never overwritten (#36) +* **QR code limit**: unpaired instances whose QR code limit is reached now end in `close` (memory and database) instead of staying `connecting`, so requesting the QR code again starts a fresh connection (#37) +* **Deleted messages resent on retry**: messages deleted for everyone are no longer resent when a device sends a retry receipt, so deleted content no longer reappears. Incoming revokes now mark the stored message `DELETED` and record a `DELETED` message update; the `MESSAGES_DELETE` webhook payload is unchanged (#38) +* **Instance-scoped chat and edit lookups**: recalculating a chat's unread count and editing a sent message now only look up rows of the current instance, so another instance's chat or message with the same `remoteJid` or message id is no longer updated (#39) +* **Payload logging**: the Baileys channel no longer writes full message payloads (including media base64) to stdout on every received message, message update, call or history sync; these logs now go through the logger at `VERBOSE`/`DEBUG` level and include only ids and counts. Webhook payloads are unchanged (#41) +* **Event error isolation**: a failure while processing one message of a `messages.upsert` or `messages.update` batch no longer drops the rest of the batch and their webhooks, and a failing or ignored event type (for example a group `presence.update` with `groupsIgnore`, or a label event) no longer skips the other event types delivered with it. Webhook payloads and event order are unchanged (#42) +* **`@lid` remoteJidAlt**: history sync messages from `@lid` chats now store `key.remoteJidAlt`, and it is backfilled on stored messages when a lid mapping is learned; payload contacts with conflicting lids are skipped (#33, #34) +* **Archive and mark unread**: `archiveChat` and `markChatUnread` no longer fail with 500 when only `chat` is sent: the last message is looked up by JSON path on the jid built from the number, chats without stored messages can be archived, and the fallback timestamp is in seconds. Response shapes are unchanged (#43; based on the fix by Razario) ### Performance -* **Message lookup indexes**: PostgreSQL migrations add `idx_message_instance_key_id` (`instanceId`, `key->>'id'`) and `idx_message_instance_remotejid_fromme` (`instanceId`, `key->>'remoteJid'`, `key->>'fromMe'`) so message updates and read receipts no longer scan every message of the instance. They are created `IF NOT EXISTS` inside the migration transaction, which runs before the server starts, so databases that already have them are left untouched and a failed build leaves no invalid index +* **Message lookup indexes**: PostgreSQL migrations add `idx_message_instance_key_id` (`instanceId`, `key->>'id'`) and `idx_message_instance_remotejid_fromme` (`instanceId`, `key->>'remoteJid'`, `key->>'fromMe'`) so message updates and read receipts no longer scan every message of the instance. They are created `IF NOT EXISTS` inside the migration transaction, which runs before the server starts, so databases that already have them are left untouched and a failed build leaves no invalid index (#40) + +### Chores + +* `scripts/` adds a one-off, row-locking backfill for `@lid` `key.remoteJidAlt` with backup and rollback (#35) +* The daily image scan fails on fixable CRITICAL or HIGH findings (#32) ## 0.1.3 (2026-10-24)