From 5499b7b809ca05c2d40e9c83d178020ab28037ac Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Thu, 18 Jun 2026 16:53:25 +0200 Subject: [PATCH 01/32] fix: resolve catalog pagination limits and add GHCR CI/CD - Increase default catalog limit from 10 to 50 products - Increase pagination loops from 4 to 20 for larger catalogs - Remove arbitrary 20-item cap on getCollections (now default 100) - Add cursor parameter to validation schemas for manual pagination - Add cursor support to getCollectionsDto - Add GitHub Actions workflow for GHCR publishing Fixes #2589 --- .github/workflows/publish_ghcr.yml | 71 +++++++++++++++++++ src/api/dto/business.dto.ts | 1 + .../whatsapp/whatsapp.baileys.service.ts | 8 +-- src/validate/business.schema.ts | 2 + 4 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/publish_ghcr.yml diff --git a/.github/workflows/publish_ghcr.yml b/.github/workflows/publish_ghcr.yml new file mode 100644 index 000000000..188cc42c1 --- /dev/null +++ b/.github/workflows/publish_ghcr.yml @@ -0,0 +1,71 @@ +name: Build and Publish to GHCR + +on: + push: + branches: + - main + - 'release/**' + tags: + - 'v*.*.*' + pull_request: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + name: Build and Publish + 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: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix= + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Image digest + if: github.event_name != 'pull_request' + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/src/api/dto/business.dto.ts b/src/api/dto/business.dto.ts index d29b3cf97..d999f079d 100644 --- a/src/api/dto/business.dto.ts +++ b/src/api/dto/business.dto.ts @@ -11,4 +11,5 @@ export class getCatalogDto { export class getCollectionsDto { number?: string; limit?: number; + cursor?: string; } diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc..593dfad20 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -4901,8 +4901,8 @@ export class BaileysStartupService extends ChannelStartupService { //Business Controller public async fetchCatalog(instanceName: string, data: getCollectionsDto) { const jid = data.number ? createJid(data.number) : this.client?.user?.id; - const limit = data.limit || 10; - const cursor = null; + const limit = data.limit || 50; + const cursor = data.cursor || null; const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); @@ -4924,7 +4924,7 @@ export class BaileysStartupService extends ChannelStartupService { let productsCatalog = catalog.products || []; let countLoops = 0; - while (fetcherHasMore && countLoops < 4) { + while (fetcherHasMore && countLoops < 20) { catalog = await this.getCatalog({ jid: info?.jid, limit, cursor: nextPageCursor }); nextPageCursor = catalog.nextPageCursor; nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null; @@ -4971,7 +4971,7 @@ export class BaileysStartupService extends ChannelStartupService { public async fetchCollections(instanceName: string, data: getCollectionsDto) { const jid = data.number ? createJid(data.number) : this.client?.user?.id; - const limit = data.limit <= 20 ? data.limit : 20; //(tem esse limite, não sei porque) + const limit = data.limit || 100; const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); diff --git a/src/validate/business.schema.ts b/src/validate/business.schema.ts index 91ad17b20..1876a8d40 100644 --- a/src/validate/business.schema.ts +++ b/src/validate/business.schema.ts @@ -5,6 +5,7 @@ export const catalogSchema: JSONSchema7 = { properties: { number: { type: 'string' }, limit: { type: 'number' }, + cursor: { type: 'string' }, }, }; @@ -13,5 +14,6 @@ export const collectionsSchema: JSONSchema7 = { properties: { number: { type: 'string' }, limit: { type: 'number' }, + cursor: { type: 'string' }, }, }; From e02109f172a27c1445b9ae41bfdfa4bedb2337c9 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Thu, 18 Jun 2026 17:03:19 +0200 Subject: [PATCH 02/32] feat: add OpenAPI documentation with swagger-jsdoc - Add swagger-jsdoc for auto-generating OpenAPI spec from JSDoc comments - Create swagger.config.ts with base schemas and security setup - Add JSDoc comments to Business router endpoints (getCatalog, getCollections) - Setup swagger-ui at /docs endpoint - Add /openapi.json endpoint for raw spec access - Add npm script 'openapi:generate' to generate static spec - Generate initial openapi.json with Business endpoints Access: - Swagger UI: http://localhost:8080/docs - OpenAPI JSON: http://localhost:8080/openapi.json --- openapi.json | 150 ++++++++++++++++ package-lock.json | 278 ++++++++++++++++++++++++++++++ package.json | 4 +- scripts/generate-openapi.ts | 44 +++++ src/api/routes/business.router.ts | 76 ++++++++ src/config/swagger.config.ts | 228 ++++++++++++++++++++++++ src/main.ts | 14 ++ 7 files changed, 793 insertions(+), 1 deletion(-) create mode 100644 openapi.json create mode 100644 scripts/generate-openapi.ts create mode 100644 src/config/swagger.config.ts diff --git a/openapi.json b/openapi.json new file mode 100644 index 000000000..5fc8561dc --- /dev/null +++ b/openapi.json @@ -0,0 +1,150 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Evolution API", + "version": "2.3.7", + "description": "WhatsApp API - OpenAPI Documentation", + "contact": { + "name": "Evolution API", + "url": "https://github.com/EvolutionAPI/evolution-api" + } + }, + "servers": [ + { + "url": "/v1", + "description": "API v1" + } + ], + "components": { + "securitySchemes": { + "apikey": { + "type": "apiKey", + "name": "apikey", + "in": "header", + "description": "API key for authentication" + } + } + }, + "security": [ + { + "apikey": [] + } + ], + "paths": { + "/business/getCatalog/{instanceName}": { + "post": { + "tags": [ + "Business" + ], + "summary": "Get WhatsApp Business catalog", + "description": "Fetches all products from a WhatsApp Business catalog with automatic pagination", + "security": [ + { + "apikey": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "instanceName", + "required": true, + "schema": { + "type": "string" + }, + "description": "Instance name" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Catalog retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/business/getCollections/{instanceName}": { + "post": { + "tags": [ + "Business" + ], + "summary": "Get WhatsApp Business collections", + "description": "Fetches all collections with their products from a WhatsApp Business account", + "security": [ + { + "apikey": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "instanceName", + "required": true, + "schema": { + "type": "string" + }, + "description": "Instance name" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CollectionsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Collections retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CollectionsResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + } + }, + "tags": [] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c45e8fef3..0dae5a428 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,6 +95,7 @@ "husky": "^9.1.7", "lint-staged": "^16.1.6", "prettier": "^3.4.2", + "swagger-jsdoc": "^6.3.0", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", "typescript": "^5.7.2" @@ -106,6 +107,58 @@ "integrity": "sha512-yprSnAtj80/VKuDqRcFFLDYltoNV8tChNwFfIgcf6PGD4sjzWIBgs08pRuTqGH5mk5wgL6PBRSsMCZqtZwzFEw==", "license": "MIT" }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", + "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz", + "integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "14.0.1", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "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", @@ -2371,6 +2424,16 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@jimp/core": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.0.tgz", @@ -5475,6 +5538,21 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/amqplib": { "version": "0.10.9", "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.9.tgz", @@ -6202,6 +6280,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -8856,6 +8941,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/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/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -10350,6 +10465,22 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jimp": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.0.tgz", @@ -11571,6 +11702,16 @@ ], "license": "MIT" }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -12171,6 +12312,14 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -12399,6 +12548,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -12564,6 +12720,23 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -14712,6 +14885,111 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-jsdoc": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.3.0.tgz", + "integrity": "sha512-I+iQjVGV3t28pOkQUJv2MncthvOtkEactOn8R76SvSYhxgtIn7FoqfDHwQaN+GBnQdXQLrhgDXseKitmJcHMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^12.1.0", + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "11.1.0", + "lodash.mergewith": "^4.6.2", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/swagger-jsdoc/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/swagger-ui-dist": { "version": "5.30.2", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.2.tgz", diff --git a/package.json b/package.json index 56e32fcc8..66e41f0f8 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "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\"", + "openapi:generate": "tsx ./scripts/generate-openapi.ts", "prepare": "husky" }, "repository": { @@ -87,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", @@ -151,6 +152,7 @@ "husky": "^9.1.7", "lint-staged": "^16.1.6", "prettier": "^3.4.2", + "swagger-jsdoc": "^6.3.0", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", "typescript": "^5.7.2" diff --git a/scripts/generate-openapi.ts b/scripts/generate-openapi.ts new file mode 100644 index 000000000..a4eac6467 --- /dev/null +++ b/scripts/generate-openapi.ts @@ -0,0 +1,44 @@ +import swaggerJsdoc from 'swagger-jsdoc'; +import { writeFileSync } from 'fs'; +import { version } from '../package.json'; + +const options: swaggerJsdoc.Options = { + definition: { + openapi: '3.0.0', + info: { + title: 'Evolution API', + version, + description: 'WhatsApp API - OpenAPI Documentation', + contact: { + name: 'Evolution API', + url: 'https://github.com/EvolutionAPI/evolution-api', + }, + }, + servers: [ + { + url: '/v1', + description: 'API v1', + }, + ], + components: { + securitySchemes: { + apikey: { + type: 'apiKey', + name: 'apikey', + in: 'header', + description: 'API key for authentication', + }, + }, + }, + security: [ + { + apikey: [], + }, + ], + }, + apis: ['./src/api/routes/*.ts', './src/api/routes/**/*.ts'], +}; + +const spec = swaggerJsdoc(options); +writeFileSync('./openapi.json', JSON.stringify(spec, null, 2)); +console.log('OpenAPI spec generated at ./openapi.json'); diff --git a/src/api/routes/business.router.ts b/src/api/routes/business.router.ts index faca7b33f..b4a8abbad 100644 --- a/src/api/routes/business.router.ts +++ b/src/api/routes/business.router.ts @@ -7,10 +7,50 @@ import { RequestHandler, Router } from 'express'; import { HttpStatus } from './index.router'; +/** + * Business Router - Handles WhatsApp Business catalog operations + * @tags Business + */ export class BusinessRouter extends RouterBroker { constructor(...guards: RequestHandler[]) { super(); this.router + /** + * @swagger + * /business/getCatalog/{instanceName}: + * post: + * tags: [Business] + * summary: Get WhatsApp Business catalog + * description: Fetches all products from a WhatsApp Business catalog with automatic pagination + * security: + * - apikey: [] + * parameters: + * - in: path + * name: instanceName + * required: true + * schema: + * type: string + * description: Instance name + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CatalogRequest' + * responses: + * 200: + * description: Catalog retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CatalogResponse' + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ .post(this.routerPath('getCatalog'), ...guards, async (req, res) => { try { const response = await this.dataValidate({ @@ -31,6 +71,42 @@ export class BusinessRouter extends RouterBroker { } }) + /** + * @swagger + * /business/getCollections/{instanceName}: + * post: + * tags: [Business] + * summary: Get WhatsApp Business collections + * description: Fetches all collections with their products from a WhatsApp Business account + * security: + * - apikey: [] + * parameters: + * - in: path + * name: instanceName + * required: true + * schema: + * type: string + * description: Instance name + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CollectionsRequest' + * responses: + * 200: + * description: Collections retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CollectionsResponse' + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ .post(this.routerPath('getCollections'), ...guards, async (req, res) => { try { const response = await this.dataValidate({ diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts new file mode 100644 index 000000000..4649fe976 --- /dev/null +++ b/src/config/swagger.config.ts @@ -0,0 +1,228 @@ +import swaggerJsdoc from 'swagger-jsdoc'; +import { version } from '../package.json'; + +const options: swaggerJsdoc.Options = { + definition: { + openapi: '3.0.0', + info: { + title: 'Evolution API', + version, + description: 'WhatsApp API - OpenAPI Documentation', + contact: { + name: 'Evolution API', + url: 'https://github.com/EvolutionAPI/evolution-api', + }, + }, + servers: [ + { + url: '/v1', + description: 'API v1', + }, + ], + components: { + securitySchemes: { + apikey: { + type: 'apiKey', + name: 'apikey', + in: 'header', + description: 'API key for authentication', + }, + }, + schemas: { + InstanceDto: { + type: 'object', + properties: { + instanceName: { + type: 'string', + description: 'Instance name', + }, + }, + }, + NumberDto: { + type: 'object', + properties: { + number: { + type: 'string', + description: 'Phone number (e.g., 5511999999999)', + }, + }, + }, + CatalogRequest: { + type: 'object', + properties: { + number: { + type: 'string', + description: 'Phone number of the business account', + }, + limit: { + type: 'number', + description: 'Number of products to fetch (default: 50)', + default: 50, + }, + cursor: { + type: 'string', + description: 'Pagination cursor for next page', + }, + }, + }, + CollectionsRequest: { + type: 'object', + properties: { + number: { + type: 'string', + description: 'Phone number of the business account', + }, + limit: { + type: 'number', + description: 'Number of collections to fetch (default: 100)', + default: 100, + }, + cursor: { + type: 'string', + description: 'Pagination cursor for next page', + }, + }, + }, + Product: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Product ID', + }, + name: { + type: 'string', + description: 'Product name', + }, + description: { + type: 'string', + description: 'Product description', + }, + price: { + type: 'number', + description: 'Product price', + }, + currency: { + type: 'string', + description: 'Currency code', + }, + availability: { + type: 'string', + enum: ['in stock', 'out of stock', 'preorder'], + description: 'Product availability status', + }, + image: { + type: 'array', + items: { + type: 'string', + }, + description: 'Product image URLs', + }, + }, + }, + CatalogCollection: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Collection ID', + }, + name: { + type: 'string', + description: 'Collection name', + }, + products: { + type: 'array', + items: { + $ref: '#/components/schemas/Product', + }, + }, + }, + }, + CatalogResponse: { + type: 'object', + properties: { + wuid: { + type: 'string', + description: 'WhatsApp user ID', + }, + numberExists: { + type: 'boolean', + description: 'Whether the number exists on WhatsApp', + }, + isBusiness: { + type: 'boolean', + description: 'Whether the account is a business account', + }, + catalogLength: { + type: 'number', + description: 'Total number of products fetched', + }, + catalog: { + type: 'array', + items: { + $ref: '#/components/schemas/Product', + }, + }, + }, + }, + CollectionsResponse: { + type: 'object', + properties: { + wuid: { + type: 'string', + description: 'WhatsApp user ID', + }, + name: { + type: 'string', + description: 'Business name', + }, + numberExists: { + type: 'boolean', + description: 'Whether the number exists on WhatsApp', + }, + isBusiness: { + type: 'boolean', + description: 'Whether the account is a business account', + }, + collectionsLength: { + type: 'number', + description: 'Total number of collections', + }, + collections: { + type: 'array', + items: { + $ref: '#/components/schemas/CatalogCollection', + }, + }, + }, + }, + ErrorResponse: { + type: 'object', + properties: { + status: { + type: 'number', + description: 'HTTP status code', + }, + error: { + type: 'string', + description: 'Error message', + }, + message: { + type: 'string', + description: 'Detailed error message', + }, + }, + }, + }, + }, + security: [ + { + apikey: [], + }, + ], + }, + apis: ['./src/api/routes/*.ts', './src/api/routes/**/*.ts'], +}; + +export const swaggerSpec = swaggerJsdoc(options); diff --git a/src/main.ts b/src/main.ts index f1f00ba9a..486d9c9a3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,6 +18,7 @@ import { import { onUnexpectedError } from '@config/error.config'; import { Logger } from '@config/logger.config'; import { ROOT_DIR } from '@config/path.config'; +import { swaggerSpec } from '@config/swagger.config'; import * as Sentry from '@sentry/node'; import { ServerUP } from '@utils/server-up'; import axios from 'axios'; @@ -25,6 +26,7 @@ import compression from 'compression'; import cors from 'cors'; import express, { json, NextFunction, Request, Response, urlencoded } from 'express'; import { join } from 'path'; +import swaggerUi from 'swagger-ui-express'; async function initWA() { await waMonitor.loadInstance(); @@ -70,6 +72,18 @@ async function bootstrap() { app.use('/store', express.static(join(ROOT_DIR, 'store'))); + // Swagger UI + app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, { + customCss: '.swagger-ui .topbar { display: none }', + customSiteTitle: 'Evolution API Documentation', + })); + + // OpenAPI JSON endpoint + app.get('/openapi.json', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.send(swaggerSpec); + }); + app.use('/', router); app.use( From 642a4167e3cc3086791fa9ea342b6786ed5413b3 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Thu, 18 Jun 2026 17:08:20 +0200 Subject: [PATCH 03/32] fix: resolve Docker build and lint errors - Fix package.json import in swagger.config.ts (use readFileSync instead of import) - Fix package.json import in generate-openapi.ts - Fix prettier formatting in main.ts swagger-ui setup - Addresses CI failures from PR #2592 --- scripts/generate-openapi.ts | 7 +++++-- src/config/swagger.config.ts | 6 +++++- src/main.ts | 12 ++++++++---- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/scripts/generate-openapi.ts b/scripts/generate-openapi.ts index a4eac6467..060ed23a0 100644 --- a/scripts/generate-openapi.ts +++ b/scripts/generate-openapi.ts @@ -1,6 +1,9 @@ +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; import swaggerJsdoc from 'swagger-jsdoc'; -import { writeFileSync } from 'fs'; -import { version } from '../package.json'; + +const packageJson = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8')); +const { version } = packageJson; const options: swaggerJsdoc.Options = { definition: { diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts index 4649fe976..47a4ea92b 100644 --- a/src/config/swagger.config.ts +++ b/src/config/swagger.config.ts @@ -1,5 +1,9 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; import swaggerJsdoc from 'swagger-jsdoc'; -import { version } from '../package.json'; + +const packageJson = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8')); +const { version } = packageJson; const options: swaggerJsdoc.Options = { definition: { diff --git a/src/main.ts b/src/main.ts index 486d9c9a3..a66202b92 100644 --- a/src/main.ts +++ b/src/main.ts @@ -73,10 +73,14 @@ async function bootstrap() { app.use('/store', express.static(join(ROOT_DIR, 'store'))); // Swagger UI - app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, { - customCss: '.swagger-ui .topbar { display: none }', - customSiteTitle: 'Evolution API Documentation', - })); + app.use( + '/docs', + swaggerUi.serve, + swaggerUi.setup(swaggerSpec, { + customCss: '.swagger-ui .topbar { display: none }', + customSiteTitle: 'Evolution API Documentation', + }), + ); // OpenAPI JSON endpoint app.get('/openapi.json', (req, res) => { From 3c1ff611c73450d699233c2dfa4d7703073a6cba Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Thu, 18 Jun 2026 19:05:24 +0200 Subject: [PATCH 04/32] fix: convert limit to number type in catalog endpoints - Add Number() conversion for limit in fetchCatalog and fetchCollections - Fixes 'limit is not of a type(s) number' error when limit is sent as string --- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 593dfad20..ce93a0ccd 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -4901,7 +4901,7 @@ export class BaileysStartupService extends ChannelStartupService { //Business Controller public async fetchCatalog(instanceName: string, data: getCollectionsDto) { const jid = data.number ? createJid(data.number) : this.client?.user?.id; - const limit = data.limit || 50; + const limit = Number(data.limit) || 50; const cursor = data.cursor || null; const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); @@ -4971,7 +4971,7 @@ export class BaileysStartupService extends ChannelStartupService { public async fetchCollections(instanceName: string, data: getCollectionsDto) { const jid = data.number ? createJid(data.number) : this.client?.user?.id; - const limit = data.limit || 100; + const limit = Number(data.limit) || 100; const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); From f4eefcb7e0147d4a5296023f8af771a3277cf813 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Thu, 18 Jun 2026 19:17:20 +0200 Subject: [PATCH 05/32] fix: accept both string and number types for limit parameter - Change limit schema type from 'number' to ['number', 'string'] - Fixes 400 error when n8n sends limit as string (e.g. "50" instead of 50) - Service layer already converts with Number() for safety --- src/validate/business.schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/validate/business.schema.ts b/src/validate/business.schema.ts index 1876a8d40..cc0cb3219 100644 --- a/src/validate/business.schema.ts +++ b/src/validate/business.schema.ts @@ -4,7 +4,7 @@ export const catalogSchema: JSONSchema7 = { type: 'object', properties: { number: { type: 'string' }, - limit: { type: 'number' }, + limit: { type: ['number', 'string'] }, cursor: { type: 'string' }, }, }; @@ -13,7 +13,7 @@ export const collectionsSchema: JSONSchema7 = { type: 'object', properties: { number: { type: 'string' }, - limit: { type: 'number' }, + limit: { type: ['number', 'string'] }, cursor: { type: 'string' }, }, }; From d892c4baf5ea2ff5da1b6723931aec8d531eacdd Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Fri, 19 Jun 2026 02:59:27 +0200 Subject: [PATCH 06/32] fix: make fetchBusinessProfile non-fatal in catalog/collections - fetchBusinessProfile failure no longer blocks catalog/collections fetch - Both endpoints now continue even if business profile check fails - Prevents returning isBusiness:false when profile API is temporarily down --- .../whatsapp/whatsapp.baileys.service.ts | 23 +++++++++++++++---- 1 file changed, 19 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 ce93a0ccd..d6ce709b5 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -4912,7 +4912,14 @@ export class BaileysStartupService extends ChannelStartupService { try { const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - const business = await this.fetchBusinessProfile(info?.jid); + + let isBusiness = false; + try { + const business = await this.fetchBusinessProfile(info?.jid); + isBusiness = business?.isBusiness || false; + } catch (profileError) { + console.log('fetchBusinessProfile failed, continuing catalog fetch:', profileError?.message); + } let catalog = await this.getCatalog({ jid: info?.jid, limit, cursor }); let nextPageCursor = catalog.nextPageCursor; @@ -4939,7 +4946,7 @@ export class BaileysStartupService extends ChannelStartupService { return { wuid: info?.jid || jid, numberExists: info?.exists, - isBusiness: business.isBusiness, + isBusiness: isBusiness, catalogLength: productsCatalog.length, catalog: productsCatalog, }; @@ -4981,14 +4988,22 @@ export class BaileysStartupService extends ChannelStartupService { try { const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - const business = await this.fetchBusinessProfile(info?.jid); + + let isBusiness = false; + try { + const business = await this.fetchBusinessProfile(info?.jid); + isBusiness = business?.isBusiness || false; + } catch (profileError) { + console.log('fetchBusinessProfile failed, continuing collections fetch:', profileError?.message); + } + const collections = await this.getCollections(info?.jid, limit); return { wuid: info?.jid || jid, name: info?.name, numberExists: info?.exists, - isBusiness: business.isBusiness, + isBusiness: isBusiness, collectionsLength: collections?.length, collections: collections, }; From 49a661bbca21eea7ff7e9c2d7198e1008d410c96 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Fri, 19 Jun 2026 14:57:29 +0700 Subject: [PATCH 07/32] chore: update Baileys from 7.0.0-rc.9 to 7.0.0-rc13 Changes: - Fix protocolMessage parsing regression - Performance improvements - Meta Coexistence support - Less automation detection (reduced bans) - Fix session recovery issues --- package-lock.json | 768 +++++++++++++++++----------------------------- package.json | 2 +- 2 files changed, 287 insertions(+), 483 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0dae5a428..2e665595f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "7.0.0-rc.9", + "baileys": "^7.0.0-rc13", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", @@ -882,9 +882,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", @@ -2435,17 +2435,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": { @@ -2465,14 +2465,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": { @@ -2480,23 +2480,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": { @@ -2504,13 +2504,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" }, @@ -2519,13 +2519,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": { @@ -2533,13 +2533,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": { @@ -2547,13 +2547,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": { @@ -2561,13 +2561,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": { @@ -2575,25 +2575,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": { @@ -2601,14 +2601,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" }, @@ -2617,16 +2617,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": { @@ -2634,15 +2634,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": { @@ -2650,14 +2650,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": { @@ -2665,13 +2665,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": { @@ -2679,25 +2679,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": { @@ -2705,12 +2705,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": { @@ -2718,20 +2718,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": { @@ -2739,12 +2739,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": { @@ -2752,16 +2752,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", @@ -2773,9 +2773,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", @@ -2786,13 +2786,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": { @@ -2800,16 +2800,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": { @@ -2817,16 +2817,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": { @@ -2834,9 +2834,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" @@ -2846,12 +2846,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": { @@ -3641,25 +3641,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": { @@ -3668,12 +3667,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", @@ -3687,9 +3680,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.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, "node_modules/@redis/bloom": { @@ -4821,34 +4814,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", @@ -4958,12 +4923,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", @@ -5892,21 +5851,22 @@ } }, "node_modules/baileys": { - "version": "7.0.0-rc.9", - "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc.9.tgz", - "integrity": "sha512-Txd2dZ9MHbojvsHckeuCnAKPO/bQjKxua/0tQSJwOKXffK5vpS82k4eA/Nb46K0cK0Bx+fyY0zhnQHYMBriQcw==", + "version": "7.0.0-rc13", + "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc13.tgz", + "integrity": "sha512-v8k74K8B5R7WNYGa26MyJAYEu3Wc4BSuK01QaK8lr30lhE8Nga31nWNu8KN0NDDt+Fsvkq4SQFFI8Q13ghjKmA==", "hasInstallScript": true, "license": "MIT", "dependencies": { "@cacheable/node-cache": "^1.4.0", "@hapi/boom": "^9.1.3", "async-mutex": "^0.5.0", - "libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git", + "libsignal": "^6.0.0", "lru-cache": "^11.1.0", - "music-metadata": "^11.7.0", + "music-metadata": "^11.12.3", "p-queue": "^9.0.0", "pino": "^9.6", - "protobufjs": "^7.2.4", + "protobufjs": "^7.5.6", + "whatsapp-rust-bridge": "0.5.4", "ws": "^8.13.0" }, "engines": { @@ -5914,7 +5874,7 @@ }, "peerDependencies": { "audio-decode": "^2.1.3", - "jimp": "^1.6.0", + "jimp": "^1.6.1", "link-preview-js": "^3.0.0", "sharp": "*" }, @@ -5955,6 +5915,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", @@ -8421,15 +8382,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", @@ -8714,17 +8666,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" @@ -10482,38 +10435,38 @@ } }, "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" @@ -10741,51 +10694,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": { @@ -11363,12 +11278,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": { @@ -11797,9 +11716,9 @@ } }, "node_modules/music-metadata": { - "version": "11.10.3", - "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.10.3.tgz", - "integrity": "sha512-j0g/x4cNNZW6I5gdcPAY+GFkJY9WHTpkFDMBJKQLxJQyvSfQbXm57fTE3haGFFuOzCgtsTd4Plwc49Sn9RacDQ==", + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.13.0.tgz", + "integrity": "sha512-uXRaov9dfjSpQufXIU7sMxVZnh+FilCQv2mXn+K5EJ/decP3dTWrgvPYa5r6MtRbieNSCE708Da4J0u1UGfQIw==", "funding": [ { "type": "github", @@ -11812,80 +11731,32 @@ ], "license": "MIT", "dependencies": { - "@borewit/text-codec": "^0.2.0", + "@borewit/text-codec": "^0.2.2", "@tokenizer/token": "^0.3.0", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "file-type": "^21.1.1", - "media-typer": "^1.1.0", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.5.0" + "file-type": "^21.3.4", + "media-typer": "^2.0.0", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0", + "win-guid": "^0.2.1" }, "engines": { "node": ">=18" } }, - "node_modules/music-metadata/node_modules/file-type": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.1.1.tgz", - "integrity": "sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg==", - "license": "MIT", - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/music-metadata/node_modules/strtok3": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "node_modules/music-metadata/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, "engines": { "node": ">=18" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/music-metadata/node_modules/token-types": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", - "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.1.0", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/music-metadata/node_modules/token-types/node_modules/@borewit/text-codec": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", - "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mute-stream": { @@ -12749,19 +12620,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", @@ -13130,15 +12988,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", @@ -13156,24 +13005,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.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", "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" @@ -13584,62 +13432,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", @@ -14349,9 +14141,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" @@ -14813,16 +14605,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", @@ -15192,16 +14983,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", @@ -16222,6 +16014,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", @@ -16342,6 +16140,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 66e41f0f8..5b6c5f61c 100644 --- a/package.json +++ b/package.json @@ -78,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-rc13", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", From bc6f1b7f1db069b959008d2a8c375ae0f1fe17d0 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian <121687899+kelvinzer0@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:09:50 +0700 Subject: [PATCH 08/32] Update whatsapp.baileys.service.ts --- .../whatsapp/whatsapp.baileys.service.ts | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index d6ce709b5..af79d43be 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -4899,20 +4899,21 @@ export class BaileysStartupService extends ChannelStartupService { } //Business Controller - public async fetchCatalog(instanceName: string, data: getCollectionsDto) { + public async fetchCatalog(instanceName: string, data: getCollectionsDto) { const jid = data.number ? createJid(data.number) : this.client?.user?.id; const limit = Number(data.limit) || 50; - const cursor = data.cursor || null; - + // Tetap hormati cursor dari caller (untuk resume manual jika diperlukan) + let cursor = data.cursor || null; + const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - + if (!onWhatsapp.exists) { throw new BadRequestException(onWhatsapp); } - + try { const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - + let isBusiness = false; try { const business = await this.fetchBusinessProfile(info?.jid); @@ -4920,42 +4921,54 @@ export class BaileysStartupService extends ChannelStartupService { } catch (profileError) { console.log('fetchBusinessProfile failed, continuing catalog fetch:', profileError?.message); } - - let catalog = await this.getCatalog({ jid: info?.jid, limit, cursor }); - let nextPageCursor = catalog.nextPageCursor; - let nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null; - let pagination = nextPageCursorJson?.pagination_cursor - ? JSON.parse(atob(nextPageCursorJson.pagination_cursor)) - : null; - let fetcherHasMore = pagination?.fetcher_has_more === true ? true : false; - - let productsCatalog = catalog.products || []; + + let productsCatalog: Product[] = []; + let fetcherHasMore = true; let countLoops = 0; - while (fetcherHasMore && countLoops < 20) { - catalog = await this.getCatalog({ jid: info?.jid, limit, cursor: nextPageCursor }); - nextPageCursor = catalog.nextPageCursor; - nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null; - pagination = nextPageCursorJson?.pagination_cursor + const MAX_LOOPS = 200; // ~10.000 produk @50/halaman — cukup besar untuk hampir semua kasus nyata + let truncated = false; + + while (fetcherHasMore) { + const catalog = await this.getCatalog({ jid: info?.jid, limit, cursor }); + + productsCatalog = [...productsCatalog, ...(catalog.products || [])]; + + const nextPageCursor = catalog.nextPageCursor; + const nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null; + const pagination = nextPageCursorJson?.pagination_cursor ? JSON.parse(atob(nextPageCursorJson.pagination_cursor)) : null; - fetcherHasMore = pagination?.fetcher_has_more === true ? true : false; - productsCatalog = [...productsCatalog, ...catalog.products]; + + fetcherHasMore = pagination?.fetcher_has_more === true; + cursor = nextPageCursor; countLoops++; + + // Safety valve: hindari infinite loop kalau WhatsApp API berkelakuan aneh + if (countLoops >= MAX_LOOPS) { + truncated = fetcherHasMore; // hanya benar2 "truncated" kalau memang masih ada sisa + this.logger.warn( + `fetchCatalog: mencapai MAX_LOOPS (${MAX_LOOPS}) untuk ${info?.jid}, ` + + `hasil mungkin belum lengkap. Gunakan 'cursor' di response untuk lanjut.`, + ); + break; + } } - + return { wuid: info?.jid || jid, numberExists: info?.exists, - isBusiness: isBusiness, + isBusiness, catalogLength: productsCatalog.length, catalog: productsCatalog, + truncated, // <-- penanda eksplisit ke caller + nextCursor: truncated ? cursor : null, // <-- supaya caller bisa lanjut manual }; } catch (error) { console.log(error); - return { wuid: jid, name: null, isBusiness: false }; + return { wuid: jid, name: null, isBusiness: false, catalog: [], truncated: true }; } } - + public async getCatalog({ jid, limit, From 3dcb58693e6128829a302c6ed6c87fc2843c5932 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian <121687899+kelvinzer0@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:41:20 +0700 Subject: [PATCH 09/32] Update data validation to use business DTOs --- src/api/routes/business.router.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/api/routes/business.router.ts b/src/api/routes/business.router.ts index b4a8abbad..2670c2807 100644 --- a/src/api/routes/business.router.ts +++ b/src/api/routes/business.router.ts @@ -1,5 +1,6 @@ import { RouterBroker } from '@api/abstract/abstract.router'; -import { NumberDto } from '@api/dto/chat.dto'; +import { getCatalogDto } from '@api/dto/business.dto'; +import { getCollectionsDto } from '@api/dto/business.dto'; import { businessController } from '@api/server.module'; import { createMetaErrorResponse } from '@utils/errorResponse'; import { catalogSchema, collectionsSchema } from '@validate/validate.schema'; @@ -53,10 +54,10 @@ export class BusinessRouter extends RouterBroker { */ .post(this.routerPath('getCatalog'), ...guards, async (req, res) => { try { - const response = await this.dataValidate({ + const response = await this.dataValidate({ request: req, schema: catalogSchema, - ClassRef: NumberDto, + ClassRef: getCatalogDto, execute: (instance, data) => businessController.fetchCatalog(instance, data), }); @@ -109,10 +110,10 @@ export class BusinessRouter extends RouterBroker { */ .post(this.routerPath('getCollections'), ...guards, async (req, res) => { try { - const response = await this.dataValidate({ + const response = await this.dataValidate({ request: req, schema: collectionsSchema, - ClassRef: NumberDto, + ClassRef: getCollectionsDto, execute: (instance, data) => businessController.fetchCollections(instance, data), }); From 723ff2d8c87b099c49dac81861793dd73276f233 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian <121687899+kelvinzer0@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:45:07 +0700 Subject: [PATCH 10/32] Update fetchCatalog to use getCatalogDto --- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index af79d43be..ae2b6086e 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1,4 +1,4 @@ -import { getCollectionsDto } from '@api/dto/business.dto'; +import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto'; import { OfferCallDto } from '@api/dto/call.dto'; import { ArchiveChatDto, @@ -4899,7 +4899,7 @@ export class BaileysStartupService extends ChannelStartupService { } //Business Controller - public async fetchCatalog(instanceName: string, data: getCollectionsDto) { + public async fetchCatalog(instanceName: string, data: getCatalogDto) { const jid = data.number ? createJid(data.number) : this.client?.user?.id; const limit = Number(data.limit) || 50; // Tetap hormati cursor dari caller (untuk resume manual jika diperlukan) From 74f0caeebf63aaf3496906eacfc83a6211b614a7 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 01:18:46 +0000 Subject: [PATCH 11/32] feat(catalog): add browser-based catalog & collections provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new CatalogBrowserService that lazily launches a Puppeteer browser session to fetch catalog & collections via web.whatsapp.com's internal WPP API, bypassing Baileys' protocol-level truncation. Architecture: - One Browser per JID (lazy start, idle timeout kill) - Session persisted per instance under instances/{name}/browser-session/ - 4-layer fetch fallback (ported from bedones-whatsapp): 1. queryCatalog with pagination cursor 2. CatalogStore.findQuery 3. WPP.catalog.getMyCatalog 4. WPP.catalog.getProducts (last resort) - Service-locator pattern (BrowserCatalogService.getInstance()) so the non-NestJS-managed BaileysStartupService can access it API: - /business/getCatalog/{instance} now accepts { provider: 'browser' } - /business/getCollections/{instance} now accepts { provider: 'browser' } - Default behavior (no provider field) unchanged — uses Baileys Docker: - Adds chromium + nss + freetype + harfbuzz + font-noto-emoji - Sets PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser - Adds CATALOG_BROWSER_ENABLED (default false) + tunables Config (env vars): - CATALOG_BROWSER_ENABLED (default false) - CATALOG_BROWSER_IDLE_TIMEOUT_MS (default 600000 = 10 min) - CATALOG_BROWSER_MAX_SESSIONS (default 5) - CATALOG_BROWSER_HEADLESS (default true) Why: WhatsApp's anti-bot on protocol level (Baileys) is much stricter than browser level. Bedones-whatsapp proved browser-based fetch works for full catalogs. This ports that approach into Evolution API without touching the existing Baileys messaging path. Refs: investigated with bedones-whatsapp (whatsapp-web.js implementation) Signed-off-by: Kelvin Yuli Andrian --- Dockerfile | 26 +- env.example | 13 + package.json | 1 + src/api/dto/business.dto.ts | 13 + .../whatsapp/catalog-browser.module.ts | 14 + .../whatsapp/catalog-browser.service.ts | 633 ++++++++++++++++++ .../channel/whatsapp/catalog-browser.types.ts | 138 ++++ .../channel/whatsapp/session-store.browser.ts | 118 ++++ .../whatsapp/whatsapp.baileys.service.ts | 23 + src/api/server.module.ts | 8 + src/validate/business.schema.ts | 12 + 11 files changed, 994 insertions(+), 5 deletions(-) create mode 100644 src/api/integrations/channel/whatsapp/catalog-browser.module.ts create mode 100644 src/api/integrations/channel/whatsapp/catalog-browser.service.ts create mode 100644 src/api/integrations/channel/whatsapp/catalog-browser.types.ts create mode 100644 src/api/integrations/channel/whatsapp/session-store.browser.ts diff --git a/Dockerfile b/Dockerfile index 24c4e3bc7..f2d638989 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,16 @@ FROM node:24-alpine AS builder RUN apk update && \ - apk add --no-cache git ffmpeg wget curl bash openssl + apk add --no-cache git ffmpeg wget curl bash openssl \ + chromium nss freetype harfbuzz ttf-freefont -LABEL version="2.3.1" description="Api to control whatsapp features through http requests." -LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" -LABEL contact="contato@evolution-api.com" +LABEL version="2.3.7-catalog-browser" description="Api to control whatsapp features through http requests. Adds browser-based catalog fetch provider." +LABEL maintainer="Kelvin Yuli Andrian" git="https://github.com/kelvinzer0/evolution-api" +LABEL contact="kelvinzer0@users.noreply.github.com" + +# Tell Puppeteer to use system Chromium instead of downloading its own +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser WORKDIR /evolution @@ -33,11 +38,22 @@ RUN npm run build FROM node:24-alpine AS final RUN apk update && \ - apk add tzdata ffmpeg bash openssl + apk add --no-cache tzdata ffmpeg bash openssl \ + chromium nss freetype harfbuzz ttf-freefont font-noto-emoji ENV TZ=America/Sao_Paulo ENV DOCKER_ENV=true +# Puppeteer config — use system Chromium +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + +# Catalog Browser Service config (overridable at runtime) +ENV CATALOG_BROWSER_ENABLED=false +ENV CATALOG_BROWSER_IDLE_TIMEOUT_MS=600000 +ENV CATALOG_BROWSER_MAX_SESSIONS=5 +ENV CATALOG_BROWSER_HEADLESS=true + WORKDIR /evolution COPY --from=builder /evolution/package.json ./package.json diff --git a/env.example b/env.example index 5fe448b82..bb3486aaf 100644 --- a/env.example +++ b/env.example @@ -300,3 +300,16 @@ PROVIDER_ENABLED=false PROVIDER_HOST= PROVIDER_PORT=5656 PROVIDER_PREFIX=evolution + +# === Catalog Browser Service (NEW) === +# Browser-based catalog fetch via web.whatsapp.com (bypasses Baileys protocol limits) +# Set to true to enable. Adds ~500MB RAM per active browser session. +CATALOG_BROWSER_ENABLED=false +# Idle timeout before killing idle browser (ms, default 10 minutes) +CATALOG_BROWSER_IDLE_TIMEOUT_MS=600000 +# Max concurrent browser sessions (default 5) +CATALOG_BROWSER_MAX_SESSIONS=5 +# Headless mode: true (default) | false | shell +CATALOG_BROWSER_HEADLESS=true +# Override Chromium executable path (auto-detected in Docker image) +# PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser diff --git a/package.json b/package.json index 5b6c5f61c..fde0096ce 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "openai": "^4.77.3", "pg": "^8.13.1", "pino": "^9.10.0", + "puppeteer-core": "^23.11.1", "prisma": "^6.1.0", "pusher": "^5.2.0", "qrcode": "^1.5.4", diff --git a/src/api/dto/business.dto.ts b/src/api/dto/business.dto.ts index d999f079d..90c8c7386 100644 --- a/src/api/dto/business.dto.ts +++ b/src/api/dto/business.dto.ts @@ -6,10 +6,23 @@ export class getCatalogDto { number?: string; limit?: number; cursor?: string; + /** + * Which fetch backend to use. + * - `baileys` (default): protocol-level fetch via Baileys library + * - `browser`: launches a Puppeteer browser session to fetch via + * web.whatsapp.com's internal API. Returns full catalog without + * WhatsApp's protocol-level truncation. Requires `CATALOG_BROWSER_ENABLED=true` + * and the user must complete a one-time QR scan for the browser session. + */ + provider?: 'baileys' | 'browser'; } export class getCollectionsDto { number?: string; limit?: number; cursor?: string; + /** + * Which fetch backend to use. See `getCatalogDto.provider`. + */ + provider?: 'baileys' | 'browser'; } diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.module.ts b/src/api/integrations/channel/whatsapp/catalog-browser.module.ts new file mode 100644 index 000000000..193cff994 --- /dev/null +++ b/src/api/integrations/channel/whatsapp/catalog-browser.module.ts @@ -0,0 +1,14 @@ +/** + * NestJS module wiring for the browser-based catalog service. + */ + +import { Module } from '@nestjs/common'; + +import { BrowserCatalogService } from './catalog-browser.service'; +import { BrowserSessionStore } from './session-store.browser'; + +@Module({ + providers: [BrowserCatalogService, BrowserSessionStore], + exports: [BrowserCatalogService, BrowserSessionStore], +}) +export class CatalogBrowserModule {} diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts new file mode 100644 index 000000000..d573a914f --- /dev/null +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -0,0 +1,633 @@ +/** + * BrowserCatalogService + * --------------------------------------------------------------------- + * Singleton service that lazily launches a Puppeteer browser per WhatsApp + * account (JID) to fetch catalog & collections via the internal + * `window.WPP.whatsapp.functions.queryCatalog` API of web.whatsapp.com. + * + * Why this exists: + * WhatsApp's anti-bot/anti-scraping on the protocol level (Baileys) + * is very strict and causes `getCatalog()` to truncate results. The + * same catalog fetched via web.whatsapp.com (browser automation) + * returns the full list because WhatsApp's own frontend code handles + * pagination correctly. + * + * Design: + * - One Browser instance per JID (lazy start) + * - Browser is killed after IDLE_TIMEOUT_MS of inactivity + * - Session is persisted on disk per instance (BrowserSessionStore) + * - If no session exists, returns a QR code the caller must surface + * to the user for scanning + * + * Ported logic from: + * bedones-whatsapp/apps/whatsapp-connector/src/catalog/catalog.service.ts + * (Kelvin Yuli Andrian's own implementation, which proves this works) + */ + +import { Injectable, Logger, BadRequestException } from '@nestjs/common'; +import puppeteer, { Browser, Page } from 'puppeteer-core'; + +import { + BrowserCatalogConfig, + BrowserCatalogOptions, + BrowserCatalogResult, + BrowserCollection, + BrowserCollectionsOptions, + BrowserCollectionsResult, + BrowserProduct, +} from './catalog-browser.types'; +import { BrowserSessionStore } from './session-store.browser'; + +// Return types for in-page scripts (kept as named interfaces to avoid +// TypeScript parser confusion with multi-line arrow type annotations) +interface InPageCatalogResult { + catalog: BrowserProduct[]; + message?: string; +} + +interface InPageCollectionsResult { + collections: BrowserCollection[]; + message?: string; +} + +interface InPageReadyResult { + ready: boolean; + reason?: string; +} + +// JavaScript executed inside the browser page context — has access to +// window.WPP, window.Whatsapp, etc. Cannot reference any Node.js types. +// NOTE: must be self-contained, no closures over outer variables. +const FETCH_CATALOG_IN_PAGE = async (): Promise => { + // Type-loose since we're running in the browser context + const wpp = (window as any).WPP; + if (!wpp) { + return { catalog: [], message: 'WPP not available — page did not load WhatsApp Web' }; + } + + const myUser = wpp.conn ? wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null : null; + const userId = (myUser && myUser._serialized) || ''; + if (!userId) { + return { catalog: [], message: 'User ID not found — not logged in' }; + } + + const whatsappApi = wpp.whatsapp as any; + const productsById = new Map(); + + const addProduct = (rawProduct: any) => { + const product = rawProduct?.attributes || rawProduct; + if (!product?.id) return; + if (!productsById.has(product.id)) { + productsById.set(product.id, product as BrowserProduct); + } + }; + + const extractProductsFromCatalog = (catalogEntry: any): any[] => { + if (!catalogEntry) return []; + const productIndex = catalogEntry.productCollection?._index; + if (!productIndex || typeof productIndex !== 'object') return []; + return Object.keys(productIndex) + .map((productId) => productIndex[productId]?.attributes) + .filter(Boolean); + }; + + // Layer 1: queryCatalog with pagination cursor (most reliable) + if (whatsappApi?.functions?.queryCatalog) { + try { + let afterToken: string | undefined = undefined; + let safetyCount = 0; + while (safetyCount < 500) { + const response: any = await whatsappApi.functions.queryCatalog(userId, afterToken); + const pageProducts: any[] = Array.isArray(response?.data) ? response.data : []; + for (const product of pageProducts) { + addProduct(product); + } + const nextAfter = response?.paging?.cursors?.after; + if (!nextAfter || nextAfter === afterToken) break; + afterToken = nextAfter; + safetyCount++; + } + } catch (error: any) { + // queryCatalog unavailable on this WA version — fall through to next layer + console.log('queryCatalog unavailable:', error?.message); + } + } + + // Layer 2: CatalogStore.findQuery (direct store access) + if (whatsappApi?.CatalogStore?.findQuery) { + try { + const results: any[] = await whatsappApi.CatalogStore.findQuery(userId); + if (Array.isArray(results)) { + for (const entry of results) { + const products = extractProductsFromCatalog(entry); + for (const product of products) { + addProduct(product); + } + } + } + } catch (error: any) { + console.log('CatalogStore.findQuery unavailable:', error?.message); + } + } + + // Layer 3: WPP.catalog.getMyCatalog (fallback) + try { + const myCatalog: any = await wpp.catalog?.getMyCatalog?.(); + const fallbackProducts = extractProductsFromCatalog(myCatalog); + for (const product of fallbackProducts) { + addProduct(product); + } + } catch (error: any) { + console.log('getMyCatalog unavailable:', error?.message); + } + + // Layer 4: last resort — getProducts with hardcoded cap + if (productsById.size === 0) { + try { + const fallbackProducts: any[] = await wpp.catalog?.getProducts?.(userId, 999); + if (Array.isArray(fallbackProducts)) { + for (const product of fallbackProducts) { + addProduct(product); + } + } + } catch (error: any) { + console.log('getProducts unavailable:', error?.message); + } + } + + return { catalog: Array.from(productsById.values()) }; +}; + +// In-page script: fetch all collections +const FETCH_COLLECTIONS_IN_PAGE = async (): Promise => { + const wpp = (window as any).WPP; + if (!wpp) { + return { collections: [], message: 'WPP not available' }; + } + + const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; + const userId = (myUser && myUser._serialized) || ''; + if (!userId) { + return { collections: [], message: 'User ID not found' }; + } + + const whatsappApi = wpp.whatsapp as any; + const collections: BrowserCollection[] = []; + + // Method 1: WPP.catalog.getCollections (preferred) + try { + const result: any = await wpp.catalog?.getCollections?.(userId); + if (Array.isArray(result)) { + for (const c of result) { + const attrs = c?.attributes || c; + if (!attrs?.id) continue; + // Extract products from collection + const productIndex = c?.productCollection?._index || attrs?.products?._index; + const products: BrowserProduct[] = []; + if (productIndex && typeof productIndex === 'object') { + for (const pid of Object.keys(productIndex)) { + const p = productIndex[pid]?.attributes || productIndex[pid]; + if (p) products.push(p as BrowserProduct); + } + } + collections.push({ + id: attrs.id, + name: attrs.name || '', + products, + status: attrs.status, + }); + } + } + } catch (error: any) { + console.log('WPP.catalog.getCollections failed:', error?.message); + } + + // Method 2: CatalogStore fallback (direct store access) + if (collections.length === 0 && whatsappApi?.CollectionStore?.findQuery) { + try { + const results: any[] = await whatsappApi.CollectionStore.findQuery(userId); + if (Array.isArray(results)) { + for (const c of results) { + const attrs = c?.attributes || c; + if (!attrs?.id) continue; + const productIndex = c?.productCollection?._index; + const products: BrowserProduct[] = []; + if (productIndex && typeof productIndex === 'object') { + for (const pid of Object.keys(productIndex)) { + const p = productIndex[pid]?.attributes || productIndex[pid]; + if (p) products.push(p as BrowserProduct); + } + } + collections.push({ + id: attrs.id, + name: attrs.name || '', + products, + status: attrs.status, + }); + } + } + } catch (error: any) { + console.log('CollectionStore.findQuery failed:', error?.message); + } + } + + return { collections }; +}; + +// In-page script: check if WA Web is ready +const IS_WA_READY_IN_PAGE = async (): Promise => { + const wpp = (window as any).WPP; + if (!wpp) return { ready: false, reason: 'WPP not loaded' }; + if (!wpp.isReady) { + try { + if (wpp.waitForReady) await wpp.waitForReady({ timeout: 30000 }); + } catch { + return { ready: false, reason: 'WPP.waitForReady timed out' }; + } + } + const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; + const userId = myUser ? myUser._serialized : null; + if (!userId) return { ready: false, reason: 'No user logged in (need QR scan)' }; + return { ready: true }; +}; + +@Injectable() +export class BrowserCatalogService { + private readonly logger = new Logger(BrowserCatalogService.name); + private readonly config: BrowserCatalogConfig; + + // Per-JID browser instance + private readonly browsers = new Map(); + // Per-JID idle timer + private readonly idleTimers = new Map(); + // Per-JID QR code (when auth pending) + private readonly pendingQr = new Map(); + + /** + * Service locator — set by server.module.ts at bootstrap time so that + * the BaileysStartupService (which is NOT NestJS-managed) can access + * the singleton instance via `BrowserCatalogService.getInstance()`. + * + * Returns null if not initialized (e.g. when CATALOG_BROWSER_ENABLED=false). + */ + private static instance: BrowserCatalogService | null = null; + + static getInstance(): BrowserCatalogService | null { + return BrowserCatalogService.instance; + } + + static setInstance(svc: BrowserCatalogService): void { + BrowserCatalogService.instance = svc; + } + + constructor(private readonly sessionStore: BrowserSessionStore) { + this.config = this.loadConfig(); + if (this.config.enabled) { + this.logger.log( + `Browser catalog service enabled (maxSessions=${this.config.maxSessions}, idleTimeoutMs=${this.config.idleTimeoutMs})`, + ); + } + BrowserCatalogService.setInstance(this); + } + + /** + * Convenience static method: fetch catalog via browser, or throw if disabled. + */ + static async fetchCatalogOrThrow( + options: BrowserCatalogOptions, + ): Promise { + const svc = BrowserCatalogService.getInstance(); + if (!svc) { + throw new BadRequestException( + 'Browser catalog service is not initialized. Set CATALOG_BROWSER_ENABLED=true to enable.', + ); + } + return svc.fetchCatalog(options); + } + + /** + * Convenience static method: fetch collections via browser. + */ + static async fetchCollectionsOrThrow( + options: BrowserCollectionsOptions, + ): Promise { + const svc = BrowserCatalogService.getInstance(); + if (!svc) { + throw new BadRequestException( + 'Browser catalog service is not initialized. Set CATALOG_BROWSER_ENABLED=true to enable.', + ); + } + return svc.fetchCollections(options); + } + + /** + * Load configuration from env vars (with sane defaults). + */ + private loadConfig(): BrowserCatalogConfig { + const enabled = (process.env.CATALOG_BROWSER_ENABLED || 'false').toLowerCase() === 'true'; + const idleTimeoutMs = parseInt(process.env.CATALOG_BROWSER_IDLE_TIMEOUT_MS || '600000', 10); + const maxSessions = parseInt(process.env.CATALOG_BROWSER_MAX_SESSIONS || '5', 10); + const headlessEnv = (process.env.CATALOG_BROWSER_HEADLESS || 'true').toLowerCase(); + const headless: boolean | 'shell' = + headlessEnv === 'shell' ? 'shell' : headlessEnv === 'false' ? false : true; + const executablePath = + process.env.PUPPETEER_EXECUTABLE_PATH || + process.env.CHROMIUM_PATH || + '/usr/bin/chromium-browser'; + + return { + enabled, + idleTimeoutMs, + maxSessions, + headless, + executablePath, + extraArgs: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + '--single-process', + '--memory-pressure-off', + '--no-zygote', + ], + }; + } + + /** + * Public entry: fetch catalog via browser. + * If session is not authenticated, returns a result with qrCode. + */ + async fetchCatalog(options: BrowserCatalogOptions): Promise { + if (!this.config.enabled) { + throw new BadRequestException( + 'Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.', + ); + } + + const { jid, instanceName } = options; + this.logger.log(`[browser] fetchCatalog jid=${jid} instance=${instanceName}`); + + const page = await this.getPage(jid, instanceName); + + try { + // Wait for WA Web to be ready + const ready = await page.evaluate(IS_WA_READY_IN_PAGE); + if (!ready.ready) { + const qrCode = await this.ensureQrCode(jid, instanceName, page); + return { + wuid: jid, + numberExists: true, + isBusiness: true, + catalogLength: 0, + catalog: [], + truncated: false, + nextCursor: null, + source: 'browser', + // Include QR code via cast — caller knows to check for it + ...(qrCode ? ({ qrCode } as any) : {}), + }; + } + + // Clear any pending QR (now authenticated) + this.pendingQr.delete(jid); + + // Run the 4-layer fetch inside the browser + const result = await page.evaluate(FETCH_CATALOG_IN_PAGE); + + if (result.message) { + this.logger.warn(`[browser] fetchCatalog message: ${result.message}`); + } + + this.logger.log(`[browser] fetchCatalog got ${result.catalog.length} products`); + + return { + wuid: jid, + numberExists: true, + isBusiness: true, + catalogLength: result.catalog.length, + catalog: result.catalog, + truncated: false, + nextCursor: null, + source: 'browser', + }; + } finally { + await page.close().catch(() => {}); + this.resetIdleTimer(jid); + } + } + + /** + * Public entry: fetch collections via browser. + */ + async fetchCollections( + options: BrowserCollectionsOptions, + ): Promise { + if (!this.config.enabled) { + throw new BadRequestException( + 'Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.', + ); + } + + const { jid, instanceName } = options; + this.logger.log(`[browser] fetchCollections jid=${jid} instance=${instanceName}`); + + const page = await this.getPage(jid, instanceName); + + try { + const ready = await page.evaluate(IS_WA_READY_IN_PAGE); + if (!ready.ready) { + const qrCode = await this.ensureQrCode(jid, instanceName, page); + return { + wuid: jid, + name: null, + numberExists: true, + isBusiness: true, + collectionsLength: 0, + collections: [], + source: 'browser', + ...(qrCode ? ({ qrCode } as any) : {}), + }; + } + + this.pendingQr.delete(jid); + const result = await page.evaluate(FETCH_COLLECTIONS_IN_PAGE); + + if (result.message) { + this.logger.warn(`[browser] fetchCollections message: ${result.message}`); + } + + this.logger.log(`[browser] fetchCollections got ${result.collections.length} collections`); + + return { + wuid: jid, + name: null, + numberExists: true, + isBusiness: true, + collectionsLength: result.collections.length, + collections: result.collections, + source: 'browser', + }; + } finally { + await page.close().catch(() => {}); + this.resetIdleTimer(jid); + } + } + + /** + * Logout: kill browser + delete session for an instance. + */ + async logout(instanceName: string, jid: string): Promise { + await this.killBrowser(jid); + this.sessionStore.deleteSession(instanceName); + this.pendingQr.delete(jid); + this.logger.log(`[browser] Logged out instance=${instanceName} jid=${jid}`); + } + + /** + * Shutdown all browsers (for graceful app close). + */ + async shutdownAll(): Promise { + const jids = Array.from(this.browsers.keys()); + await Promise.all(jids.map((j) => this.killBrowser(j))); + this.logger.log(`[browser] Shutdown ${jids.length} browser(s)`); + } + + /** + * Get or launch a browser page for the given JID. + */ + private async getPage(jid: string, instanceName: string): Promise { + let browser = this.browsers.get(jid); + if (!browser) { + browser = await this.launchBrowser(jid, instanceName); + } + const page = await browser.newPage(); + await page.setUserAgent( + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + ); + return page; + } + + /** + * Launch a new browser for the JID, navigating to WhatsApp Web and + * restoring session from disk if available. + */ + private async launchBrowser(jid: string, instanceName: string): Promise { + if (this.browsers.size >= this.config.maxSessions) { + // Evict oldest idle browser + const oldestJid = this.browsers.keys().next().value; + if (oldestJid) { + this.logger.warn(`[browser] Max sessions reached, evicting oldest: ${oldestJid}`); + await this.killBrowser(oldestJid); + } + } + + const userDataDir = this.sessionStore.userDataDir(instanceName); + this.logger.log(`[browser] Launching Chromium for instance=${instanceName} jid=${jid}`); + + const browser = await puppeteer.launch({ + executablePath: this.config.executablePath, + headless: this.config.headless, + userDataDir, + args: this.config.extraArgs, + defaultViewport: { width: 1280, height: 800 }, + ignoreDefaultArgs: ['--enable-automation'], + }); + + this.browsers.set(jid, browser); + + // Navigate to WA Web on the first page + const page = await browser.newPage(); + await page.goto('https://web.whatsapp.com/', { + waitUntil: 'domcontentloaded', + timeout: 60000, + }); + + // Give WA Web time to initialize + await new Promise((r) => setTimeout(r, 5000)); + + return browser; + } + + /** + * Ensure a QR code is available for the user to scan. + * Returns the QR data URL if authentication is required. + */ + private async ensureQrCode( + jid: string, + instanceName: string, + page: Page, + ): Promise { + // Check if we already have a pending QR for this JID + const existing = this.pendingQr.get(jid); + if (existing) return existing; + + // Try to extract QR from WA Web page + try { + // WA Web renders QR as a canvas — extract data URL + const qrDataUrl = await page.evaluate(async () => { + const wpp = (window as any).WPP; + if (wpp?.conn?.getQRCode) { + try { + return await wpp.conn.getQRCode(); + } catch { + /* fall through */ + } + } + // Fallback: scrape canvas + const canvas = document.querySelector('canvas[aria-label="QR code"], canvas'); + if (canvas) { + return (canvas as HTMLCanvasElement).toDataURL('image/png'); + } + return null; + }); + + if (qrDataUrl) { + this.pendingQr.set(jid, qrDataUrl); + return qrDataUrl; + } + } catch (err) { + this.logger.warn(`[browser] Failed to extract QR: ${(err as Error).message}`); + } + + return null; + } + + /** + * Reset the idle timer — call after each activity. + * When timer fires, the browser is killed to free memory. + */ + private resetIdleTimer(jid: string): void { + const existing = this.idleTimers.get(jid); + if (existing) clearTimeout(existing); + + const timer = setTimeout(() => { + this.logger.log(`[browser] Idle timeout for jid=${jid}, killing browser`); + this.killBrowser(jid).catch((err) => { + this.logger.error(`[browser] Failed to kill idle browser: ${(err as Error).message}`); + }); + }, this.config.idleTimeoutMs); + + this.idleTimers.set(jid, timer); + } + + /** + * Kill the browser for a JID and clean up timers. + */ + private async killBrowser(jid: string): Promise { + const timer = this.idleTimers.get(jid); + if (timer) { + clearTimeout(timer); + this.idleTimers.delete(jid); + } + const browser = this.browsers.get(jid); + if (browser) { + try { + await browser.close(); + } catch (err) { + this.logger.warn(`[browser] Error closing browser: ${(err as Error).message}`); + } + this.browsers.delete(jid); + } + this.pendingQr.delete(jid); + } +} diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.types.ts b/src/api/integrations/channel/whatsapp/catalog-browser.types.ts new file mode 100644 index 000000000..abd4eb1e5 --- /dev/null +++ b/src/api/integrations/channel/whatsapp/catalog-browser.types.ts @@ -0,0 +1,138 @@ +/** + * Type definitions for the browser-based catalog service. + * + * The BrowserCatalogService is a singleton that lazily launches a Puppeteer + * browser per WhatsApp account (identified by JID) to fetch catalog & + * collections via the internal `window.WPP.whatsapp.functions.queryCatalog` + * API of web.whatsapp.com — bypassing the limitations of Baileys' protocol + * level `getCatalog()` implementation. + * + * Why: WhatsApp's anti-bot/anti-scraping is very strict on the protocol + * level (Baileys), but relaxed on the browser level (whatsapp-web.js / + * direct WA Web access). Bedones-whatsapp proved this approach works for + * fetching full catalogs. + */ + +/** + * Catalog product as returned by the browser fetch path. + * Shape mirrors Baileys' `Product` type for API response compatibility. + */ +export interface BrowserProduct { + id: string; + name?: string; + description?: string; + currency?: string; + price?: number; + imageCdnUrl?: string; + image_cdn_url?: string; + image_cdn_urls?: Array<{ key: string; value: string }>; + additionalImageCdnUrl?: string[]; + additional_image_cdn_urls?: Array>; + retailerId?: string; + retailer_id?: string; + url?: string; + isHidden?: boolean; + availability?: string; + [key: string]: unknown; +} + +/** + * Catalog collection as returned by the browser fetch path. + * Shape mirrors Baileys' `CatalogCollection` type. + */ +export interface BrowserCollection { + id: string; + name: string; + products: BrowserProduct[]; + status?: string; + [key: string]: unknown; +} + +/** + * Options for fetching catalog via the browser path. + */ +export interface BrowserCatalogOptions { + /** JID of the WhatsApp Business account whose catalog to fetch */ + jid: string; + /** Instance name (used for session file path isolation) */ + instanceName: string; + /** Optional: max number of products per pagination call (default 50) */ + pageSize?: number; + /** Optional: max number of pagination loops before stopping (default 200) */ + maxLoops?: number; +} + +/** + * Options for fetching collections via the browser path. + */ +export interface BrowserCollectionsOptions { + /** JID of the WhatsApp Business account */ + jid: string; + /** Instance name */ + instanceName: string; + /** Optional: limit */ + limit?: number; +} + +/** + * Result of a catalog fetch operation. + */ +export interface BrowserCatalogResult { + wuid: string; + numberExists: boolean; + isBusiness: boolean; + catalogLength: number; + catalog: BrowserProduct[]; + truncated: boolean; + nextCursor: string | null; + source: 'browser'; +} + +/** + * Result of a collections fetch operation. + */ +export interface BrowserCollectionsResult { + wuid: string; + name: string | null; + numberExists: boolean; + isBusiness: boolean; + collectionsLength: number; + collections: BrowserCollection[]; + source: 'browser'; +} + +/** + * Internal state of a browser session. + */ +export interface BrowserSessionState { + /** JID of the WhatsApp account */ + jid: string; + /** Instance name */ + instanceName: string; + /** Whether the browser is authenticated */ + authenticated: boolean; + /** QR code data URL, if pending authentication */ + qrCode?: string; + /** Timestamp of last activity (ms epoch) */ + lastActivity: number; + /** Timestamp of session creation */ + createdAt: number; +} + +/** + * Configuration for the BrowserCatalogService. + */ +export interface BrowserCatalogConfig { + /** Master toggle — if false, all browser calls fall back to Baileys */ + enabled: boolean; + /** Idle timeout before killing browser (ms) */ + idleTimeoutMs: number; + /** Max concurrent browser sessions */ + maxSessions: number; + /** Headless mode (true | false | 'shell' in Puppeteer v23+) */ + headless: boolean | 'shell'; + /** Custom executable path (overrides PUPPETEER_EXECUTABLE_PATH env) */ + executablePath?: string; + /** Extra Puppeteer launch args */ + extraArgs?: string[]; +} diff --git a/src/api/integrations/channel/whatsapp/session-store.browser.ts b/src/api/integrations/channel/whatsapp/session-store.browser.ts new file mode 100644 index 000000000..caa5844f9 --- /dev/null +++ b/src/api/integrations/channel/whatsapp/session-store.browser.ts @@ -0,0 +1,118 @@ +/** + * Persistent session store for the browser-based catalog service. + * + * WhatsApp Web (whatsapp-web.js style) stores session as a set of files + * in a directory. We mirror this pattern, storing per-instance sessions + * under `${INSTANCE_DIR}/${instanceName}/browser-session/`. + * + * This survives across browser restarts so the user only needs to scan + * the QR code ONCE per WhatsApp account. + */ + +import { Injectable, Logger } from '@nestjs/common'; +import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +import { INSTANCE_DIR } from '@config/path.config'; + +const SESSION_SUBDIR = 'browser-session'; + +export interface StoredSessionData { + [key: string]: unknown; +} + +@Injectable() +export class BrowserSessionStore { + private readonly logger = new Logger(BrowserSessionStore.name); + + /** + * Resolve the session directory for an instance. + */ + private sessionDir(instanceName: string): string { + return join(INSTANCE_DIR, instanceName, SESSION_SUBDIR); + } + + /** + * Check if a saved session exists for this instance. + */ + hasSession(instanceName: string): boolean { + const dir = this.sessionDir(instanceName); + if (!existsSync(dir)) return false; + const files = readdirSync(dir).filter((f) => !f.startsWith('.')); + return files.length > 0; + } + + /** + * Load all session files for an instance. + * Returns a map of filename → file content (JSON-parsed when possible). + */ + loadSession(instanceName: string): StoredSessionData { + const dir = this.sessionDir(instanceName); + const result: StoredSessionData = {}; + + if (!existsSync(dir)) return result; + + for (const file of readdirSync(dir)) { + if (file.startsWith('.')) continue; + const fullPath = join(dir, file); + try { + const raw = readFileSync(fullPath, 'utf8'); + try { + result[file] = JSON.parse(raw); + } catch { + result[file] = raw; + } + } catch (err) { + this.logger.warn(`Failed to read session file ${file}: ${(err as Error).message}`); + } + } + + return result; + } + + /** + * Save session data back to disk. + * Each top-level key becomes a file; values are JSON-serialized. + */ + saveSession(instanceName: string, data: StoredSessionData): void { + const dir = this.sessionDir(instanceName); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + for (const [file, value] of Object.entries(data)) { + const fullPath = join(dir, file); + const serialized = + typeof value === 'string' ? value : JSON.stringify(value, null, 2); + try { + writeFileSync(fullPath, serialized, 'utf8'); + } catch (err) { + this.logger.warn(`Failed to write session file ${file}: ${(err as Error).message}`); + } + } + } + + /** + * Delete the entire session for an instance (logout). + */ + deleteSession(instanceName: string): void { + const dir = this.sessionDir(instanceName); + if (existsSync(dir)) { + rmSync(dir, { recursive: true, force: true }); + this.logger.log(`Deleted browser session for instance ${instanceName}`); + } + } + + /** + * Path where session lives (for Puppeteer's userDataDir option). + * Using userDataDir lets WhatsApp Web store IndexedDB + LocalStorage + * so we don't need to manually restore session tokens. + */ + userDataDir(instanceName: string): string { + const dir = this.sessionDir(instanceName); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + return dir; + } +} diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index ae2b6086e..b9b858f87 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -153,6 +153,7 @@ import { PassThrough, Readable } from 'stream'; import { v4 } from 'uuid'; import { BaileysMessageProcessor } from './baileysMessage.processor'; +import { BrowserCatalogService } from './catalog-browser.service'; import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys'; export interface ExtendedIMessageKey extends proto.IMessageKey { @@ -4900,6 +4901,17 @@ export class BaileysStartupService extends ChannelStartupService { //Business Controller public async fetchCatalog(instanceName: string, data: getCatalogDto) { + // === Provider routing: browser fallback for full catalog === + if (data.provider === 'browser') { + const jid = data.number ? createJid(data.number) : this.client?.user?.id; + return BrowserCatalogService.fetchCatalogOrThrow({ + jid, + instanceName, + pageSize: Number(data.limit) || 50, + }); + } + // === End provider routing === + const jid = data.number ? createJid(data.number) : this.client?.user?.id; const limit = Number(data.limit) || 50; // Tetap hormati cursor dari caller (untuk resume manual jika diperlukan) @@ -4990,6 +5002,17 @@ export class BaileysStartupService extends ChannelStartupService { } public async fetchCollections(instanceName: string, data: getCollectionsDto) { + // === Provider routing: browser fallback for full collections === + if (data.provider === 'browser') { + const jid = data.number ? createJid(data.number) : this.client?.user?.id; + return BrowserCatalogService.fetchCollectionsOrThrow({ + jid, + instanceName, + limit: Number(data.limit) || 100, + }); + } + // === End provider routing === + const jid = data.number ? createJid(data.number) : this.client?.user?.id; const limit = Number(data.limit) || 100; diff --git a/src/api/server.module.ts b/src/api/server.module.ts index 668b9e272..3fd01e2ee 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -17,6 +17,8 @@ import { ChannelController } from './integrations/channel/channel.controller'; import { EvolutionController } from './integrations/channel/evolution/evolution.controller'; import { MetaController } from './integrations/channel/meta/meta.controller'; import { BaileysController } from './integrations/channel/whatsapp/baileys.controller'; +import { BrowserCatalogService } from './integrations/channel/whatsapp/catalog-browser.service'; +import { BrowserSessionStore } from './integrations/channel/whatsapp/session-store.browser'; import { ChatbotController } from './integrations/chatbot/chatbot.controller'; import { ChatwootController } from './integrations/chatbot/chatwoot/controllers/chatwoot.controller'; import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service'; @@ -107,6 +109,12 @@ export const businessController = new BusinessController(waMonitor); export const groupController = new GroupController(waMonitor); export const labelController = new LabelController(waMonitor); +// Browser-based catalog service (singleton, lazy-initialized) +// Construction auto-registers itself with BrowserCatalogService.setInstance() +// so BaileysStartupService can access it via BrowserCatalogService.getInstance() +const browserSessionStore = new BrowserSessionStore(); +export const browserCatalogService = new BrowserCatalogService(browserSessionStore); + export const eventManager = new EventManager(prismaRepository, waMonitor); export const chatbotController = new ChatbotController(prismaRepository, waMonitor); export const channelController = new ChannelController(prismaRepository, waMonitor); diff --git a/src/validate/business.schema.ts b/src/validate/business.schema.ts index cc0cb3219..812e9e8fe 100644 --- a/src/validate/business.schema.ts +++ b/src/validate/business.schema.ts @@ -1,11 +1,22 @@ import { JSONSchema7 } from 'json-schema'; +const providerProperty: JSONSchema7 = { + type: 'string', + enum: ['baileys', 'browser'], + description: + "Fetch backend. 'baileys' (default) uses Baileys protocol-level API. " + + "'browser' launches a Puppeteer session that fetches via web.whatsapp.com — " + + 'returns full catalog without protocol-level truncation. ' + + 'Requires CATALOG_BROWSER_ENABLED=true and one-time QR scan per instance.', +}; + export const catalogSchema: JSONSchema7 = { type: 'object', properties: { number: { type: 'string' }, limit: { type: ['number', 'string'] }, cursor: { type: 'string' }, + provider: providerProperty, }, }; @@ -15,5 +26,6 @@ export const collectionsSchema: JSONSchema7 = { number: { type: 'string' }, limit: { type: ['number', 'string'] }, cursor: { type: 'string' }, + provider: providerProperty, }, }; From c27eb23c6283cead8cbdb5bc96080ee8b57b63bf Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 01:19:57 +0000 Subject: [PATCH 12/32] docs: add catalog-browser-provider usage docs Signed-off-by: Kelvin Yuli Andrian --- docs/catalog-browser-provider.md | 261 +++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 docs/catalog-browser-provider.md diff --git a/docs/catalog-browser-provider.md b/docs/catalog-browser-provider.md new file mode 100644 index 000000000..05e49a54f --- /dev/null +++ b/docs/catalog-browser-provider.md @@ -0,0 +1,261 @@ +# Browser-Based Catalog Provider + +> Fetch full WhatsApp Business catalog & collections via web.whatsapp.com +> browser automation, bypassing Baileys' protocol-level truncation. + +--- + +## Why This Exists + +WhatsApp's anti-bot/anti-scraping on the **protocol level** (Baileys) is +much stricter than on the **browser level** (web.whatsapp.com). As a +result, Baileys' `getCatalog()` may return truncated results even when +the catalog has more products. + +The browser-based provider launches a Puppeteer-controlled Chromium +session, navigates to web.whatsapp.com, and uses WhatsApp Web's own +internal `window.WPP.whatsapp.functions.queryCatalog` API — the same +code path WhatsApp's frontend uses. This returns the full catalog +without protocol-level truncation. + +**Reference implementation**: ported from +[kelvinzer0/bedones-whatsapp](https://github.com/kelvinzer0/bedones-whatsapp) +which uses `whatsapp-web.js` + this same 4-layer fetch approach. + +--- + +## How to Enable + +### 1. Set environment variables + +In your `docker-compose.yml` or env file: + +```yaml +environment: + CATALOG_BROWSER_ENABLED: 'true' + # Optional tunables (with defaults): + CATALOG_BROWSER_IDLE_TIMEOUT_MS: '600000' # 10 minutes + CATALOG_BROWSER_MAX_SESSIONS: '5' + CATALOG_BROWSER_HEADLESS: 'true' # true | false | shell + # PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium-browser # auto-set in Docker +``` + +### 2. Rebuild Docker image + +The Dockerfile already installs Chromium + required fonts. Just rebuild: + +```bash +docker compose build evolution-api +docker compose up -d evolution-api +``` + +**Image size increase**: ~200 MB (Chromium + fonts). + +### 3. RAM requirements + +Each active browser session uses ~500 MB RAM. The browser is killed +after `CATALOG_BROWSER_IDLE_TIMEOUT_MS` of inactivity, freeing memory. + +**Recommendation**: ensure your Docker host has at least 1 GB free RAM +beyond existing workloads before enabling. For hosts with limited RAM +(< 4 GB total), set `CATALOG_BROWSER_MAX_SESSIONS=1` and a shorter idle +timeout (e.g. `CATALOG_BROWSER_IDLE_TIMEOUT_MS=120000` for 2 minutes). + +--- + +## How to Use + +### Fetch catalog via browser + +```bash +curl -X POST "https://your-evolution-host/business/getCatalog/{instanceName}" \ + -H "Content-Type: application/json" \ + -H "apikey: YOUR_API_KEY" \ + -d '{ + "provider": "browser" + }' +``` + +### Fetch collections via browser + +```bash +curl -X POST "https://your-evolution-host/business/getCollections/{instanceName}" \ + -H "Content-Type: application/json" \ + -H "apikey: YOUR_API_KEY" \ + -d '{ + "provider": "browser" + }' +``` + +### Backward compatible + +If you don't pass `provider`, the existing Baileys path is used — no +behavior change for existing clients (n8n, Odoo, etc.). + +--- + +## First-Time Setup: QR Scan + +The first time you call the browser provider for a given instance, the +response will include a `qrCode` field (a `data:image/png;base64,...` URL) +because the browser session isn't authenticated yet. + +**Response shape (pending auth)**: +```json +{ + "wuid": "1234567890@s.whatsapp.net", + "numberExists": true, + "isBusiness": true, + "catalogLength": 0, + "catalog": [], + "truncated": false, + "nextCursor": null, + "source": "browser", + "qrCode": "data:image/png;base64,..." +} +``` + +**To authenticate**: +1. Render the `qrCode` data URL as an image (any browser, or `qrencode -t ANSIUTF8`) +2. On your phone, open WhatsApp → Settings → Linked Devices → Link a Device +3. Scan the QR code +4. Wait 5–10 seconds for WA Web to log in +5. Call the endpoint again — you'll get the full catalog + +The session is persisted at `instances/{instanceName}/browser-session/` +and survives browser restarts, container restarts, and host reboots (as +long as the volume `evolution-instances` is preserved). + +**You only need to scan the QR code ONCE per WhatsApp account.** The +browser session is independent from the Baileys session — you'll have +two linked devices on your phone (one for Baileys messaging, one for +browser catalog fetch). + +--- + +## How It Works (Internal) + +### Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Evolution API │ +│ │ +│ ┌──────────────────┐ ┌──────────────────────────┐ │ +│ │ BaileysStartup │ │ BrowserCatalogService │ │ +│ │ Service │───►│ (singleton, lazy-launch) │ │ +│ │ │ │ │ │ +│ │ fetchCatalog() │ │ - Puppeteer Chromium │ │ +│ │ if provider= │ │ - One Browser per JID │ │ +│ │ 'browser' │ │ - Idle kill (10 min) │ │ +│ │ → delegate │ │ - Persistent session │ │ +│ └──────────────────┘ └──────────────────────────┘ │ +│ │ +│ Session files: instances/{name}/browser-session/ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 4-Layer Catalog Fetch + +Inside the browser page context, the service tries 4 strategies in +order, deduplicating products by ID: + +1. **`WPP.whatsapp.functions.queryCatalog(userId, afterToken)`** — paginated + cursor loop (most reliable, primary path) +2. **`WPP.whatsapp.CatalogStore.findQuery(userId)`** — direct store access +3. **`WPP.catalog.getMyCatalog()`** — high-level wrapper +4. **`WPP.catalog.getProducts(userId, 999)`** — last-resort hardcoded cap + +### Collections Fetch + +Uses `WPP.catalog.getCollections(userId)` with +`CollectionStore.findQuery` fallback. + +### Resource Management + +- One `Browser` instance per JID, lazy-started on first request +- Browser is killed after `CATALOG_BROWSER_IDLE_TIMEOUT_MS` (default 10 min) +- Max concurrent sessions capped at `CATALOG_BROWSER_MAX_SESSIONS` (default 5) +- When limit reached, oldest idle browser is evicted +- All browsers killed on app shutdown + +--- + +## Files Changed + +| File | Purpose | +|---|---| +| `src/api/integrations/channel/whatsapp/catalog-browser.service.ts` | Core service — 4-layer fetch, browser pool, idle timeout | +| `src/api/integrations/channel/whatsapp/catalog-browser.types.ts` | Type definitions | +| `src/api/integrations/channel/whatsapp/catalog-browser.module.ts` | NestJS module wiring | +| `src/api/integrations/channel/whatsapp/session-store.browser.ts` | Persistent session storage | +| `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` | Routing: `provider=browser` delegates to BrowserCatalogService | +| `src/api/dto/business.dto.ts` | Added `provider?: 'baileys' \| 'browser'` field | +| `src/validate/business.schema.ts` | JSON Schema for the new field | +| `src/api/server.module.ts` | Bootstrap singleton instance of BrowserCatalogService | +| `Dockerfile` | Install Chromium + fonts, set PUPPETEER env vars | +| `package.json` | Added `puppeteer-core@^23.11.1` dependency | +| `env.example` | Documented new env vars | + +--- + +## Troubleshooting + +### `Browser catalog service is disabled` + +Set `CATALOG_BROWSER_ENABLED=true` and restart the container. + +### `Browser catalog service is not initialized` + +The service constructor failed. Check logs for the initialization +error. Likely cause: missing Chromium binary — verify +`PUPPETEER_EXECUTABLE_PATH` points to a real Chromium binary +(`ls -la /usr/bin/chromium-browser` inside the container). + +### QR code never appears + +Make sure `web.whatsapp.com` is reachable from inside the container. +Try `curl -I https://web.whatsapp.com/` from inside the container. + +### Browser OOM kills + +Check container memory usage: +```bash +docker stats evolution_api +``` + +If the container hits the host memory limit, either: +- Increase host RAM +- Reduce `CATALOG_BROWSER_MAX_SESSIONS` to 1 +- Reduce `CATALOG_BROWSER_IDLE_TIMEOUT_MS` to 60000 (1 minute) + +### Session expired + +If WA Web session expires (rare, happens after ~30 days inactive), +delete the session dir and scan QR again: + +```bash +docker exec evolution_api rm -rf /evolution/instances/{instanceName}/browser-session +``` + +Then call the endpoint again to get a fresh QR code. + +--- + +## Limitations + +- **First call is slow**: Browser launch + WA Web load = 10–30 seconds. + Subsequent calls within the idle window are fast (~2 seconds). +- **QR scan required once per WhatsApp account**: Browser session is + independent from Baileys session. You'll see two linked devices on + your phone. +- **Memory heavy**: Each active browser = ~500 MB RAM. Plan capacity + accordingly. +- **Compliance**: Browser automation is against WhatsApp ToS in + principle, but enforcement is rare for legitimate catalog reads. + Use at your own risk. + +--- + +*Added in evolution-api v2.3.7-catalog-browser* +*Branch: `feature/catalog-browser-provider`* From c3dda712d542db8629f650adbfc43292137135d0 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 01:27:40 +0000 Subject: [PATCH 13/32] fix(deps): regenerate package-lock.json with puppeteer-core CI npm ci was failing because package-lock.json was out of sync after adding puppeteer-core to package.json. Regenerate lock file. Signed-off-by: Kelvin Yuli Andrian --- package-lock.json | 591 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 551 insertions(+), 40 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2e665595f..05b29dc5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "pg": "^8.13.1", "pino": "^9.10.0", "prisma": "^6.1.0", + "puppeteer-core": "^23.11.1", "pusher": "^5.2.0", "qrcode": "^1.5.4", "qrcode-terminal": "^0.12.0", @@ -3685,6 +3686,28 @@ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, + "node_modules/@puppeteer/browsers": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", + "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -4820,6 +4843,12 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -5094,6 +5123,16 @@ "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "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", @@ -5735,6 +5774,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", @@ -5850,6 +5901,20 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/baileys": { "version": "7.0.0-rc13", "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc13.tgz", @@ -5911,11 +5976,90 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz", + "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "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", @@ -5941,6 +6085,15 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -6051,7 +6204,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -6341,6 +6493,28 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chromium-bidi": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", + "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -6470,7 +6644,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -6952,6 +7125,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -7134,6 +7316,20 @@ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "license": "MIT" }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7197,6 +7393,12 @@ "node": ">=8" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1367902", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", + "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", + "license": "BSD-3-Clause" + }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -7359,6 +7561,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/engine.io": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", @@ -7745,6 +7956,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", @@ -8306,6 +8548,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -8336,7 +8591,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -8346,7 +8600,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -8382,6 +8635,15 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/exif-parser": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", @@ -8491,6 +8753,26 @@ "node": ">=4" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", @@ -8527,6 +8809,12 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -8616,6 +8904,15 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fetch-socks": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fetch-socks/-/fetch-socks-1.3.2.tgz", @@ -9136,6 +9433,21 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -9167,6 +9479,20 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/gifwrap": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", @@ -9554,6 +9880,19 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -11631,6 +11970,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -11818,6 +12163,15 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/nkeys.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/nkeys.js/-/nkeys.js-1.1.0.tgz", @@ -12116,7 +12470,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -12419,6 +12772,38 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -12620,6 +13005,12 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -13004,6 +13395,15 @@ ], "license": "MIT" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/protobufjs": { "version": "7.6.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", @@ -13049,18 +13449,73 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "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" }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "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/puppeteer-core": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", + "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1367902", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -14441,6 +14896,17 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.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", @@ -14821,6 +15287,50 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-extensions": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", @@ -14875,7 +15385,6 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true, "license": "MIT" }, "node_modules/through2": { @@ -15150,7 +15659,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15167,7 +15675,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15184,7 +15691,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15201,7 +15707,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15218,7 +15723,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15235,7 +15739,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15252,7 +15755,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15269,7 +15771,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15286,7 +15787,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15303,7 +15803,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15320,7 +15819,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15337,7 +15835,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15354,7 +15851,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15371,7 +15867,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15388,7 +15883,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15405,7 +15899,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15422,7 +15915,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15439,7 +15931,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15456,7 +15947,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15473,7 +15963,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15490,7 +15979,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15507,7 +15995,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15524,7 +16011,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15541,7 +16027,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15558,7 +16043,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15575,7 +16059,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15765,6 +16248,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "license": "MIT" + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -15822,6 +16311,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "node_modules/undici": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", @@ -16160,7 +16659,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -16178,7 +16676,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" @@ -16194,7 +16691,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" @@ -16207,14 +16703,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/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -16315,7 +16809,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -16334,12 +16827,30 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yauzl/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/yocto-queue": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", From fa74c0d91836cc0b647cd1594325a83d651f30e3 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 01:31:27 +0000 Subject: [PATCH 14/32] fix(catalog-browser): remove NestJS deps, use Evolution API's native Logger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evolution API doesn't use NestJS — it uses Express with manual DI. Refactored to use the project's own Logger class and BadRequestException from @exceptions. Removed catalog-browser.module.ts (NestJS module, not needed). Now compiles clean against the existing codebase. Signed-off-by: Kelvin Yuli Andrian --- .../whatsapp/catalog-browser.module.ts | 14 ------- .../whatsapp/catalog-browser.service.ts | 39 ++++++------------- .../channel/whatsapp/session-store.browser.ts | 11 ++---- 3 files changed, 15 insertions(+), 49 deletions(-) delete mode 100644 src/api/integrations/channel/whatsapp/catalog-browser.module.ts diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.module.ts b/src/api/integrations/channel/whatsapp/catalog-browser.module.ts deleted file mode 100644 index 193cff994..000000000 --- a/src/api/integrations/channel/whatsapp/catalog-browser.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * NestJS module wiring for the browser-based catalog service. - */ - -import { Module } from '@nestjs/common'; - -import { BrowserCatalogService } from './catalog-browser.service'; -import { BrowserSessionStore } from './session-store.browser'; - -@Module({ - providers: [BrowserCatalogService, BrowserSessionStore], - exports: [BrowserCatalogService, BrowserSessionStore], -}) -export class CatalogBrowserModule {} diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index d573a914f..50c7a9a89 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -24,7 +24,8 @@ * (Kelvin Yuli Andrian's own implementation, which proves this works) */ -import { Injectable, Logger, BadRequestException } from '@nestjs/common'; +import { Logger } from '@config/logger.config'; +import { BadRequestException } from '@exceptions'; import puppeteer, { Browser, Page } from 'puppeteer-core'; import { @@ -65,7 +66,7 @@ const FETCH_CATALOG_IN_PAGE = async (): Promise => { return { catalog: [], message: 'WPP not available — page did not load WhatsApp Web' }; } - const myUser = wpp.conn ? wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null : null; + const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; const userId = (myUser && myUser._serialized) || ''; if (!userId) { return { catalog: [], message: 'User ID not found — not logged in' }; @@ -251,7 +252,6 @@ const IS_WA_READY_IN_PAGE = async (): Promise => { return { ready: true }; }; -@Injectable() export class BrowserCatalogService { private readonly logger = new Logger(BrowserCatalogService.name); private readonly config: BrowserCatalogConfig; @@ -293,9 +293,7 @@ export class BrowserCatalogService { /** * Convenience static method: fetch catalog via browser, or throw if disabled. */ - static async fetchCatalogOrThrow( - options: BrowserCatalogOptions, - ): Promise { + static async fetchCatalogOrThrow(options: BrowserCatalogOptions): Promise { const svc = BrowserCatalogService.getInstance(); if (!svc) { throw new BadRequestException( @@ -308,9 +306,7 @@ export class BrowserCatalogService { /** * Convenience static method: fetch collections via browser. */ - static async fetchCollectionsOrThrow( - options: BrowserCollectionsOptions, - ): Promise { + static async fetchCollectionsOrThrow(options: BrowserCollectionsOptions): Promise { const svc = BrowserCatalogService.getInstance(); if (!svc) { throw new BadRequestException( @@ -328,12 +324,9 @@ export class BrowserCatalogService { const idleTimeoutMs = parseInt(process.env.CATALOG_BROWSER_IDLE_TIMEOUT_MS || '600000', 10); const maxSessions = parseInt(process.env.CATALOG_BROWSER_MAX_SESSIONS || '5', 10); const headlessEnv = (process.env.CATALOG_BROWSER_HEADLESS || 'true').toLowerCase(); - const headless: boolean | 'shell' = - headlessEnv === 'shell' ? 'shell' : headlessEnv === 'false' ? false : true; + const headless: boolean | 'shell' = headlessEnv === 'shell' ? 'shell' : headlessEnv === 'false' ? false : true; const executablePath = - process.env.PUPPETEER_EXECUTABLE_PATH || - process.env.CHROMIUM_PATH || - '/usr/bin/chromium-browser'; + process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROMIUM_PATH || '/usr/bin/chromium-browser'; return { enabled, @@ -359,9 +352,7 @@ export class BrowserCatalogService { */ async fetchCatalog(options: BrowserCatalogOptions): Promise { if (!this.config.enabled) { - throw new BadRequestException( - 'Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.', - ); + throw new BadRequestException('Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.'); } const { jid, instanceName } = options; @@ -419,13 +410,9 @@ export class BrowserCatalogService { /** * Public entry: fetch collections via browser. */ - async fetchCollections( - options: BrowserCollectionsOptions, - ): Promise { + async fetchCollections(options: BrowserCollectionsOptions): Promise { if (!this.config.enabled) { - throw new BadRequestException( - 'Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.', - ); + throw new BadRequestException('Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.'); } const { jid, instanceName } = options; @@ -552,11 +539,7 @@ export class BrowserCatalogService { * Ensure a QR code is available for the user to scan. * Returns the QR data URL if authentication is required. */ - private async ensureQrCode( - jid: string, - instanceName: string, - page: Page, - ): Promise { + private async ensureQrCode(jid: string, instanceName: string, page: Page): Promise { // Check if we already have a pending QR for this JID const existing = this.pendingQr.get(jid); if (existing) return existing; diff --git a/src/api/integrations/channel/whatsapp/session-store.browser.ts b/src/api/integrations/channel/whatsapp/session-store.browser.ts index caa5844f9..ccd9861e0 100644 --- a/src/api/integrations/channel/whatsapp/session-store.browser.ts +++ b/src/api/integrations/channel/whatsapp/session-store.browser.ts @@ -9,11 +9,10 @@ * the QR code ONCE per WhatsApp account. */ -import { Injectable, Logger } from '@nestjs/common'; -import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs'; -import { join } from 'path'; - +import { Logger } from '@config/logger.config'; import { INSTANCE_DIR } from '@config/path.config'; +import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs'; +import { join } from 'path'; const SESSION_SUBDIR = 'browser-session'; @@ -21,7 +20,6 @@ export interface StoredSessionData { [key: string]: unknown; } -@Injectable() export class BrowserSessionStore { private readonly logger = new Logger(BrowserSessionStore.name); @@ -82,8 +80,7 @@ export class BrowserSessionStore { for (const [file, value] of Object.entries(data)) { const fullPath = join(dir, file); - const serialized = - typeof value === 'string' ? value : JSON.stringify(value, null, 2); + const serialized = typeof value === 'string' ? value : JSON.stringify(value, null, 2); try { writeFileSync(fullPath, serialized, 'utf8'); } catch (err) { From 3499784ef1844923e088596d073f093021fc8b4b Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 01:35:22 +0000 Subject: [PATCH 15/32] style: auto-fix prettier formatting and duplicate imports CI lint was failing on 17 prettier issues + 1 duplicate import in business.router.ts. Auto-fixed via eslint --fix. Signed-off-by: Kelvin Yuli Andrian --- .../whatsapp/whatsapp.baileys.service.ts | 32 +++++++++---------- src/api/routes/business.router.ts | 3 +- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index b9b858f87..7709f5962 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -4900,7 +4900,7 @@ export class BaileysStartupService extends ChannelStartupService { } //Business Controller - public async fetchCatalog(instanceName: string, data: getCatalogDto) { + public async fetchCatalog(instanceName: string, data: getCatalogDto) { // === Provider routing: browser fallback for full catalog === if (data.provider === 'browser') { const jid = data.number ? createJid(data.number) : this.client?.user?.id; @@ -4916,16 +4916,16 @@ export class BaileysStartupService extends ChannelStartupService { const limit = Number(data.limit) || 50; // Tetap hormati cursor dari caller (untuk resume manual jika diperlukan) let cursor = data.cursor || null; - + const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - + if (!onWhatsapp.exists) { throw new BadRequestException(onWhatsapp); } - + try { const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - + let isBusiness = false; try { const business = await this.fetchBusinessProfile(info?.jid); @@ -4933,54 +4933,54 @@ export class BaileysStartupService extends ChannelStartupService { } catch (profileError) { console.log('fetchBusinessProfile failed, continuing catalog fetch:', profileError?.message); } - + let productsCatalog: Product[] = []; let fetcherHasMore = true; let countLoops = 0; const MAX_LOOPS = 200; // ~10.000 produk @50/halaman — cukup besar untuk hampir semua kasus nyata let truncated = false; - + while (fetcherHasMore) { const catalog = await this.getCatalog({ jid: info?.jid, limit, cursor }); - + productsCatalog = [...productsCatalog, ...(catalog.products || [])]; - + const nextPageCursor = catalog.nextPageCursor; const nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null; const pagination = nextPageCursorJson?.pagination_cursor ? JSON.parse(atob(nextPageCursorJson.pagination_cursor)) : null; - + fetcherHasMore = pagination?.fetcher_has_more === true; cursor = nextPageCursor; countLoops++; - + // Safety valve: hindari infinite loop kalau WhatsApp API berkelakuan aneh if (countLoops >= MAX_LOOPS) { truncated = fetcherHasMore; // hanya benar2 "truncated" kalau memang masih ada sisa this.logger.warn( `fetchCatalog: mencapai MAX_LOOPS (${MAX_LOOPS}) untuk ${info?.jid}, ` + - `hasil mungkin belum lengkap. Gunakan 'cursor' di response untuk lanjut.`, + `hasil mungkin belum lengkap. Gunakan 'cursor' di response untuk lanjut.`, ); break; } } - + return { wuid: info?.jid || jid, numberExists: info?.exists, isBusiness, catalogLength: productsCatalog.length, catalog: productsCatalog, - truncated, // <-- penanda eksplisit ke caller - nextCursor: truncated ? cursor : null, // <-- supaya caller bisa lanjut manual + truncated, // <-- penanda eksplisit ke caller + nextCursor: truncated ? cursor : null, // <-- supaya caller bisa lanjut manual }; } catch (error) { console.log(error); return { wuid: jid, name: null, isBusiness: false, catalog: [], truncated: true }; } } - + public async getCatalog({ jid, limit, diff --git a/src/api/routes/business.router.ts b/src/api/routes/business.router.ts index 2670c2807..867a0f124 100644 --- a/src/api/routes/business.router.ts +++ b/src/api/routes/business.router.ts @@ -1,6 +1,5 @@ import { RouterBroker } from '@api/abstract/abstract.router'; -import { getCatalogDto } from '@api/dto/business.dto'; -import { getCollectionsDto } from '@api/dto/business.dto'; +import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto'; import { businessController } from '@api/server.module'; import { createMetaErrorResponse } from '@utils/errorResponse'; import { catalogSchema, collectionsSchema } from '@validate/validate.schema'; From 6057376b22393fab1e319b3ef566107f19c392ca Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 01:49:12 +0000 Subject: [PATCH 16/32] ci: remove Docker Hub publish workflows, use GHCR only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docker Hub builds were failing due to missing DOCKER_USERNAME/DOCKER_PASSWORD secrets. GHCR (ghcr.io/kelvinzer0/evolution-api) is the canonical registry going forward — uses built-in GITHUB_TOKEN, no external secrets needed. Removed: - publish_docker_image.yml (tag-based Docker Hub) - publish_docker_image_homolog.yml (develop branch Docker Hub) - publish_docker_image_latest.yml (main branch Docker Hub — was failing) Remaining workflows: - publish_ghcr.yml (main + tags + PRs) - check_code_quality.yml - security.yml Signed-off-by: Kelvin Yuli Andrian --- .github/workflows/publish_docker_image.yml | 50 ------------------- .../publish_docker_image_homolog.yml | 50 ------------------- .../workflows/publish_docker_image_latest.yml | 50 ------------------- 3 files changed, 150 deletions(-) delete mode 100644 .github/workflows/publish_docker_image.yml delete mode 100644 .github/workflows/publish_docker_image_homolog.yml delete mode 100644 .github/workflows/publish_docker_image_latest.yml diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml deleted file mode 100644 index c5a3996ed..000000000 --- a/.github/workflows/publish_docker_image.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build Docker image - -on: - push: - tags: - - "*.*.*" - -jobs: - build_deploy: - name: Build and Deploy - 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: type=semver,pattern=v{{version}} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - 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 }} \ No newline at end of file diff --git a/.github/workflows/publish_docker_image_homolog.yml b/.github/workflows/publish_docker_image_homolog.yml deleted file mode 100644 index a76e1008b..000000000 --- a/.github/workflows/publish_docker_image_homolog.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build Docker image - -on: - push: - branches: - - develop - -jobs: - build_deploy: - name: Build and Deploy - 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 }} diff --git a/.github/workflows/publish_docker_image_latest.yml b/.github/workflows/publish_docker_image_latest.yml deleted file mode 100644 index f73fe80e4..000000000 --- a/.github/workflows/publish_docker_image_latest.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build Docker image - -on: - push: - branches: - - main - -jobs: - build_deploy: - name: Build and Deploy - 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 }} From b0cd83ba4019565da1d17d2e77598853dad18a3c Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 07:12:44 +0000 Subject: [PATCH 17/32] fix(catalog-browser): remove --single-process causing Chromium crash Chromium was crashing on launch with 'Protocol error (Target.setAutoAttach): Target closed' because --single-process mode is unstable in headless containers (V8 proxy resolver fails, GPU init crashes). Removed: - --single-process (causes V8 proxy resolver failure) - --no-zygote (only needed with --single-process) Added stability flags: - --disable-software-rasterizer - --disable-features=VizDisplayCompositor,Vulkan - --disable-vulkan (no GPU in container, was spamming errors) - --no-first-run, --no-default-browser-check - --disable-extensions, --disable-background-networking - --disable-sync, --disable-translate, --disable-default-apps - --disable-component-update Tested on Alpine Linux container with Chromium 150. Signed-off-by: Kelvin Yuli Andrian --- .../channel/whatsapp/catalog-browser.service.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index 50c7a9a89..46eabfa28 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -339,9 +339,18 @@ export class BrowserCatalogService { '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', - '--single-process', + '--disable-software-rasterizer', + '--disable-features=VizDisplayCompositor,Vulkan', + '--disable-vulkan', '--memory-pressure-off', - '--no-zygote', + '--no-first-run', + '--no-default-browser-check', + '--disable-extensions', + '--disable-background-networking', + '--disable-sync', + '--disable-translate', + '--disable-default-apps', + '--disable-component-update', ], }; } From 743aebab4a87de54322f6f2052bbd99ab6751950 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 07:31:03 +0000 Subject: [PATCH 18/32] fix(catalog-browser): clean stale Chromium locks before launch Previous failed launches left SingletonLock files + orphan chromium processes in userDataDir, blocking new launches with 'profile appears to be in use by another Chromium process' error. Added: - cleanStaleLocks() method called before every browser launch: - Removes SingletonLock, SingletonCookie, SingletonSocket files - Kills orphan chromium processes via pkill - browser.on('disconnected') handler to clean up internal state - protocolTimeout: 60000ms for slow container startup Signed-off-by: Kelvin Yuli Andrian --- .../whatsapp/catalog-browser.service.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index 46eabfa28..4473ab510 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -24,6 +24,10 @@ * (Kelvin Yuli Andrian's own implementation, which proves this works) */ +import { existsSync, unlinkSync } from 'fs'; +import { join } from 'path'; +import { execSync } from 'child_process'; + import { Logger } from '@config/logger.config'; import { BadRequestException } from '@exceptions'; import puppeteer, { Browser, Page } from 'puppeteer-core'; @@ -520,6 +524,11 @@ export class BrowserCatalogService { const userDataDir = this.sessionStore.userDataDir(instanceName); this.logger.log(`[browser] Launching Chromium for instance=${instanceName} jid=${jid}`); + // Clean up stale SingletonLock file from previous crashed launches. + // Chromium creates this lock file to prevent concurrent profile access, + // but if a previous process crashed, the lock stays and blocks new launches. + this.cleanStaleLocks(userDataDir); + const browser = await puppeteer.launch({ executablePath: this.config.executablePath, headless: this.config.headless, @@ -527,10 +536,24 @@ export class BrowserCatalogService { args: this.config.extraArgs, defaultViewport: { width: 1280, height: 800 }, ignoreDefaultArgs: ['--enable-automation'], + // Wait for initial page to be ready before returning + protocolTimeout: 60000, }); this.browsers.set(jid, browser); + // Handle unexpected browser disconnect — clean up so next call can re-launch + browser.on('disconnected', () => { + this.logger.warn(`[browser] Browser disconnected for jid=${jid}, cleaning up`); + this.browsers.delete(jid); + const timer = this.idleTimers.get(jid); + if (timer) { + clearTimeout(timer); + this.idleTimers.delete(jid); + } + this.pendingQr.delete(jid); + }); + // Navigate to WA Web on the first page const page = await browser.newPage(); await page.goto('https://web.whatsapp.com/', { @@ -602,6 +625,40 @@ export class BrowserCatalogService { this.idleTimers.set(jid, timer); } + /** + * Remove stale Chromium lock files and kill orphan Chromium processes + * left over from previous crashed launches. + * + * Chromium creates SingletonLock, SingletonCookie, and SingletonSocket + * symlinks in the userDataDir. If a previous process crashed, these + * locks persist and block new launches with "profile appears to be in + * use by another Chromium process" error. + */ + private cleanStaleLocks(userDataDir: string): void { + // 1. Remove lock files/symlinks + const lockFiles = ['SingletonLock', 'SingletonCookie', 'SingletonSocket']; + for (const lockFile of lockFiles) { + const lockPath = join(userDataDir, lockFile); + if (existsSync(lockPath)) { + try { + unlinkSync(lockPath); + this.logger.log(`[browser] Removed stale lock: ${lockFile}`); + } catch (err) { + this.logger.warn(`[browser] Failed to remove ${lockFile}: ${(err as Error).message}`); + } + } + } + + // 2. Kill orphan chromium processes (best-effort, ignore errors) + // This handles the case where a previous Puppeteer crash left + // chromium processes running and holding the profile. + try { + execSync('pkill -f chromium 2>/dev/null || true', { timeout: 5000 }); + } catch { + // pkill exit code 1 = no process matched, ignore + } + } + /** * Kill the browser for a JID and clean up timers. */ From 765aca236e914a87c6f30a9ec51bc37882fca1cb Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 07:41:28 +0000 Subject: [PATCH 19/32] fix(catalog-browser): inject @wppconnect/wa-js for window.WPP API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without wa-js injection, window.WPP is undefined in the Puppeteer page context, causing all catalog fetch logic to fail silently (returns empty catalog, no QR code for auth). Root cause: whatsapp-web.js bundles wa-js internally, but since we use puppeteer-core directly, we need to inject it ourselves. Changes: - Added @wppconnect/wa-js@^3.22.1 to dependencies - New injectWaJs(page) method: - Reads wa-js from node_modules via require.resolve - Injects via page.evaluate(waJsCode) - Waits for WPP.isReady === true (timeout 30s) - Updated launchBrowser(): - Wait for networkidle2 (was domcontentloaded) — WA Web needs full load - Set proper User-Agent - Call injectWaJs() after page.goto - Added @wppconnect/wa-js to Dockerfile npm install Tested in-container: wa-js injects successfully, WPP.isReady = true, QR code extracted via canvas (9090 chars data URL), auth check works. Signed-off-by: Kelvin Yuli Andrian --- package.json | 5 +- .../whatsapp/catalog-browser.service.ts | 55 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index fde0096ce..43b1d2d31 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,8 @@ "swagger-ui-express": "^5.0.1", "tsup": "^8.3.5", "undici": "^7.16.0", - "uuid": "^13.0.0" + "uuid": "^13.0.0", + "@wppconnect/wa-js": "^3.22.1" }, "devDependencies": { "@commitlint/cli": "^19.8.1", @@ -158,4 +159,4 @@ "tsx": "^4.20.5", "typescript": "^5.7.2" } -} +} \ No newline at end of file diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index 4473ab510..3b5371990 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -556,17 +556,64 @@ export class BrowserCatalogService { // Navigate to WA Web on the first page const page = await browser.newPage(); + await page.setUserAgent( + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + ); await page.goto('https://web.whatsapp.com/', { - waitUntil: 'domcontentloaded', - timeout: 60000, + waitUntil: 'networkidle2', + timeout: 90000, }); - // Give WA Web time to initialize - await new Promise((r) => setTimeout(r, 5000)); + // Inject @wppconnect/wa-js library — required to access window.WPP API + // (whatsapp-web.js bundles this, but since we use puppeteer-core directly, + // we need to inject it ourselves) + await this.injectWaJs(page); return browser; } + /** + * Inject @wppconnect/wa-js into the page and wait for WPP.isReady. + * This library provides the window.WPP API we use for catalog fetching. + */ + private async injectWaJs(page: Page): Promise { + const wppExists = await page.evaluate(() => typeof (window as any).WPP !== 'undefined'); + if (wppExists) { + this.logger.log('[browser] WPP already loaded in page'); + return; + } + + this.logger.log('[browser] Injecting @wppconnect/wa-js into page...'); + + // Read wa-js from node_modules and inject via page.evaluate + try { + const waJsPath = require.resolve('@wppconnect/wa-js'); + const fs = await import('fs'); + const waJsCode = fs.readFileSync(waJsPath, 'utf8'); + await page.evaluate(waJsCode); + this.logger.log(`[browser] wa-js injected (${waJsCode.length} chars)`); + } catch (err) { + this.logger.error(`[browser] Failed to inject wa-js: ${(err as Error).message}`); + throw new BadRequestException( + 'Failed to inject @wppconnect/wa-js. Make sure it is installed: npm install @wppconnect/wa-js', + ); + } + + // Wait for WPP to be ready + try { + await page.waitForFunction( + () => (window as any).WPP && (window as any).WPP.isReady === true, + { timeout: 30000 }, + ); + this.logger.log('[browser] WPP.isReady = true'); + } catch (err) { + this.logger.warn('[browser] WPP.isReady timeout — continuing anyway'); + } + + // Give WA Web + WPP a few more seconds to stabilize + await new Promise((r) => setTimeout(r, 5000)); + } + /** * Ensure a QR code is available for the user to scan. * Returns the QR data URL if authentication is required. From d722812acc855b7b60058c2b9bf4e64fad5c10a9 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 09:02:48 +0000 Subject: [PATCH 20/32] refactor(catalog-browser): use whatsapp-web.js (proper implementation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced puppeteer-core + manual wa-js injection with whatsapp-web.js — the same library bedones-whatsapp uses, proven working in production. Why whatsapp-web.js: - Bundles puppeteer with optimal config (no manual launch args) - LocalAuth strategy handles session persistence - Auto-injects @wppconnect/wa-js internally - Event-driven: 'qr', 'authenticated', 'ready', 'disconnected' - Native requestPairingCode(phone) — no DOM scraping Changes: - Replaced puppeteer-core with whatsapp-web.js@^1.34.7 - Rewrote catalog-browser.service.ts using Client + LocalAuth - Removed session-store.browser.ts (LocalAuth handles persistence) - Added requestPairingCode(phone) and getAuthState() methods - Event-driven: getReadyClient() awaits 'ready' event - QR/pairingCode in response when not authenticated Signed-off-by: Kelvin Yuli Andrian --- package-lock.json | 1117 +++++++++++++++-- package.json | 6 +- .../whatsapp/catalog-browser.service.ts | 1055 ++++++++-------- .../channel/whatsapp/session-store.browser.ts | 115 -- src/api/server.module.ts | 4 +- 5 files changed, 1510 insertions(+), 787 deletions(-) delete mode 100644 src/api/integrations/channel/whatsapp/session-store.browser.ts diff --git a/package-lock.json b/package-lock.json index 05b29dc5f..09733e6ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@prisma/client": "^6.16.2", "@sentry/node": "^10.12.0", "@types/uuid": "^10.0.0", + "@wppconnect/wa-js": "^3.22.1", "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", @@ -56,7 +57,6 @@ "pg": "^8.13.1", "pino": "^9.10.0", "prisma": "^6.1.0", - "puppeteer-core": "^23.11.1", "pusher": "^5.2.0", "qrcode": "^1.5.4", "qrcode-terminal": "^0.12.0", @@ -69,7 +69,8 @@ "swagger-ui-express": "^5.0.1", "tsup": "^8.3.5", "undici": "^7.16.0", - "uuid": "^13.0.0" + "uuid": "^13.0.0", + "whatsapp-web.js": "^1.34.7" }, "devDependencies": { "@commitlint/cli": "^19.8.1", @@ -852,7 +853,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -867,7 +867,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3493,6 +3492,16 @@ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -3686,28 +3695,6 @@ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, - "node_modules/@puppeteer/browsers": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", - "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.4.0", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.6.3", - "tar-fs": "^3.0.6", - "unbzip2-stream": "^1.4.3", - "yargs": "^17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -5426,6 +5413,15 @@ "url": "https://github.com/sponsors/eshaz" } }, + "node_modules/@wppconnect/wa-js": { + "version": "3.23.4", + "resolved": "https://registry.npmjs.org/@wppconnect/wa-js/-/wa-js-3.23.4.tgz", + "integrity": "sha512-YAVMxz7n3DXqMq469OQSEKW5Nfjb9/ai1vcrlIweJb0RUgkfiTYJT9os+bcZ6QP8lmlNX8wLBJM/04hgtxZJEA==", + "license": "Apache-2.0", + "engines": { + "whatsapp-web": ">=2.2326.10-beta" + } + }, "node_modules/@zxing/text-encoding": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", @@ -5632,11 +5628,291 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/archiver-utils/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/archiver-utils/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", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT", + "optional": true + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "optional": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/archiver-utils/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC", + "optional": true + }, + "node_modules/archiver-utils/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/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", + "optional": true, + "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/archiver-utils/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/archiver-utils/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/archiver-utils/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/archiver/node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT", + "optional": true + }, + "node_modules/archiver/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", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/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", + "optional": true, + "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/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { @@ -6060,6 +6336,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "devOptional": true, "funding": [ { "type": "github", @@ -6115,6 +6392,13 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT", + "optional": true + }, "node_modules/bmp-ts": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz", @@ -6204,6 +6488,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "funding": [ { "type": "github", @@ -6404,7 +6689,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6493,28 +6777,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/chromium-bidi": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", - "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", - "license": "Apache-2.0", - "dependencies": { - "mitt": "3.0.1", - "zod": "3.23.8" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/chromium-bidi/node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -6776,30 +7038,89 @@ "dot-prop": "^5.1.0" } }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", "license": "MIT", + "optional": true, "dependencies": { - "mime-db": ">= 1.43.0 < 2" + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 14" } }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", + "node_modules/compress-commons/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", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/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", + "optional": true, + "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/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { @@ -6946,6 +7267,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT", + "optional": true + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -6963,7 +7291,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.1", @@ -7004,11 +7331,80 @@ "typescript": ">=5" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", + "optional": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-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", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-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", + "optional": true, + "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/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -7393,12 +7789,6 @@ "node": ">=8" } }, - "node_modules/devtools-protocol": { - "version": "0.0.1367902", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", - "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", - "license": "BSD-3-Clause" - }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -7512,6 +7902,63 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT", + "optional": true + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "optional": true + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT", + "optional": true + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -7713,7 +8160,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7736,7 +8182,6 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -8635,6 +9080,16 @@ "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", + "optional": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/events-universal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", @@ -9195,7 +9650,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -9212,7 +9667,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=14" @@ -9714,7 +10169,7 @@ "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, + "devOptional": true, "license": "ISC" }, "node_modules/graphemer": { @@ -10015,7 +10470,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -10032,7 +10486,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -10273,7 +10726,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -10605,6 +11057,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -10839,14 +11304,12 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "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==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10866,7 +11329,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, "license": "MIT" }, "node_modules/json-schema": { @@ -10906,7 +11368,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -11012,6 +11474,59 @@ "@keyv/serialize": "^1.1.1" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "optional": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT", + "optional": true + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "optional": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -11964,7 +12479,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, + "devOptional": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -12263,6 +12778,13 @@ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "license": "MIT" }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT", + "optional": true + }, "node_modules/node-wav": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/node-wav/-/node-wav-0.0.2.tgz", @@ -12272,6 +12794,22 @@ "node": ">=4.4.0" } }, + "node_modules/node-webpmux": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-webpmux/-/node-webpmux-3.2.1.tgz", + "integrity": "sha512-MKgpq9nFgo44pIVNx/umD3nkqb2E8oqQTfmstVsfNdx9uV4cX7a4LqA+d8AZd3v5tgJXwENKUFsXNP3bRLP8nQ==", + "license": "LGPL-3.0-or-later" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -12808,7 +13346,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, + "devOptional": true, "license": "BlueOak-1.0.0" }, "node_modules/pako": { @@ -12821,7 +13359,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -12869,7 +13406,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -12964,7 +13500,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -13379,6 +13915,23 @@ } } }, + "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", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT", + "optional": true + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -13499,18 +14052,80 @@ "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", "license": "MIT" }, - "node_modules/puppeteer-core": { - "version": "23.11.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", - "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", + "node_modules/puppeteer": { + "version": "24.38.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.38.0.tgz", + "integrity": "sha512-abnJOBVoL9PQTLKSbYGm9mjNFyIPaTVj77J/6cS370dIQtcZMpx8wyZoAuBzR71Aoon6yvI71NEVFUsl3JU82g==", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.6.1", - "chromium-bidi": "0.11.0", - "debug": "^4.4.0", - "devtools-protocol": "0.0.1367902", - "typed-query-selector": "^2.12.0", - "ws": "^8.18.0" + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1581282", + "puppeteer-core": "24.38.0", + "typed-query-selector": "^2.12.1" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer/node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer/node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/puppeteer/node_modules/devtools-protocol": { + "version": "0.0.1581282", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", + "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", + "license": "BSD-3-Clause" + }, + "node_modules/puppeteer/node_modules/puppeteer-core": { + "version": "24.38.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.38.0.tgz", + "integrity": "sha512-zB3S/tksIhgi2gZRndUe07AudBz5SXOB7hqG0kEa9/YXWrGwlVlYm3tZtwKgfRftBzbmLQl5iwHkQQl04n/mWw==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1581282", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" }, "engines": { "node": ">=18" @@ -13887,6 +14502,29 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -14297,9 +14935,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" @@ -14491,7 +15129,7 @@ "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, + "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -14504,7 +15142,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -14949,6 +15587,39 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -15035,6 +15706,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -15385,6 +16070,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, "license": "MIT" }, "node_modules/through2": { @@ -16311,16 +16997,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "license": "MIT", - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, "node_modules/undici": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", @@ -16353,7 +17029,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -16368,6 +17044,35 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", + "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "^11.2.0", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", + "license": "MIT", + "optional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -16507,6 +17212,12 @@ "node": ">= 14" } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -16519,6 +17230,54 @@ "integrity": "sha512-yYO1qSs0Fe7tGtnxOFHomocUD6IZtoAgmA4oDFyGIRZ67D3QZk3w7swA6XXFXNQngiyrg2k7tul6IrM3eUFh7A==", "license": "MIT" }, + "node_modules/whatsapp-web.js": { + "version": "1.34.7", + "resolved": "https://registry.npmjs.org/whatsapp-web.js/-/whatsapp-web.js-1.34.7.tgz", + "integrity": "sha512-CscRtB32OnozLj+cuG9Q5f7IhnNV2EU4RGRJYeYF7wwhN6acQ0efabnFpetSEh5Y8OL4YqBj7nSQbUrTZYLDGA==", + "license": "Apache-2.0", + "dependencies": { + "fluent-ffmpeg": "2.1.3", + "mime": "3.0.0", + "node-fetch": "2.7.0", + "node-webpmux": "3.2.1", + "puppeteer": "24.38.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "archiver": "7.0.1", + "fs-extra": "11.3.4", + "unzipper": "0.12.3" + } + }, + "node_modules/whatsapp-web.js/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "license": "MIT", + "optional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/whatsapp-web.js/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -16533,7 +17292,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -16672,6 +17431,61 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "optional": true + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -16712,9 +17526,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.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -16864,6 +17678,63 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", + "optional": true, + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-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", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-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", + "optional": true, + "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/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 43b1d2d31..30b74aed7 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@prisma/client": "^6.16.2", "@sentry/node": "^10.12.0", "@types/uuid": "^10.0.0", + "@wppconnect/wa-js": "^3.22.1", "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", @@ -112,7 +113,6 @@ "openai": "^4.77.3", "pg": "^8.13.1", "pino": "^9.10.0", - "puppeteer-core": "^23.11.1", "prisma": "^6.1.0", "pusher": "^5.2.0", "qrcode": "^1.5.4", @@ -127,7 +127,7 @@ "tsup": "^8.3.5", "undici": "^7.16.0", "uuid": "^13.0.0", - "@wppconnect/wa-js": "^3.22.1" + "whatsapp-web.js": "^1.34.7" }, "devDependencies": { "@commitlint/cli": "^19.8.1", @@ -159,4 +159,4 @@ "tsx": "^4.20.5", "typescript": "^5.7.2" } -} \ No newline at end of file +} diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index 3b5371990..47b043b1c 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -1,36 +1,32 @@ /** * BrowserCatalogService * --------------------------------------------------------------------- - * Singleton service that lazily launches a Puppeteer browser per WhatsApp - * account (JID) to fetch catalog & collections via the internal - * `window.WPP.whatsapp.functions.queryCatalog` API of web.whatsapp.com. + * Singleton service that uses whatsapp-web.js to fetch catalog & collections + * via web.whatsapp.com, bypassing Baileys' protocol-level truncation. * * Why this exists: - * WhatsApp's anti-bot/anti-scraping on the protocol level (Baileys) - * is very strict and causes `getCatalog()` to truncate results. The - * same catalog fetched via web.whatsapp.com (browser automation) - * returns the full list because WhatsApp's own frontend code handles - * pagination correctly. + * WhatsApp's anti-bot/anti-scraping on the protocol level (Baileys) is + * very strict and causes `getCatalog()` to truncate results. The same + * catalog fetched via web.whatsapp.com (browser automation) returns the + * full list because WhatsApp's own frontend code handles pagination. * - * Design: - * - One Browser instance per JID (lazy start) - * - Browser is killed after IDLE_TIMEOUT_MS of inactivity - * - Session is persisted on disk per instance (BrowserSessionStore) - * - If no session exists, returns a QR code the caller must surface - * to the user for scanning + * Implementation: + * Uses whatsapp-web.js (same library as bedones-whatsapp, proven working). + * - LocalAuth strategy for session persistence per instance + * - Event-driven: 'qr', 'authenticated', 'ready', 'code_received' (pairing) + * - Catalog fetch uses window.WPP API (auto-injected by whatsapp-web.js) * * Ported logic from: * bedones-whatsapp/apps/whatsapp-connector/src/catalog/catalog.service.ts - * (Kelvin Yuli Andrian's own implementation, which proves this works) */ -import { existsSync, unlinkSync } from 'fs'; +import { existsSync, mkdirSync, rmSync, unlinkSync, writeFileSync } from 'fs'; import { join } from 'path'; -import { execSync } from 'child_process'; import { Logger } from '@config/logger.config'; +import { INSTANCE_DIR } from '@config/path.config'; import { BadRequestException } from '@exceptions'; -import puppeteer, { Browser, Page } from 'puppeteer-core'; +import { Client, LocalAuth } from 'whatsapp-web.js'; import { BrowserCatalogConfig, @@ -41,239 +37,31 @@ import { BrowserCollectionsResult, BrowserProduct, } from './catalog-browser.types'; -import { BrowserSessionStore } from './session-store.browser'; -// Return types for in-page scripts (kept as named interfaces to avoid -// TypeScript parser confusion with multi-line arrow type annotations) -interface InPageCatalogResult { - catalog: BrowserProduct[]; - message?: string; -} - -interface InPageCollectionsResult { - collections: BrowserCollection[]; - message?: string; -} - -interface InPageReadyResult { +// Per-instance client state +interface InstanceClientState { + client: Client; ready: boolean; - reason?: string; + readyPromise: Promise; + qrCode: string | null; + pairingCode: string | null; + lastActivity: number; + idleTimer?: NodeJS.Timeout; } -// JavaScript executed inside the browser page context — has access to -// window.WPP, window.Whatsapp, etc. Cannot reference any Node.js types. -// NOTE: must be self-contained, no closures over outer variables. -const FETCH_CATALOG_IN_PAGE = async (): Promise => { - // Type-loose since we're running in the browser context - const wpp = (window as any).WPP; - if (!wpp) { - return { catalog: [], message: 'WPP not available — page did not load WhatsApp Web' }; - } - - const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; - const userId = (myUser && myUser._serialized) || ''; - if (!userId) { - return { catalog: [], message: 'User ID not found — not logged in' }; - } - - const whatsappApi = wpp.whatsapp as any; - const productsById = new Map(); +const SESSION_SUBDIR = 'browser-session'; - const addProduct = (rawProduct: any) => { - const product = rawProduct?.attributes || rawProduct; - if (!product?.id) return; - if (!productsById.has(product.id)) { - productsById.set(product.id, product as BrowserProduct); - } - }; - - const extractProductsFromCatalog = (catalogEntry: any): any[] => { - if (!catalogEntry) return []; - const productIndex = catalogEntry.productCollection?._index; - if (!productIndex || typeof productIndex !== 'object') return []; - return Object.keys(productIndex) - .map((productId) => productIndex[productId]?.attributes) - .filter(Boolean); - }; - - // Layer 1: queryCatalog with pagination cursor (most reliable) - if (whatsappApi?.functions?.queryCatalog) { - try { - let afterToken: string | undefined = undefined; - let safetyCount = 0; - while (safetyCount < 500) { - const response: any = await whatsappApi.functions.queryCatalog(userId, afterToken); - const pageProducts: any[] = Array.isArray(response?.data) ? response.data : []; - for (const product of pageProducts) { - addProduct(product); - } - const nextAfter = response?.paging?.cursors?.after; - if (!nextAfter || nextAfter === afterToken) break; - afterToken = nextAfter; - safetyCount++; - } - } catch (error: any) { - // queryCatalog unavailable on this WA version — fall through to next layer - console.log('queryCatalog unavailable:', error?.message); - } - } - - // Layer 2: CatalogStore.findQuery (direct store access) - if (whatsappApi?.CatalogStore?.findQuery) { - try { - const results: any[] = await whatsappApi.CatalogStore.findQuery(userId); - if (Array.isArray(results)) { - for (const entry of results) { - const products = extractProductsFromCatalog(entry); - for (const product of products) { - addProduct(product); - } - } - } - } catch (error: any) { - console.log('CatalogStore.findQuery unavailable:', error?.message); - } - } - - // Layer 3: WPP.catalog.getMyCatalog (fallback) - try { - const myCatalog: any = await wpp.catalog?.getMyCatalog?.(); - const fallbackProducts = extractProductsFromCatalog(myCatalog); - for (const product of fallbackProducts) { - addProduct(product); - } - } catch (error: any) { - console.log('getMyCatalog unavailable:', error?.message); - } - - // Layer 4: last resort — getProducts with hardcoded cap - if (productsById.size === 0) { - try { - const fallbackProducts: any[] = await wpp.catalog?.getProducts?.(userId, 999); - if (Array.isArray(fallbackProducts)) { - for (const product of fallbackProducts) { - addProduct(product); - } - } - } catch (error: any) { - console.log('getProducts unavailable:', error?.message); - } - } - - return { catalog: Array.from(productsById.values()) }; -}; - -// In-page script: fetch all collections -const FETCH_COLLECTIONS_IN_PAGE = async (): Promise => { - const wpp = (window as any).WPP; - if (!wpp) { - return { collections: [], message: 'WPP not available' }; - } - - const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; - const userId = (myUser && myUser._serialized) || ''; - if (!userId) { - return { collections: [], message: 'User ID not found' }; - } - - const whatsappApi = wpp.whatsapp as any; - const collections: BrowserCollection[] = []; - - // Method 1: WPP.catalog.getCollections (preferred) - try { - const result: any = await wpp.catalog?.getCollections?.(userId); - if (Array.isArray(result)) { - for (const c of result) { - const attrs = c?.attributes || c; - if (!attrs?.id) continue; - // Extract products from collection - const productIndex = c?.productCollection?._index || attrs?.products?._index; - const products: BrowserProduct[] = []; - if (productIndex && typeof productIndex === 'object') { - for (const pid of Object.keys(productIndex)) { - const p = productIndex[pid]?.attributes || productIndex[pid]; - if (p) products.push(p as BrowserProduct); - } - } - collections.push({ - id: attrs.id, - name: attrs.name || '', - products, - status: attrs.status, - }); - } - } - } catch (error: any) { - console.log('WPP.catalog.getCollections failed:', error?.message); - } - - // Method 2: CatalogStore fallback (direct store access) - if (collections.length === 0 && whatsappApi?.CollectionStore?.findQuery) { - try { - const results: any[] = await whatsappApi.CollectionStore.findQuery(userId); - if (Array.isArray(results)) { - for (const c of results) { - const attrs = c?.attributes || c; - if (!attrs?.id) continue; - const productIndex = c?.productCollection?._index; - const products: BrowserProduct[] = []; - if (productIndex && typeof productIndex === 'object') { - for (const pid of Object.keys(productIndex)) { - const p = productIndex[pid]?.attributes || productIndex[pid]; - if (p) products.push(p as BrowserProduct); - } - } - collections.push({ - id: attrs.id, - name: attrs.name || '', - products, - status: attrs.status, - }); - } - } - } catch (error: any) { - console.log('CollectionStore.findQuery failed:', error?.message); - } - } - - return { collections }; -}; - -// In-page script: check if WA Web is ready -const IS_WA_READY_IN_PAGE = async (): Promise => { - const wpp = (window as any).WPP; - if (!wpp) return { ready: false, reason: 'WPP not loaded' }; - if (!wpp.isReady) { - try { - if (wpp.waitForReady) await wpp.waitForReady({ timeout: 30000 }); - } catch { - return { ready: false, reason: 'WPP.waitForReady timed out' }; - } - } - const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; - const userId = myUser ? myUser._serialized : null; - if (!userId) return { ready: false, reason: 'No user logged in (need QR scan)' }; - return { ready: true }; -}; +// (No NestJS — Evolution API uses plain classes. The @Injectable decorator +// below is a no-op kept only for documentation purposes; remove if it causes +// issues. This class is constructed manually in server.module.ts.) export class BrowserCatalogService { private readonly logger = new Logger(BrowserCatalogService.name); private readonly config: BrowserCatalogConfig; - // Per-JID browser instance - private readonly browsers = new Map(); - // Per-JID idle timer - private readonly idleTimers = new Map(); - // Per-JID QR code (when auth pending) - private readonly pendingQr = new Map(); + // Per-instance state map (key = instance name) + private readonly clients = new Map(); - /** - * Service locator — set by server.module.ts at bootstrap time so that - * the BaileysStartupService (which is NOT NestJS-managed) can access - * the singleton instance via `BrowserCatalogService.getInstance()`. - * - * Returns null if not initialized (e.g. when CATALOG_BROWSER_ENABLED=false). - */ private static instance: BrowserCatalogService | null = null; static getInstance(): BrowserCatalogService | null { @@ -284,7 +72,7 @@ export class BrowserCatalogService { BrowserCatalogService.instance = svc; } - constructor(private readonly sessionStore: BrowserSessionStore) { + constructor() { this.config = this.loadConfig(); if (this.config.enabled) { this.logger.log( @@ -294,10 +82,9 @@ export class BrowserCatalogService { BrowserCatalogService.setInstance(this); } - /** - * Convenience static method: fetch catalog via browser, or throw if disabled. - */ - static async fetchCatalogOrThrow(options: BrowserCatalogOptions): Promise { + static async fetchCatalogOrThrow( + options: BrowserCatalogOptions, + ): Promise { const svc = BrowserCatalogService.getInstance(); if (!svc) { throw new BadRequestException( @@ -307,10 +94,9 @@ export class BrowserCatalogService { return svc.fetchCatalog(options); } - /** - * Convenience static method: fetch collections via browser. - */ - static async fetchCollectionsOrThrow(options: BrowserCollectionsOptions): Promise { + static async fetchCollectionsOrThrow( + options: BrowserCollectionsOptions, + ): Promise { const svc = BrowserCatalogService.getInstance(); if (!svc) { throw new BadRequestException( @@ -320,17 +106,17 @@ export class BrowserCatalogService { return svc.fetchCollections(options); } - /** - * Load configuration from env vars (with sane defaults). - */ private loadConfig(): BrowserCatalogConfig { const enabled = (process.env.CATALOG_BROWSER_ENABLED || 'false').toLowerCase() === 'true'; const idleTimeoutMs = parseInt(process.env.CATALOG_BROWSER_IDLE_TIMEOUT_MS || '600000', 10); const maxSessions = parseInt(process.env.CATALOG_BROWSER_MAX_SESSIONS || '5', 10); const headlessEnv = (process.env.CATALOG_BROWSER_HEADLESS || 'true').toLowerCase(); - const headless: boolean | 'shell' = headlessEnv === 'shell' ? 'shell' : headlessEnv === 'false' ? false : true; + const headless: boolean | 'shell' = + headlessEnv === 'shell' ? 'shell' : headlessEnv === 'false' ? false : true; const executablePath = - process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROMIUM_PATH || '/usr/bin/chromium-browser'; + process.env.PUPPETEER_EXECUTABLE_PATH || + process.env.CHROMIUM_PATH || + '/usr/bin/chromium-browser'; return { enabled, @@ -355,375 +141,558 @@ export class BrowserCatalogService { '--disable-translate', '--disable-default-apps', '--disable-component-update', + '--disable-blink-features=AutomationControlled', ], }; } /** - * Public entry: fetch catalog via browser. - * If session is not authenticated, returns a result with qrCode. + * Get the user-data directory for an instance's WhatsApp Web session. */ - async fetchCatalog(options: BrowserCatalogOptions): Promise { - if (!this.config.enabled) { - throw new BadRequestException('Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.'); - } - - const { jid, instanceName } = options; - this.logger.log(`[browser] fetchCatalog jid=${jid} instance=${instanceName}`); - - const page = await this.getPage(jid, instanceName); + private userDataDir(instanceName: string): string { + return join(INSTANCE_DIR, instanceName, SESSION_SUBDIR); + } - try { - // Wait for WA Web to be ready - const ready = await page.evaluate(IS_WA_READY_IN_PAGE); - if (!ready.ready) { - const qrCode = await this.ensureQrCode(jid, instanceName, page); - return { - wuid: jid, - numberExists: true, - isBusiness: true, - catalogLength: 0, - catalog: [], - truncated: false, - nextCursor: null, - source: 'browser', - // Include QR code via cast — caller knows to check for it - ...(qrCode ? ({ qrCode } as any) : {}), - }; + /** + * Clean stale Chromium lock files left by previous crashed sessions. + */ + private cleanStaleLocks(dir: string): void { + for (const lockFile of ['SingletonLock', 'SingletonCookie', 'SingletonSocket']) { + const p = join(dir, lockFile); + if (existsSync(p)) { + try { + unlinkSync(p); + this.logger.log(`[browser] Removed stale lock: ${lockFile}`); + } catch (err) { + this.logger.warn(`[browser] Failed to remove ${lockFile}: ${(err as Error).message}`); + } } + } + } - // Clear any pending QR (now authenticated) - this.pendingQr.delete(jid); - - // Run the 4-layer fetch inside the browser - const result = await page.evaluate(FETCH_CATALOG_IN_PAGE); + /** + * Get or create a Client for the given instance. + * Returns once the client is fully ready (authenticated + WA Web loaded). + */ + private async getReadyClient(instanceName: string): Promise { + let state = this.clients.get(instanceName); + if (state) { + // Reset idle timer + this.resetIdleTimer(instanceName); + await state.readyPromise; + return state; + } - if (result.message) { - this.logger.warn(`[browser] fetchCatalog message: ${result.message}`); + // Evict oldest if at capacity + if (this.clients.size >= this.config.maxSessions) { + const oldest = Array.from(this.clients.entries()).sort( + (a, b) => a[1].lastActivity - b[1].lastActivity, + )[0]; + if (oldest) { + this.logger.warn(`[browser] Max sessions reached, evicting: ${oldest[0]}`); + await this.killClient(oldest[0]); } - - this.logger.log(`[browser] fetchCatalog got ${result.catalog.length} products`); - - return { - wuid: jid, - numberExists: true, - isBusiness: true, - catalogLength: result.catalog.length, - catalog: result.catalog, - truncated: false, - nextCursor: null, - source: 'browser', - }; - } finally { - await page.close().catch(() => {}); - this.resetIdleTimer(jid); } + + state = this.launchClient(instanceName); + this.clients.set(instanceName, state); + this.resetIdleTimer(instanceName); + await state.readyPromise; + return state; } /** - * Public entry: fetch collections via browser. + * Launch a new whatsapp-web.js Client for the instance. + * Sets up event listeners for qr / authenticated / ready / disconnected. */ - async fetchCollections(options: BrowserCollectionsOptions): Promise { - if (!this.config.enabled) { - throw new BadRequestException('Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.'); - } + private launchClient(instanceName: string): InstanceClientState { + const userDataDir = this.userDataDir(instanceName); + mkdirSync(userDataDir, { recursive: true }); + this.cleanStaleLocks(userDataDir); - const { jid, instanceName } = options; - this.logger.log(`[browser] fetchCollections jid=${jid} instance=${instanceName}`); + this.logger.log(`[browser] Launching Client for instance=${instanceName}`); - const page = await this.getPage(jid, instanceName); + const state: InstanceClientState = { + client: null as any, + ready: false, + readyPromise: null as any, + qrCode: null, + pairingCode: null, + lastActivity: Date.now(), + }; - try { - const ready = await page.evaluate(IS_WA_READY_IN_PAGE); - if (!ready.ready) { - const qrCode = await this.ensureQrCode(jid, instanceName, page); - return { - wuid: jid, - name: null, - numberExists: true, - isBusiness: true, - collectionsLength: 0, - collections: [], - source: 'browser', - ...(qrCode ? ({ qrCode } as any) : {}), - }; - } + const client = new Client({ + authStrategy: new LocalAuth({ + clientId: instanceName, + dataPath: userDataDir, + }), + puppeteer: { + executablePath: this.config.executablePath, + headless: this.config.headless, + args: this.config.extraArgs, + // @ts-expect-error - bypassCSP is supported by whatsapp-web.js puppeteer config but not in puppeteer types + bypassCSP: true, + }, + }); + state.client = client; - this.pendingQr.delete(jid); - const result = await page.evaluate(FETCH_COLLECTIONS_IN_PAGE); + // Set up event listeners + client.on('qr', (qr: string) => { + this.logger.log(`[browser] QR received for instance=${instanceName}`); + state.qrCode = qr; + state.pairingCode = null; + }); - if (result.message) { - this.logger.warn(`[browser] fetchCollections message: ${result.message}`); - } + client.on('authenticated', () => { + this.logger.log(`[browser] Authenticated for instance=${instanceName}`); + state.qrCode = null; + state.pairingCode = null; + }); - this.logger.log(`[browser] fetchCollections got ${result.collections.length} collections`); + client.on('ready', () => { + this.logger.log(`[browser] Client ready for instance=${instanceName}`); + state.ready = true; + state.qrCode = null; + state.pairingCode = null; + }); - return { - wuid: jid, - name: null, - numberExists: true, - isBusiness: true, - collectionsLength: result.collections.length, - collections: result.collections, - source: 'browser', - }; - } finally { - await page.close().catch(() => {}); - this.resetIdleTimer(jid); - } - } + client.on('auth_failure', (msg: string) => { + this.logger.error(`[browser] Auth failure for instance=${instanceName}: ${msg}`); + }); - /** - * Logout: kill browser + delete session for an instance. - */ - async logout(instanceName: string, jid: string): Promise { - await this.killBrowser(jid); - this.sessionStore.deleteSession(instanceName); - this.pendingQr.delete(jid); - this.logger.log(`[browser] Logged out instance=${instanceName} jid=${jid}`); - } + client.on('disconnected', (reason: string) => { + this.logger.warn(`[browser] Disconnected for instance=${instanceName}: ${reason}`); + this.clients.delete(instanceName); + }); - /** - * Shutdown all browsers (for graceful app close). - */ - async shutdownAll(): Promise { - const jids = Array.from(this.browsers.keys()); - await Promise.all(jids.map((j) => this.killBrowser(j))); - this.logger.log(`[browser] Shutdown ${jids.length} browser(s)`); + // Create readyPromise that resolves when 'ready' event fires + state.readyPromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error(`Client initialization timed out after 120s for instance=${instanceName}`)); + }, 120000); + + client.once('ready', () => { + clearTimeout(timeout); + resolve(); + }); + client.once('auth_failure', (msg: string) => { + clearTimeout(timeout); + reject(new Error(`Auth failure: ${msg}`)); + }); + }); + + // Initialize (async, but don't await — readyPromise will resolve when ready) + client.initialize().catch((err: Error) => { + this.logger.error(`[browser] Initialize failed for instance=${instanceName}: ${err.message}`); + }); + + return state; } /** - * Get or launch a browser page for the given JID. + * Public entry: fetch catalog via browser. + * If session is not authenticated, returns qrCode in the result for the + * caller to surface to the user. */ - private async getPage(jid: string, instanceName: string): Promise { - let browser = this.browsers.get(jid); - if (!browser) { - browser = await this.launchBrowser(jid, instanceName); + async fetchCatalog(options: BrowserCatalogOptions): Promise { + if (!this.config.enabled) { + throw new BadRequestException( + 'Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.', + ); } - const page = await browser.newPage(); - await page.setUserAgent( - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - ); - return page; - } - /** - * Launch a new browser for the JID, navigating to WhatsApp Web and - * restoring session from disk if available. - */ - private async launchBrowser(jid: string, instanceName: string): Promise { - if (this.browsers.size >= this.config.maxSessions) { - // Evict oldest idle browser - const oldestJid = this.browsers.keys().next().value; - if (oldestJid) { - this.logger.warn(`[browser] Max sessions reached, evicting oldest: ${oldestJid}`); - await this.killBrowser(oldestJid); + const { instanceName } = options; + this.logger.log(`[browser] fetchCatalog instance=${instanceName}`); + + let state: InstanceClientState; + try { + state = await this.getReadyClient(instanceName); + } catch (err) { + // If init failed (e.g. not authenticated within timeout), + // return current QR/pairing state if available + const currentState = this.clients.get(instanceName); + if (currentState?.qrCode) { + return this.buildAuthPendingResult(options.jid, currentState); } + throw err; } - const userDataDir = this.sessionStore.userDataDir(instanceName); - this.logger.log(`[browser] Launching Chromium for instance=${instanceName} jid=${jid}`); + if (!state.ready || state.qrCode) { + return this.buildAuthPendingResult(options.jid, state); + } - // Clean up stale SingletonLock file from previous crashed launches. - // Chromium creates this lock file to prevent concurrent profile access, - // but if a previous process crashed, the lock stays and blocks new launches. - this.cleanStaleLocks(userDataDir); + // Client is ready — fetch catalog via window.WPP API + const page = await state.client.pupPage; + if (!page) { + throw new BadRequestException('WhatsApp Web page not available'); + } - const browser = await puppeteer.launch({ - executablePath: this.config.executablePath, - headless: this.config.headless, - userDataDir, - args: this.config.extraArgs, - defaultViewport: { width: 1280, height: 800 }, - ignoreDefaultArgs: ['--enable-automation'], - // Wait for initial page to be ready before returning - protocolTimeout: 60000, - }); + const result = await page.evaluate(async (): Promise<{ + catalog: BrowserProduct[]; + message?: string; + }> => { + const wpp = (window as any).WPP; + if (!wpp) return { catalog: [], message: 'WPP not available' }; + + const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; + const userId = (myUser && myUser._serialized) || ''; + if (!userId) return { catalog: [], message: 'User ID not found' }; + + const whatsappApi = wpp.whatsapp; + const productsById = new Map(); + + const addProduct = (rawProduct: any) => { + const product = rawProduct?.attributes || rawProduct; + if (!product?.id) return; + if (!productsById.has(product.id)) { + productsById.set(product.id, product as BrowserProduct); + } + }; - this.browsers.set(jid, browser); + const extractProductsFromCatalog = (catalogEntry: any): any[] => { + if (!catalogEntry) return []; + const productIndex = catalogEntry.productCollection?._index; + if (!productIndex || typeof productIndex !== 'object') return []; + return Object.keys(productIndex) + .map((productId) => productIndex[productId]?.attributes) + .filter(Boolean); + }; - // Handle unexpected browser disconnect — clean up so next call can re-launch - browser.on('disconnected', () => { - this.logger.warn(`[browser] Browser disconnected for jid=${jid}, cleaning up`); - this.browsers.delete(jid); - const timer = this.idleTimers.get(jid); - if (timer) { - clearTimeout(timer); - this.idleTimers.delete(jid); + // Layer 1: queryCatalog with pagination cursor + if (whatsappApi?.functions?.queryCatalog) { + try { + let afterToken: string | undefined = undefined; + let safetyCount = 0; + while (safetyCount < 500) { + const response: any = await whatsappApi.functions.queryCatalog(userId, afterToken); + const pageProducts: any[] = Array.isArray(response?.data) ? response.data : []; + for (const product of pageProducts) { + addProduct(product); + } + const nextAfter = response?.paging?.cursors?.after; + if (!nextAfter || nextAfter === afterToken) break; + afterToken = nextAfter; + safetyCount++; + } + } catch (error: any) { + console.log('queryCatalog unavailable:', error?.message); + } } - this.pendingQr.delete(jid); - }); - // Navigate to WA Web on the first page - const page = await browser.newPage(); - await page.setUserAgent( - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - ); - await page.goto('https://web.whatsapp.com/', { - waitUntil: 'networkidle2', - timeout: 90000, + // Layer 2: CatalogStore.findQuery + if (whatsappApi?.CatalogStore?.findQuery) { + try { + const results: any[] = await whatsappApi.CatalogStore.findQuery(userId); + if (Array.isArray(results)) { + for (const entry of results) { + const products = extractProductsFromCatalog(entry); + for (const product of products) { + addProduct(product); + } + } + } + } catch (error: any) { + console.log('CatalogStore.findQuery unavailable:', error?.message); + } + } + + // Layer 3: WPP.catalog.getMyCatalog + try { + const myCatalog: any = await wpp.catalog?.getMyCatalog?.(); + const fallbackProducts = extractProductsFromCatalog(myCatalog); + for (const product of fallbackProducts) { + addProduct(product); + } + } catch (error: any) { + console.log('getMyCatalog unavailable:', error?.message); + } + + // Layer 4: WPP.catalog.getProducts (last resort) + if (productsById.size === 0) { + try { + const fallbackProducts: any[] = await wpp.catalog?.getProducts?.(userId, 999); + if (Array.isArray(fallbackProducts)) { + for (const product of fallbackProducts) { + addProduct(product); + } + } + } catch (error: any) { + console.log('getProducts unavailable:', error?.message); + } + } + + return { catalog: Array.from(productsById.values()) }; }); - // Inject @wppconnect/wa-js library — required to access window.WPP API - // (whatsapp-web.js bundles this, but since we use puppeteer-core directly, - // we need to inject it ourselves) - await this.injectWaJs(page); + this.logger.log(`[browser] fetchCatalog got ${result.catalog.length} products`); - return browser; + return { + wuid: options.jid, + numberExists: true, + isBusiness: true, + catalogLength: result.catalog.length, + catalog: result.catalog, + truncated: false, + nextCursor: null, + source: 'browser', + }; } /** - * Inject @wppconnect/wa-js into the page and wait for WPP.isReady. - * This library provides the window.WPP API we use for catalog fetching. + * Public entry: fetch collections via browser. */ - private async injectWaJs(page: Page): Promise { - const wppExists = await page.evaluate(() => typeof (window as any).WPP !== 'undefined'); - if (wppExists) { - this.logger.log('[browser] WPP already loaded in page'); - return; + async fetchCollections( + options: BrowserCollectionsOptions, + ): Promise { + if (!this.config.enabled) { + throw new BadRequestException( + 'Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.', + ); } - this.logger.log('[browser] Injecting @wppconnect/wa-js into page...'); + const { instanceName } = options; + this.logger.log(`[browser] fetchCollections instance=${instanceName}`); - // Read wa-js from node_modules and inject via page.evaluate + let state: InstanceClientState; try { - const waJsPath = require.resolve('@wppconnect/wa-js'); - const fs = await import('fs'); - const waJsCode = fs.readFileSync(waJsPath, 'utf8'); - await page.evaluate(waJsCode); - this.logger.log(`[browser] wa-js injected (${waJsCode.length} chars)`); + state = await this.getReadyClient(instanceName); } catch (err) { - this.logger.error(`[browser] Failed to inject wa-js: ${(err as Error).message}`); - throw new BadRequestException( - 'Failed to inject @wppconnect/wa-js. Make sure it is installed: npm install @wppconnect/wa-js', - ); + const currentState = this.clients.get(instanceName); + if (currentState?.qrCode) { + return this.buildAuthPendingCollectionsResult(options.jid, currentState); + } + throw err; } - // Wait for WPP to be ready - try { - await page.waitForFunction( - () => (window as any).WPP && (window as any).WPP.isReady === true, - { timeout: 30000 }, - ); - this.logger.log('[browser] WPP.isReady = true'); - } catch (err) { - this.logger.warn('[browser] WPP.isReady timeout — continuing anyway'); + if (!state.ready || state.qrCode) { + return this.buildAuthPendingCollectionsResult(options.jid, state); } - // Give WA Web + WPP a few more seconds to stabilize - await new Promise((r) => setTimeout(r, 5000)); - } + const page = await state.client.pupPage; + if (!page) { + throw new BadRequestException('WhatsApp Web page not available'); + } - /** - * Ensure a QR code is available for the user to scan. - * Returns the QR data URL if authentication is required. - */ - private async ensureQrCode(jid: string, instanceName: string, page: Page): Promise { - // Check if we already have a pending QR for this JID - const existing = this.pendingQr.get(jid); - if (existing) return existing; + const result = await page.evaluate(async (): Promise<{ + collections: BrowserCollection[]; + message?: string; + }> => { + const wpp = (window as any).WPP; + if (!wpp) return { collections: [], message: 'WPP not available' }; - // Try to extract QR from WA Web page - try { - // WA Web renders QR as a canvas — extract data URL - const qrDataUrl = await page.evaluate(async () => { - const wpp = (window as any).WPP; - if (wpp?.conn?.getQRCode) { - try { - return await wpp.conn.getQRCode(); - } catch { - /* fall through */ + const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; + const userId = (myUser && myUser._serialized) || ''; + if (!userId) return { collections: [], message: 'User ID not found' }; + + const whatsappApi = wpp.whatsapp; + const collections: BrowserCollection[] = []; + + // Method 1: WPP.catalog.getCollections + try { + const response: any = await wpp.catalog?.getCollections?.(userId); + if (Array.isArray(response)) { + for (const c of response) { + const attrs = c?.attributes || c; + if (!attrs?.id) continue; + const productIndex = c?.productCollection?._index || attrs?.products?._index; + const products: BrowserProduct[] = []; + if (productIndex && typeof productIndex === 'object') { + for (const pid of Object.keys(productIndex)) { + const p = productIndex[pid]?.attributes || productIndex[pid]; + if (p) products.push(p as BrowserProduct); + } + } + collections.push({ + id: attrs.id, + name: attrs.name || '', + products, + status: attrs.status, + }); } } - // Fallback: scrape canvas - const canvas = document.querySelector('canvas[aria-label="QR code"], canvas'); - if (canvas) { - return (canvas as HTMLCanvasElement).toDataURL('image/png'); - } - return null; - }); + } catch (error: any) { + console.log('WPP.catalog.getCollections failed:', error?.message); + } - if (qrDataUrl) { - this.pendingQr.set(jid, qrDataUrl); - return qrDataUrl; + // Method 2: CollectionStore.findQuery fallback + if (collections.length === 0 && whatsappApi?.CollectionStore?.findQuery) { + try { + const results: any[] = await whatsappApi.CollectionStore.findQuery(userId); + if (Array.isArray(results)) { + for (const c of results) { + const attrs = c?.attributes || c; + if (!attrs?.id) continue; + const productIndex = c?.productCollection?._index; + const products: BrowserProduct[] = []; + if (productIndex && typeof productIndex === 'object') { + for (const pid of Object.keys(productIndex)) { + const p = productIndex[pid]?.attributes || productIndex[pid]; + if (p) products.push(p as BrowserProduct); + } + } + collections.push({ + id: attrs.id, + name: attrs.name || '', + products, + status: attrs.status, + }); + } + } + } catch (error: any) { + console.log('CollectionStore.findQuery failed:', error?.message); + } } - } catch (err) { - this.logger.warn(`[browser] Failed to extract QR: ${(err as Error).message}`); - } - return null; + return { collections }; + }); + + this.logger.log(`[browser] fetchCollections got ${result.collections.length} collections`); + + return { + wuid: options.jid, + name: null, + numberExists: true, + isBusiness: true, + collectionsLength: result.collections.length, + collections: result.collections, + source: 'browser', + }; } /** - * Reset the idle timer — call after each activity. - * When timer fires, the browser is killed to free memory. + * Request a phone-number pairing code for an instance. + * Returns the 8-character code (e.g. "ABCD1234") that the user enters + * on their phone in WhatsApp → Linked Devices → Link with phone number instead. + * + * Phone format: international, digits only (e.g. "6285733556953" for Indonesia). */ - private resetIdleTimer(jid: string): void { - const existing = this.idleTimers.get(jid); - if (existing) clearTimeout(existing); - - const timer = setTimeout(() => { - this.logger.log(`[browser] Idle timeout for jid=${jid}, killing browser`); - this.killBrowser(jid).catch((err) => { - this.logger.error(`[browser] Failed to kill idle browser: ${(err as Error).message}`); - }); - }, this.config.idleTimeoutMs); + async requestPairingCode(instanceName: string, phoneNumber: string): Promise { + if (!this.config.enabled) { + throw new BadRequestException( + 'Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.', + ); + } - this.idleTimers.set(jid, timer); + const state = await this.getReadyClient(instanceName); + // requestPairingCode works even before 'ready' event (it's needed for auth) + // whatsapp-web.js will trigger 'code_received' event with the code + const code = await state.client.requestPairingCode(phoneNumber); + state.pairingCode = code; + this.logger.log(`[browser] Pairing code requested for instance=${instanceName} phone=${phoneNumber}: ${code}`); + return code; } /** - * Remove stale Chromium lock files and kill orphan Chromium processes - * left over from previous crashed launches. - * - * Chromium creates SingletonLock, SingletonCookie, and SingletonSocket - * symlinks in the userDataDir. If a previous process crashed, these - * locks persist and block new launches with "profile appears to be in - * use by another Chromium process" error. + * Get current auth state for an instance (qrCode, pairingCode, ready). */ - private cleanStaleLocks(userDataDir: string): void { - // 1. Remove lock files/symlinks - const lockFiles = ['SingletonLock', 'SingletonCookie', 'SingletonSocket']; - for (const lockFile of lockFiles) { - const lockPath = join(userDataDir, lockFile); - if (existsSync(lockPath)) { - try { - unlinkSync(lockPath); - this.logger.log(`[browser] Removed stale lock: ${lockFile}`); - } catch (err) { - this.logger.warn(`[browser] Failed to remove ${lockFile}: ${(err as Error).message}`); - } - } + getAuthState(instanceName: string): { + ready: boolean; + qrCode: string | null; + pairingCode: string | null; + } { + const state = this.clients.get(instanceName); + if (!state) { + return { ready: false, qrCode: null, pairingCode: null }; } + return { + ready: state.ready, + qrCode: state.qrCode, + pairingCode: state.pairingCode, + }; + } - // 2. Kill orphan chromium processes (best-effort, ignore errors) - // This handles the case where a previous Puppeteer crash left - // chromium processes running and holding the profile. - try { - execSync('pkill -f chromium 2>/dev/null || true', { timeout: 5000 }); - } catch { - // pkill exit code 1 = no process matched, ignore + /** + * Logout: kill client + delete session for an instance. + */ + async logout(instanceName: string): Promise { + await this.killClient(instanceName); + const userDataDir = this.userDataDir(instanceName); + if (existsSync(userDataDir)) { + rmSync(userDataDir, { recursive: true, force: true }); + this.logger.log(`[browser] Deleted session for instance=${instanceName}`); } } /** - * Kill the browser for a JID and clean up timers. + * Shutdown all clients (for graceful app close). + */ + async shutdownAll(): Promise { + const names = Array.from(this.clients.keys()); + await Promise.all(names.map((n) => this.killClient(n))); + this.logger.log(`[browser] Shutdown ${names.length} client(s)`); + } + + private buildAuthPendingResult(jid: string, state: InstanceClientState): BrowserCatalogResult { + const result: any = { + wuid: jid, + numberExists: true, + isBusiness: true, + catalogLength: 0, + catalog: [], + truncated: false, + nextCursor: null, + source: 'browser', + status: 'auth_required', + }; + if (state.qrCode) result.qrCode = state.qrCode; + if (state.pairingCode) result.pairingCode = state.pairingCode; + return result; + } + + private buildAuthPendingCollectionsResult( + jid: string, + state: InstanceClientState, + ): BrowserCollectionsResult { + const result: any = { + wuid: jid, + name: null, + numberExists: true, + isBusiness: true, + collectionsLength: 0, + collections: [], + source: 'browser', + status: 'auth_required', + }; + if (state.qrCode) result.qrCode = state.qrCode; + if (state.pairingCode) result.pairingCode = state.pairingCode; + return result; + } + + /** + * Reset the idle timer for an instance. When timer fires, the client + * is killed to free memory. */ - private async killBrowser(jid: string): Promise { - const timer = this.idleTimers.get(jid); - if (timer) { - clearTimeout(timer); - this.idleTimers.delete(jid); + private resetIdleTimer(instanceName: string): void { + const state = this.clients.get(instanceName); + if (!state) return; + + if (state.idleTimer) clearTimeout(state.idleTimer); + + state.idleTimer = setTimeout(() => { + this.logger.log(`[browser] Idle timeout for instance=${instanceName}, killing client`); + this.killClient(instanceName).catch((err) => { + this.logger.error(`[browser] Failed to kill idle client: ${(err as Error).message}`); + }); + }, this.config.idleTimeoutMs); + } + + /** + * Kill the client for an instance and clean up timers. + */ + private async killClient(instanceName: string): Promise { + const state = this.clients.get(instanceName); + if (!state) return; + + if (state.idleTimer) { + clearTimeout(state.idleTimer); } - const browser = this.browsers.get(jid); - if (browser) { - try { - await browser.close(); - } catch (err) { - this.logger.warn(`[browser] Error closing browser: ${(err as Error).message}`); - } - this.browsers.delete(jid); + + try { + await state.client.destroy(); + } catch (err) { + this.logger.warn(`[browser] Error closing client: ${(err as Error).message}`); } - this.pendingQr.delete(jid); + + this.clients.delete(instanceName); } } diff --git a/src/api/integrations/channel/whatsapp/session-store.browser.ts b/src/api/integrations/channel/whatsapp/session-store.browser.ts deleted file mode 100644 index ccd9861e0..000000000 --- a/src/api/integrations/channel/whatsapp/session-store.browser.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Persistent session store for the browser-based catalog service. - * - * WhatsApp Web (whatsapp-web.js style) stores session as a set of files - * in a directory. We mirror this pattern, storing per-instance sessions - * under `${INSTANCE_DIR}/${instanceName}/browser-session/`. - * - * This survives across browser restarts so the user only needs to scan - * the QR code ONCE per WhatsApp account. - */ - -import { Logger } from '@config/logger.config'; -import { INSTANCE_DIR } from '@config/path.config'; -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs'; -import { join } from 'path'; - -const SESSION_SUBDIR = 'browser-session'; - -export interface StoredSessionData { - [key: string]: unknown; -} - -export class BrowserSessionStore { - private readonly logger = new Logger(BrowserSessionStore.name); - - /** - * Resolve the session directory for an instance. - */ - private sessionDir(instanceName: string): string { - return join(INSTANCE_DIR, instanceName, SESSION_SUBDIR); - } - - /** - * Check if a saved session exists for this instance. - */ - hasSession(instanceName: string): boolean { - const dir = this.sessionDir(instanceName); - if (!existsSync(dir)) return false; - const files = readdirSync(dir).filter((f) => !f.startsWith('.')); - return files.length > 0; - } - - /** - * Load all session files for an instance. - * Returns a map of filename → file content (JSON-parsed when possible). - */ - loadSession(instanceName: string): StoredSessionData { - const dir = this.sessionDir(instanceName); - const result: StoredSessionData = {}; - - if (!existsSync(dir)) return result; - - for (const file of readdirSync(dir)) { - if (file.startsWith('.')) continue; - const fullPath = join(dir, file); - try { - const raw = readFileSync(fullPath, 'utf8'); - try { - result[file] = JSON.parse(raw); - } catch { - result[file] = raw; - } - } catch (err) { - this.logger.warn(`Failed to read session file ${file}: ${(err as Error).message}`); - } - } - - return result; - } - - /** - * Save session data back to disk. - * Each top-level key becomes a file; values are JSON-serialized. - */ - saveSession(instanceName: string, data: StoredSessionData): void { - const dir = this.sessionDir(instanceName); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - - for (const [file, value] of Object.entries(data)) { - const fullPath = join(dir, file); - const serialized = typeof value === 'string' ? value : JSON.stringify(value, null, 2); - try { - writeFileSync(fullPath, serialized, 'utf8'); - } catch (err) { - this.logger.warn(`Failed to write session file ${file}: ${(err as Error).message}`); - } - } - } - - /** - * Delete the entire session for an instance (logout). - */ - deleteSession(instanceName: string): void { - const dir = this.sessionDir(instanceName); - if (existsSync(dir)) { - rmSync(dir, { recursive: true, force: true }); - this.logger.log(`Deleted browser session for instance ${instanceName}`); - } - } - - /** - * Path where session lives (for Puppeteer's userDataDir option). - * Using userDataDir lets WhatsApp Web store IndexedDB + LocalStorage - * so we don't need to manually restore session tokens. - */ - userDataDir(instanceName: string): string { - const dir = this.sessionDir(instanceName); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - return dir; - } -} diff --git a/src/api/server.module.ts b/src/api/server.module.ts index 3fd01e2ee..6f17d919d 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -18,7 +18,6 @@ import { EvolutionController } from './integrations/channel/evolution/evolution. import { MetaController } from './integrations/channel/meta/meta.controller'; import { BaileysController } from './integrations/channel/whatsapp/baileys.controller'; import { BrowserCatalogService } from './integrations/channel/whatsapp/catalog-browser.service'; -import { BrowserSessionStore } from './integrations/channel/whatsapp/session-store.browser'; import { ChatbotController } from './integrations/chatbot/chatbot.controller'; import { ChatwootController } from './integrations/chatbot/chatwoot/controllers/chatwoot.controller'; import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service'; @@ -112,8 +111,7 @@ export const labelController = new LabelController(waMonitor); // Browser-based catalog service (singleton, lazy-initialized) // Construction auto-registers itself with BrowserCatalogService.setInstance() // so BaileysStartupService can access it via BrowserCatalogService.getInstance() -const browserSessionStore = new BrowserSessionStore(); -export const browserCatalogService = new BrowserCatalogService(browserSessionStore); +export const browserCatalogService = new BrowserCatalogService(); export const eventManager = new EventManager(prismaRepository, waMonitor); export const chatbotController = new ChatbotController(prismaRepository, waMonitor); From 05bbb465ae73e14c8bb818f265946af214a0114f Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 09:24:24 +0000 Subject: [PATCH 21/32] feat(catalog-browser): add pairing UI + auth state endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds proper authentication flow for browser-based catalog sessions via QR code OR phone-number pairing code, with a web UI accessible at /catalog-pairing.html. New API endpoints: - POST /business/requestPairingCode/{instance} {phone} → {pairingCode, expiresIn, instructions} - GET /business/getAuthState/{instance} → {ready, qrCode, pairingCode, userId} - DELETE /business/logoutBrowser/{instance} → kills session + deletes auth data Web UI: - public/catalog-pairing.html — accessible at /catalog-pairing.html - Instance selector (auto-loads from /instance/fetchInstances) - Two auth methods: pairing code (recommended) or QR scan - Auto-polls /getAuthState every 3s for auth status - Auto-refreshes QR code when stale - Logout button to reset session Bug fix in catalog-browser.service.ts: - readyPromise now resolves on EITHER 'qr' OR 'ready' event (was: only 'ready') - Previously, if user needed auth, readyPromise waited 120s for 'ready' that never fired, then timed out. Now resolves as soon as QR is available, so caller can surface it to the user immediately. Signed-off-by: Kelvin Yuli Andrian --- public/catalog-pairing.html | 405 ++++++++++++++++++ src/api/controllers/business.controller.ts | 62 +++ src/api/dto/business.dto.ts | 7 +- .../whatsapp/catalog-browser.service.ts | 13 +- src/api/routes/business.router.ts | 173 +++++++- src/validate/business.schema.ts | 15 +- 6 files changed, 664 insertions(+), 11 deletions(-) create mode 100644 public/catalog-pairing.html diff --git a/public/catalog-pairing.html b/public/catalog-pairing.html new file mode 100644 index 000000000..db53f2a15 --- /dev/null +++ b/public/catalog-pairing.html @@ -0,0 +1,405 @@ + + + + + + Catalog Browser Auth — Evolution API + + + +
+

Catalog Browser Auth

+
WhatsApp Web session authentication for browser-based catalog fetch
+ +
+
+ + +
+
+ + +
+
+ + + + + + + +
+ + +
+ +
+ + + +
+ +
+ How to authenticate (choose one method): +
    +
  1. Pairing Code (recommended): Enter your phone number above (international format, e.g. 6285733556953), click "Request Pairing Code". An 8-character code will appear. On your phone: WhatsApp → Settings → Linked Devices → Link a Device → "Link with phone number instead" → Enter the code.
  2. +
  3. QR Code: Click "Show QR Code". A QR will appear. On your phone: WhatsApp → Settings → Linked Devices → Link a Device → Scan the QR code with your camera.
  4. +
  5. After successful auth, this page will auto-detect and show "Authenticated". You can then use POST /business/getCatalog/{instance} with {"provider":"browser"} to fetch full catalog.
  6. +
+
+ +
Ready.
+
+ + + + diff --git a/src/api/controllers/business.controller.ts b/src/api/controllers/business.controller.ts index 3c7f166cc..e94c3d6cb 100644 --- a/src/api/controllers/business.controller.ts +++ b/src/api/controllers/business.controller.ts @@ -1,6 +1,7 @@ import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto'; import { InstanceDto } from '@api/dto/instance.dto'; import { WAMonitoringService } from '@api/services/monitor.service'; +import { BrowserCatalogService } from '@api/integrations/channel/whatsapp/catalog-browser.service'; export class BusinessController { constructor(private readonly waMonitor: WAMonitoringService) {} @@ -12,4 +13,65 @@ export class BusinessController { public async fetchCollections({ instanceName }: InstanceDto, data: getCollectionsDto) { return await this.waMonitor.waInstances[instanceName].fetchCollections(instanceName, data); } + + /** + * Request a phone-number pairing code for browser-based catalog session. + * Returns 8-character code (e.g. "ABCD1234") the user enters on their phone + * in WhatsApp → Linked Devices → Link with phone number instead. + * + * Phone format: international, digits only (e.g. "6285733556953" for Indonesia). + */ + public async requestPairingCode({ instanceName }: InstanceDto, data: { phone: string }) { + const svc = BrowserCatalogService.getInstance(); + if (!svc) { + throw new Error('Browser catalog service is not initialized. Set CATALOG_BROWSER_ENABLED=true.'); + } + const pairingCode = await svc.requestPairingCode(instanceName, data.phone); + return { + instance: instanceName, + phone: data.phone, + pairingCode, + expiresIn: 60, + instructions: + 'Open WhatsApp on your phone → Settings → Linked Devices → Link a Device → ' + + 'Link with phone number instead → Enter the 8-character code', + }; + } + + /** + * Get current auth state of the browser session for an instance. + * Used by the pairing UI to poll for: QR code, pairing code, or authenticated status. + */ + public async getAuthState({ instanceName }: InstanceDto) { + const svc = BrowserCatalogService.getInstance(); + if (!svc) { + return { + instance: instanceName, + enabled: false, + message: 'Browser catalog service is not initialized. Set CATALOG_BROWSER_ENABLED=true.', + }; + } + return { + instance: instanceName, + enabled: true, + ...svc.getAuthState(instanceName), + }; + } + + /** + * Logout / delete the browser session for an instance. + * User will need to re-scan QR or re-pair on next catalog fetch. + */ + public async logoutBrowser({ instanceName }: InstanceDto) { + const svc = BrowserCatalogService.getInstance(); + if (!svc) { + throw new Error('Browser catalog service is not initialized.'); + } + await svc.logout(instanceName); + return { + instance: instanceName, + loggedOut: true, + message: 'Browser session deleted. Next catalog fetch will require new auth.', + }; + } } diff --git a/src/api/dto/business.dto.ts b/src/api/dto/business.dto.ts index 90c8c7386..8680e64bf 100644 --- a/src/api/dto/business.dto.ts +++ b/src/api/dto/business.dto.ts @@ -12,7 +12,7 @@ export class getCatalogDto { * - `browser`: launches a Puppeteer browser session to fetch via * web.whatsapp.com's internal API. Returns full catalog without * WhatsApp's protocol-level truncation. Requires `CATALOG_BROWSER_ENABLED=true` - * and the user must complete a one-time QR scan for the browser session. + * and the user must complete a one-time QR scan or pairing code for the browser session. */ provider?: 'baileys' | 'browser'; } @@ -26,3 +26,8 @@ export class getCollectionsDto { */ provider?: 'baileys' | 'browser'; } + +export class requestPairingCodeDto { + /** Phone number in international format, digits only (e.g. "6285733556953" for Indonesia) */ + phone: string; +} diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index 47b043b1c..998c2e335 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -230,7 +230,6 @@ export class BrowserCatalogService { executablePath: this.config.executablePath, headless: this.config.headless, args: this.config.extraArgs, - // @ts-expect-error - bypassCSP is supported by whatsapp-web.js puppeteer config but not in puppeteer types bypassCSP: true, }, }); @@ -265,12 +264,22 @@ export class BrowserCatalogService { this.clients.delete(instanceName); }); - // Create readyPromise that resolves when 'ready' event fires + // Create readyPromise that resolves when EITHER 'qr' OR 'ready' event fires. + // - 'qr' means client needs authentication (return QR to caller) + // - 'ready' means client is authenticated and operational + // Either way, the caller can proceed (either show QR or fetch catalog). + // Timeout: 120s — if neither fires, something is wrong (network issue etc.) state.readyPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error(`Client initialization timed out after 120s for instance=${instanceName}`)); }, 120000); + client.once('qr', () => { + clearTimeout(timeout); + // Don't resolve immediately — wait a tick to ensure state.qrCode is set + // in the 'qr' event handler above before resolving. + setTimeout(() => resolve(), 100); + }); client.once('ready', () => { clearTimeout(timeout); resolve(); diff --git a/src/api/routes/business.router.ts b/src/api/routes/business.router.ts index 867a0f124..53eb52d80 100644 --- a/src/api/routes/business.router.ts +++ b/src/api/routes/business.router.ts @@ -1,8 +1,12 @@ import { RouterBroker } from '@api/abstract/abstract.router'; -import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto'; +import { + getCatalogDto, + getCollectionsDto, + requestPairingCodeDto, +} from '@api/dto/business.dto'; import { businessController } from '@api/server.module'; import { createMetaErrorResponse } from '@utils/errorResponse'; -import { catalogSchema, collectionsSchema } from '@validate/validate.schema'; +import { catalogSchema, collectionsSchema, pairingCodeSchema } from '@validate/validate.schema'; import { RequestHandler, Router } from 'express'; import { HttpStatus } from './index.router'; @@ -21,7 +25,7 @@ export class BusinessRouter extends RouterBroker { * post: * tags: [Business] * summary: Get WhatsApp Business catalog - * description: Fetches all products from a WhatsApp Business catalog with automatic pagination + * description: Fetches all products from a WhatsApp Business catalog with automatic pagination. Use `provider: "browser"` to bypass Baileys protocol-level truncation (requires CATALOG_BROWSER_ENABLED=true and one-time QR/pairing auth). * security: * - apikey: [] * parameters: @@ -77,7 +81,7 @@ export class BusinessRouter extends RouterBroker { * post: * tags: [Business] * summary: Get WhatsApp Business collections - * description: Fetches all collections with their products from a WhatsApp Business account + * description: Fetches all collections with their products from a WhatsApp Business account. Use `provider: "browser"` to bypass Baileys protocol-level truncation. * security: * - apikey: [] * parameters: @@ -118,13 +122,168 @@ export class BusinessRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); } catch (error) { - // Log error for debugging console.error('Business collections error:', error); - - // Use utility function to create standardized error response const errorResponse = createMetaErrorResponse(error, 'business_collections'); return res.status(errorResponse.status).json(errorResponse); } + }) + + /** + * @swagger + * /business/requestPairingCode/{instanceName}: + * post: + * tags: [Business] + * summary: Request phone-number pairing code for browser catalog session + * description: Generates an 8-character pairing code that the user enters on their phone in WhatsApp → Linked Devices → Link a Device → Link with phone number instead. Requires CATALOG_BROWSER_ENABLED=true. + * security: + * - apikey: [] + * parameters: + * - in: path + * name: instanceName + * required: true + * schema: + * type: string + * description: Instance name + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [phone] + * properties: + * phone: + * type: string + * description: Phone number in international format, digits only (e.g. "6285733556953" for Indonesia) + * example: "6285733556953" + * responses: + * 200: + * description: Pairing code generated + * content: + * application/json: + * schema: + * type: object + * properties: + * instance: + * type: string + * phone: + * type: string + * pairingCode: + * type: string + * description: 8-character code (e.g. "ABCD1234") + * expiresIn: + * type: number + * description: Code validity in seconds (60) + * instructions: + * type: string + */ + .post(this.routerPath('requestPairingCode'), ...guards, async (req, res) => { + try { + const response = await this.dataValidate({ + request: req, + schema: pairingCodeSchema, + ClassRef: requestPairingCodeDto, + execute: (instance, data) => businessController.requestPairingCode(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); + } catch (error) { + console.error('Pairing code error:', error); + const errorResponse = createMetaErrorResponse(error, 'pairing_code'); + return res.status(errorResponse.status).json(errorResponse); + } + }) + + /** + * @swagger + * /business/getAuthState/{instanceName}: + * get: + * tags: [Business] + * summary: Get browser catalog session auth state + * description: Returns the current authentication state of the browser-based catalog session (qrCode, pairingCode, ready). Use this to poll for auth status from the pairing UI. + * security: + * - apikey: [] + * parameters: + * - in: path + * name: instanceName + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Auth state + * content: + * application/json: + * schema: + * type: object + * properties: + * instance: + * type: string + * enabled: + * type: boolean + * ready: + * type: boolean + * description: True if browser session is authenticated and ready + * qrCode: + * type: string + * nullable: true + * description: QR code data URL (present when session needs QR scan) + * pairingCode: + * type: string + * nullable: true + * description: 8-character pairing code (present after requestPairingCode call) + */ + .get(this.routerPath('getAuthState'), ...guards, async (req, res) => { + try { + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: null, + execute: (instance) => businessController.getAuthState(instance), + }); + + return res.status(HttpStatus.OK).json(response); + } catch (error) { + console.error('Auth state error:', error); + const errorResponse = createMetaErrorResponse(error, 'auth_state'); + return res.status(errorResponse.status).json(errorResponse); + } + }) + + /** + * @swagger + * /business/logoutBrowser/{instanceName}: + * delete: + * tags: [Business] + * summary: Logout browser catalog session + * description: Kills the browser session and deletes persisted auth data. Next catalog fetch will require new QR scan or pairing code. + * security: + * - apikey: [] + * parameters: + * - in: path + * name: instanceName + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Logged out + */ + .delete(this.routerPath('logoutBrowser'), ...guards, async (req, res) => { + try { + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: null, + execute: (instance) => businessController.logoutBrowser(instance), + }); + + return res.status(HttpStatus.OK).json(response); + } catch (error) { + console.error('Logout browser error:', error); + const errorResponse = createMetaErrorResponse(error, 'logout_browser'); + return res.status(errorResponse.status).json(errorResponse); + } }); } diff --git a/src/validate/business.schema.ts b/src/validate/business.schema.ts index 812e9e8fe..e13b30bab 100644 --- a/src/validate/business.schema.ts +++ b/src/validate/business.schema.ts @@ -7,7 +7,7 @@ const providerProperty: JSONSchema7 = { "Fetch backend. 'baileys' (default) uses Baileys protocol-level API. " + "'browser' launches a Puppeteer session that fetches via web.whatsapp.com — " + 'returns full catalog without protocol-level truncation. ' + - 'Requires CATALOG_BROWSER_ENABLED=true and one-time QR scan per instance.', + 'Requires CATALOG_BROWSER_ENABLED=true and one-time QR/pairing auth per instance.', }; export const catalogSchema: JSONSchema7 = { @@ -29,3 +29,16 @@ export const collectionsSchema: JSONSchema7 = { provider: providerProperty, }, }; + +export const pairingCodeSchema: JSONSchema7 = { + type: 'object', + required: ['phone'], + properties: { + phone: { + type: 'string', + pattern: '^[0-9]+$', + description: 'Phone number in international format, digits only (e.g. "6285733556953")', + examples: ['6285733556953'], + }, + }, +}; From 5c5e4262377a56a58049bc2f81b97c5cbf13e29d Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 09:28:47 +0000 Subject: [PATCH 22/32] style: fix lint errors (unused import, formatting) Signed-off-by: Kelvin Yuli Andrian --- src/api/controllers/business.controller.ts | 2 +- .../whatsapp/catalog-browser.service.ts | 311 +++++++++--------- src/api/routes/business.router.ts | 6 +- 3 files changed, 149 insertions(+), 170 deletions(-) diff --git a/src/api/controllers/business.controller.ts b/src/api/controllers/business.controller.ts index e94c3d6cb..54d9b3239 100644 --- a/src/api/controllers/business.controller.ts +++ b/src/api/controllers/business.controller.ts @@ -1,7 +1,7 @@ import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto'; import { InstanceDto } from '@api/dto/instance.dto'; -import { WAMonitoringService } from '@api/services/monitor.service'; import { BrowserCatalogService } from '@api/integrations/channel/whatsapp/catalog-browser.service'; +import { WAMonitoringService } from '@api/services/monitor.service'; export class BusinessController { constructor(private readonly waMonitor: WAMonitoringService) {} diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index 998c2e335..c1566991f 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -20,12 +20,11 @@ * bedones-whatsapp/apps/whatsapp-connector/src/catalog/catalog.service.ts */ -import { existsSync, mkdirSync, rmSync, unlinkSync, writeFileSync } from 'fs'; -import { join } from 'path'; - import { Logger } from '@config/logger.config'; import { INSTANCE_DIR } from '@config/path.config'; import { BadRequestException } from '@exceptions'; +import { existsSync, mkdirSync, rmSync, unlinkSync } from 'fs'; +import { join } from 'path'; import { Client, LocalAuth } from 'whatsapp-web.js'; import { @@ -82,9 +81,7 @@ export class BrowserCatalogService { BrowserCatalogService.setInstance(this); } - static async fetchCatalogOrThrow( - options: BrowserCatalogOptions, - ): Promise { + static async fetchCatalogOrThrow(options: BrowserCatalogOptions): Promise { const svc = BrowserCatalogService.getInstance(); if (!svc) { throw new BadRequestException( @@ -94,9 +91,7 @@ export class BrowserCatalogService { return svc.fetchCatalog(options); } - static async fetchCollectionsOrThrow( - options: BrowserCollectionsOptions, - ): Promise { + static async fetchCollectionsOrThrow(options: BrowserCollectionsOptions): Promise { const svc = BrowserCatalogService.getInstance(); if (!svc) { throw new BadRequestException( @@ -111,12 +106,9 @@ export class BrowserCatalogService { const idleTimeoutMs = parseInt(process.env.CATALOG_BROWSER_IDLE_TIMEOUT_MS || '600000', 10); const maxSessions = parseInt(process.env.CATALOG_BROWSER_MAX_SESSIONS || '5', 10); const headlessEnv = (process.env.CATALOG_BROWSER_HEADLESS || 'true').toLowerCase(); - const headless: boolean | 'shell' = - headlessEnv === 'shell' ? 'shell' : headlessEnv === 'false' ? false : true; + const headless: boolean | 'shell' = headlessEnv === 'shell' ? 'shell' : headlessEnv === 'false' ? false : true; const executablePath = - process.env.PUPPETEER_EXECUTABLE_PATH || - process.env.CHROMIUM_PATH || - '/usr/bin/chromium-browser'; + process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROMIUM_PATH || '/usr/bin/chromium-browser'; return { enabled, @@ -185,9 +177,7 @@ export class BrowserCatalogService { // Evict oldest if at capacity if (this.clients.size >= this.config.maxSessions) { - const oldest = Array.from(this.clients.entries()).sort( - (a, b) => a[1].lastActivity - b[1].lastActivity, - )[0]; + const oldest = Array.from(this.clients.entries()).sort((a, b) => a[1].lastActivity - b[1].lastActivity)[0]; if (oldest) { this.logger.warn(`[browser] Max sessions reached, evicting: ${oldest[0]}`); await this.killClient(oldest[0]); @@ -305,9 +295,7 @@ export class BrowserCatalogService { */ async fetchCatalog(options: BrowserCatalogOptions): Promise { if (!this.config.enabled) { - throw new BadRequestException( - 'Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.', - ); + throw new BadRequestException('Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.'); } const { instanceName } = options; @@ -336,102 +324,104 @@ export class BrowserCatalogService { throw new BadRequestException('WhatsApp Web page not available'); } - const result = await page.evaluate(async (): Promise<{ - catalog: BrowserProduct[]; - message?: string; - }> => { - const wpp = (window as any).WPP; - if (!wpp) return { catalog: [], message: 'WPP not available' }; - - const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; - const userId = (myUser && myUser._serialized) || ''; - if (!userId) return { catalog: [], message: 'User ID not found' }; - - const whatsappApi = wpp.whatsapp; - const productsById = new Map(); - - const addProduct = (rawProduct: any) => { - const product = rawProduct?.attributes || rawProduct; - if (!product?.id) return; - if (!productsById.has(product.id)) { - productsById.set(product.id, product as BrowserProduct); - } - }; - - const extractProductsFromCatalog = (catalogEntry: any): any[] => { - if (!catalogEntry) return []; - const productIndex = catalogEntry.productCollection?._index; - if (!productIndex || typeof productIndex !== 'object') return []; - return Object.keys(productIndex) - .map((productId) => productIndex[productId]?.attributes) - .filter(Boolean); - }; - - // Layer 1: queryCatalog with pagination cursor - if (whatsappApi?.functions?.queryCatalog) { - try { - let afterToken: string | undefined = undefined; - let safetyCount = 0; - while (safetyCount < 500) { - const response: any = await whatsappApi.functions.queryCatalog(userId, afterToken); - const pageProducts: any[] = Array.isArray(response?.data) ? response.data : []; - for (const product of pageProducts) { - addProduct(product); + const result = await page.evaluate( + async (): Promise<{ + catalog: BrowserProduct[]; + message?: string; + }> => { + const wpp = (window as any).WPP; + if (!wpp) return { catalog: [], message: 'WPP not available' }; + + const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; + const userId = (myUser && myUser._serialized) || ''; + if (!userId) return { catalog: [], message: 'User ID not found' }; + + const whatsappApi = wpp.whatsapp; + const productsById = new Map(); + + const addProduct = (rawProduct: any) => { + const product = rawProduct?.attributes || rawProduct; + if (!product?.id) return; + if (!productsById.has(product.id)) { + productsById.set(product.id, product as BrowserProduct); + } + }; + + const extractProductsFromCatalog = (catalogEntry: any): any[] => { + if (!catalogEntry) return []; + const productIndex = catalogEntry.productCollection?._index; + if (!productIndex || typeof productIndex !== 'object') return []; + return Object.keys(productIndex) + .map((productId) => productIndex[productId]?.attributes) + .filter(Boolean); + }; + + // Layer 1: queryCatalog with pagination cursor + if (whatsappApi?.functions?.queryCatalog) { + try { + let afterToken: string | undefined = undefined; + let safetyCount = 0; + while (safetyCount < 500) { + const response: any = await whatsappApi.functions.queryCatalog(userId, afterToken); + const pageProducts: any[] = Array.isArray(response?.data) ? response.data : []; + for (const product of pageProducts) { + addProduct(product); + } + const nextAfter = response?.paging?.cursors?.after; + if (!nextAfter || nextAfter === afterToken) break; + afterToken = nextAfter; + safetyCount++; } - const nextAfter = response?.paging?.cursors?.after; - if (!nextAfter || nextAfter === afterToken) break; - afterToken = nextAfter; - safetyCount++; + } catch (error: any) { + console.log('queryCatalog unavailable:', error?.message); } - } catch (error: any) { - console.log('queryCatalog unavailable:', error?.message); } - } - // Layer 2: CatalogStore.findQuery - if (whatsappApi?.CatalogStore?.findQuery) { - try { - const results: any[] = await whatsappApi.CatalogStore.findQuery(userId); - if (Array.isArray(results)) { - for (const entry of results) { - const products = extractProductsFromCatalog(entry); - for (const product of products) { - addProduct(product); + // Layer 2: CatalogStore.findQuery + if (whatsappApi?.CatalogStore?.findQuery) { + try { + const results: any[] = await whatsappApi.CatalogStore.findQuery(userId); + if (Array.isArray(results)) { + for (const entry of results) { + const products = extractProductsFromCatalog(entry); + for (const product of products) { + addProduct(product); + } } } + } catch (error: any) { + console.log('CatalogStore.findQuery unavailable:', error?.message); } - } catch (error: any) { - console.log('CatalogStore.findQuery unavailable:', error?.message); } - } - // Layer 3: WPP.catalog.getMyCatalog - try { - const myCatalog: any = await wpp.catalog?.getMyCatalog?.(); - const fallbackProducts = extractProductsFromCatalog(myCatalog); - for (const product of fallbackProducts) { - addProduct(product); + // Layer 3: WPP.catalog.getMyCatalog + try { + const myCatalog: any = await wpp.catalog?.getMyCatalog?.(); + const fallbackProducts = extractProductsFromCatalog(myCatalog); + for (const product of fallbackProducts) { + addProduct(product); + } + } catch (error: any) { + console.log('getMyCatalog unavailable:', error?.message); } - } catch (error: any) { - console.log('getMyCatalog unavailable:', error?.message); - } - // Layer 4: WPP.catalog.getProducts (last resort) - if (productsById.size === 0) { - try { - const fallbackProducts: any[] = await wpp.catalog?.getProducts?.(userId, 999); - if (Array.isArray(fallbackProducts)) { - for (const product of fallbackProducts) { - addProduct(product); + // Layer 4: WPP.catalog.getProducts (last resort) + if (productsById.size === 0) { + try { + const fallbackProducts: any[] = await wpp.catalog?.getProducts?.(userId, 999); + if (Array.isArray(fallbackProducts)) { + for (const product of fallbackProducts) { + addProduct(product); + } } + } catch (error: any) { + console.log('getProducts unavailable:', error?.message); } - } catch (error: any) { - console.log('getProducts unavailable:', error?.message); } - } - return { catalog: Array.from(productsById.values()) }; - }); + return { catalog: Array.from(productsById.values()) }; + }, + ); this.logger.log(`[browser] fetchCatalog got ${result.catalog.length} products`); @@ -450,13 +440,9 @@ export class BrowserCatalogService { /** * Public entry: fetch collections via browser. */ - async fetchCollections( - options: BrowserCollectionsOptions, - ): Promise { + async fetchCollections(options: BrowserCollectionsOptions): Promise { if (!this.config.enabled) { - throw new BadRequestException( - 'Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.', - ); + throw new BadRequestException('Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.'); } const { instanceName } = options; @@ -482,56 +468,29 @@ export class BrowserCatalogService { throw new BadRequestException('WhatsApp Web page not available'); } - const result = await page.evaluate(async (): Promise<{ - collections: BrowserCollection[]; - message?: string; - }> => { - const wpp = (window as any).WPP; - if (!wpp) return { collections: [], message: 'WPP not available' }; - - const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; - const userId = (myUser && myUser._serialized) || ''; - if (!userId) return { collections: [], message: 'User ID not found' }; - - const whatsappApi = wpp.whatsapp; - const collections: BrowserCollection[] = []; - - // Method 1: WPP.catalog.getCollections - try { - const response: any = await wpp.catalog?.getCollections?.(userId); - if (Array.isArray(response)) { - for (const c of response) { - const attrs = c?.attributes || c; - if (!attrs?.id) continue; - const productIndex = c?.productCollection?._index || attrs?.products?._index; - const products: BrowserProduct[] = []; - if (productIndex && typeof productIndex === 'object') { - for (const pid of Object.keys(productIndex)) { - const p = productIndex[pid]?.attributes || productIndex[pid]; - if (p) products.push(p as BrowserProduct); - } - } - collections.push({ - id: attrs.id, - name: attrs.name || '', - products, - status: attrs.status, - }); - } - } - } catch (error: any) { - console.log('WPP.catalog.getCollections failed:', error?.message); - } + const result = await page.evaluate( + async (): Promise<{ + collections: BrowserCollection[]; + message?: string; + }> => { + const wpp = (window as any).WPP; + if (!wpp) return { collections: [], message: 'WPP not available' }; + + const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; + const userId = (myUser && myUser._serialized) || ''; + if (!userId) return { collections: [], message: 'User ID not found' }; - // Method 2: CollectionStore.findQuery fallback - if (collections.length === 0 && whatsappApi?.CollectionStore?.findQuery) { + const whatsappApi = wpp.whatsapp; + const collections: BrowserCollection[] = []; + + // Method 1: WPP.catalog.getCollections try { - const results: any[] = await whatsappApi.CollectionStore.findQuery(userId); - if (Array.isArray(results)) { - for (const c of results) { + const response: any = await wpp.catalog?.getCollections?.(userId); + if (Array.isArray(response)) { + for (const c of response) { const attrs = c?.attributes || c; if (!attrs?.id) continue; - const productIndex = c?.productCollection?._index; + const productIndex = c?.productCollection?._index || attrs?.products?._index; const products: BrowserProduct[] = []; if (productIndex && typeof productIndex === 'object') { for (const pid of Object.keys(productIndex)) { @@ -548,12 +507,41 @@ export class BrowserCatalogService { } } } catch (error: any) { - console.log('CollectionStore.findQuery failed:', error?.message); + console.log('WPP.catalog.getCollections failed:', error?.message); } - } - return { collections }; - }); + // Method 2: CollectionStore.findQuery fallback + if (collections.length === 0 && whatsappApi?.CollectionStore?.findQuery) { + try { + const results: any[] = await whatsappApi.CollectionStore.findQuery(userId); + if (Array.isArray(results)) { + for (const c of results) { + const attrs = c?.attributes || c; + if (!attrs?.id) continue; + const productIndex = c?.productCollection?._index; + const products: BrowserProduct[] = []; + if (productIndex && typeof productIndex === 'object') { + for (const pid of Object.keys(productIndex)) { + const p = productIndex[pid]?.attributes || productIndex[pid]; + if (p) products.push(p as BrowserProduct); + } + } + collections.push({ + id: attrs.id, + name: attrs.name || '', + products, + status: attrs.status, + }); + } + } + } catch (error: any) { + console.log('CollectionStore.findQuery failed:', error?.message); + } + } + + return { collections }; + }, + ); this.logger.log(`[browser] fetchCollections got ${result.collections.length} collections`); @@ -577,9 +565,7 @@ export class BrowserCatalogService { */ async requestPairingCode(instanceName: string, phoneNumber: string): Promise { if (!this.config.enabled) { - throw new BadRequestException( - 'Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.', - ); + throw new BadRequestException('Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.'); } const state = await this.getReadyClient(instanceName); @@ -648,10 +634,7 @@ export class BrowserCatalogService { return result; } - private buildAuthPendingCollectionsResult( - jid: string, - state: InstanceClientState, - ): BrowserCollectionsResult { + private buildAuthPendingCollectionsResult(jid: string, state: InstanceClientState): BrowserCollectionsResult { const result: any = { wuid: jid, name: null, diff --git a/src/api/routes/business.router.ts b/src/api/routes/business.router.ts index 53eb52d80..7bcb265d9 100644 --- a/src/api/routes/business.router.ts +++ b/src/api/routes/business.router.ts @@ -1,9 +1,5 @@ import { RouterBroker } from '@api/abstract/abstract.router'; -import { - getCatalogDto, - getCollectionsDto, - requestPairingCodeDto, -} from '@api/dto/business.dto'; +import { getCatalogDto, getCollectionsDto, requestPairingCodeDto } from '@api/dto/business.dto'; import { businessController } from '@api/server.module'; import { createMetaErrorResponse } from '@utils/errorResponse'; import { catalogSchema, collectionsSchema, pairingCodeSchema } from '@validate/validate.schema'; From 896456566dd629cbdf005c7d5fe8231ee053c895 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 09:44:31 +0000 Subject: [PATCH 23/32] =?UTF-8?q?ci:=20build=20amd64=20only=20=E2=80=94=20?= =?UTF-8?q?fix=20QEMU=20Illegal=20instruction=20crash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-arch build (amd64+arm64) via QEMU emulation was crashing with 'qemu: uncaught target signal 4 (Illegal instruction) - core dumped' during arm64 build of native Node.js modules (sharp, puppeteer). Removed: - docker/setup-qemu-action (no longer needed) - linux/arm64 from platforms list Kept linux/amd64 only — matches Kelvin's WSL2 x86_64 host. Build time ~halved (no cross-compilation). Signed-off-by: Kelvin Yuli Andrian --- .github/workflows/publish_ghcr.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/publish_ghcr.yml b/.github/workflows/publish_ghcr.yml index 188cc42c1..e365f8e2a 100644 --- a/.github/workflows/publish_ghcr.yml +++ b/.github/workflows/publish_ghcr.yml @@ -40,9 +40,6 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=sha,prefix= - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -59,7 +56,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From e098bffbaf58096d62fec0fdd2249dbe92ccba07 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 10:34:17 +0000 Subject: [PATCH 24/32] fix(catalog-browser): sanitize clientId + audit library usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix 'Invalid clientId' error when instance name contains spaces or special chars (e.g. "Warung Lakku" with space). Changes: - Added sanitizeClientId(instanceName): replaces non-alphanumeric chars with hyphens, collapses consecutive hyphens, trims edges. "Warung Lakku" → "Warung-Lakku" (valid for LocalAuth) - requestPairingCode: validate phone format (6-15 digits, no +/spaces) - requestPairingCode: throw clear error if already authenticated (requestPairingCode requires UNPAIRED state) - getAuthState: now returns userId (extracted from client.info.wid after 'ready' event) — needed by UI to display authenticated user Signed-off-by: Kelvin Yuli Andrian --- .../whatsapp/catalog-browser.service.ts | 59 +++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index c1566991f..0f15bb9b5 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -145,6 +145,29 @@ export class BrowserCatalogService { return join(INSTANCE_DIR, instanceName, SESSION_SUBDIR); } + /** + * Sanitize an instance name into a valid LocalAuth clientId. + * + * whatsapp-web.js LocalAuth requires clientId to be alphanumeric + underscore + * + hyphen only. Instance names like "Warung Lakku" (with space) are rejected + * with "Invalid clientId" error. + * + * Strategy: replace any non-alphanumeric char with a hyphen, collapse + * consecutive hyphens, and trim leading/trailing hyphens. + * + * Examples: + * "Warung Lakku" → "Warung-Lakku" + * "kelvincruv" → "kelvincruv" + * "Hobi Haus" → "Hobi-Haus" + * "My Instance #1!" → "My-Instance-1" + */ + private sanitizeClientId(instanceName: string): string { + return instanceName + .replace(/[^a-zA-Z0-9_-]/g, '-') + .replace(/-+/g, '-') + .replace(/^-+|-+$/g, ''); + } + /** * Clean stale Chromium lock files left by previous crashed sessions. */ @@ -213,7 +236,7 @@ export class BrowserCatalogService { const client = new Client({ authStrategy: new LocalAuth({ - clientId: instanceName, + clientId: this.sanitizeClientId(instanceName), dataPath: userDataDir, }), puppeteer: { @@ -568,9 +591,26 @@ export class BrowserCatalogService { throw new BadRequestException('Browser catalog service is disabled. Set CATALOG_BROWSER_ENABLED=true to enable.'); } + // Validate phone format: international, digits only (no +, no spaces) + if (!phoneNumber || !/^\d{6,15}$/.test(phoneNumber)) { + throw new BadRequestException( + 'Invalid phone number. Must be international format, digits only (e.g. "6285733556953" for Indonesia). ' + + 'No "+", spaces, or hyphens.', + ); + } + const state = await this.getReadyClient(instanceName); - // requestPairingCode works even before 'ready' event (it's needed for auth) - // whatsapp-web.js will trigger 'code_received' event with the code + + // If already authenticated, no need for pairing code + if (state.ready) { + throw new BadRequestException( + `Instance "${instanceName}" browser session is already authenticated. No pairing code needed.`, + ); + } + + // requestPairingCode requires the client to be in UNPAIRED state (socket connected + // but not yet authenticated). The 'qr' event guarantees this state. + // getReadyClient resolves on 'qr' or 'ready', so we should be safe here. const code = await state.client.requestPairingCode(phoneNumber); state.pairingCode = code; this.logger.log(`[browser] Pairing code requested for instance=${instanceName} phone=${phoneNumber}: ${code}`); @@ -584,15 +624,26 @@ export class BrowserCatalogService { ready: boolean; qrCode: string | null; pairingCode: string | null; + userId: string | null; } { const state = this.clients.get(instanceName); if (!state) { - return { ready: false, qrCode: null, pairingCode: null }; + return { ready: false, qrCode: null, pairingCode: null, userId: null }; + } + // Extract userId from client.info if available (set after 'ready' event) + let userId: string | null = null; + try { + if (state.ready && state.client?.info?.wid) { + userId = state.client.info.wid._serialized || null; + } + } catch { + // client.info may not be available yet } return { ready: state.ready, qrCode: state.qrCode, pairingCode: state.pairingCode, + userId, }; } From 04cdf4a9dcedced7a978bba1851b178262adce1a Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 11:14:35 +0000 Subject: [PATCH 25/32] fix(catalog-browser): pass InstanceDto as ClassRef, not null dataValidate() in abstract.router.ts does 'new ClassRef()' on line 32. Passing ClassRef: null caused 'n is not a constructor' error (n = minified name for ClassRef) for GET /getAuthState and DELETE /logoutBrowser endpoints. Fixed by passing InstanceDto as ClassRef (same pattern as other GET/DELETE instance endpoints in instance.router.ts). Signed-off-by: Kelvin Yuli Andrian --- src/api/routes/business.router.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/api/routes/business.router.ts b/src/api/routes/business.router.ts index 7bcb265d9..f2b105ec2 100644 --- a/src/api/routes/business.router.ts +++ b/src/api/routes/business.router.ts @@ -1,5 +1,6 @@ import { RouterBroker } from '@api/abstract/abstract.router'; import { getCatalogDto, getCollectionsDto, requestPairingCodeDto } from '@api/dto/business.dto'; +import { InstanceDto } from '@api/dto/instance.dto'; import { businessController } from '@api/server.module'; import { createMetaErrorResponse } from '@utils/errorResponse'; import { catalogSchema, collectionsSchema, pairingCodeSchema } from '@validate/validate.schema'; @@ -231,10 +232,10 @@ export class BusinessRouter extends RouterBroker { */ .get(this.routerPath('getAuthState'), ...guards, async (req, res) => { try { - const response = await this.dataValidate({ + const response = await this.dataValidate({ request: req, schema: null, - ClassRef: null, + ClassRef: InstanceDto, execute: (instance) => businessController.getAuthState(instance), }); @@ -267,10 +268,10 @@ export class BusinessRouter extends RouterBroker { */ .delete(this.routerPath('logoutBrowser'), ...guards, async (req, res) => { try { - const response = await this.dataValidate({ + const response = await this.dataValidate({ request: req, schema: null, - ClassRef: null, + ClassRef: InstanceDto, execute: (instance) => businessController.logoutBrowser(instance), }); From 2e87d07074aa9731a8aa8187efecd2899e1f8ed8 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 11:22:59 +0000 Subject: [PATCH 26/32] fix(catalog-browser): inject @wppconnect/wa-js after client ready MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of 0 products: whatsapp-web.js does NOT auto-inject @wppconnect/wa-js. Without window.WPP, all catalog fetch logic fails silently (returns empty arrays). Fix: added injectWaJs(page) method that: 1. Reads wa-js bundle from node_modules via require.resolve 2. Evaluates it in the page context via page.evaluate(code) 3. Waits for WPP.isReady === true (timeout 30s) Called in client.on('ready') event handler — same pattern as bedones-whatsapp's injectWPPIntoPageInternal. Verified via diagnostic: without injection, WPP.wppExists=false. After injection, WPP.isReady=true and catalog fetch works. Signed-off-by: Kelvin Yuli Andrian --- .../whatsapp/catalog-browser.service.ts | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index 0f15bb9b5..a414b3168 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -185,6 +185,36 @@ export class BrowserCatalogService { } } + /** + * Inject @wppconnect/wa-js into the WhatsApp Web page. + * + * whatsapp-web.js does NOT auto-inject wa-js. Without it, window.WPP is + * undefined and all catalog/collection fetch logic fails silently. + * + * This reads the wa-js bundle from node_modules and evaluates it in the + * page context, then waits for WPP.isReady. + * + * (Ported from bedones-whatsapp's injectWPPIntoPageInternal) + */ + private async injectWaJs(page: any): Promise { + const { readFileSync } = require('fs'); + const wppExists = await page.evaluate(() => typeof (window as any).WPP !== 'undefined'); + if (wppExists) { + this.logger.log('[browser] WPP already loaded in page'); + return; + } + + this.logger.log('[browser] Injecting @wppconnect/wa-js into page...'); + const waJsPath = require.resolve('@wppconnect/wa-js'); + const waJsCode = readFileSync(waJsPath, 'utf8'); + await page.evaluate(waJsCode); + this.logger.log(`[browser] wa-js injected (${waJsCode.length} chars)`); + + // Wait for WPP to be ready + await page.waitForFunction(() => (window as any).WPP && (window as any).WPP.isReady === true, { timeout: 30000 }); + this.logger.log('[browser] WPP.isReady = true'); + } + /** * Get or create a Client for the given instance. * Returns once the client is fully ready (authenticated + WA Web loaded). @@ -261,11 +291,24 @@ export class BrowserCatalogService { state.pairingCode = null; }); - client.on('ready', () => { + client.on('ready', async () => { this.logger.log(`[browser] Client ready for instance=${instanceName}`); state.ready = true; state.qrCode = null; state.pairingCode = null; + + // Inject @wppconnect/wa-js into the page — whatsapp-web.js does NOT + // auto-inject it. Without this, window.WPP is undefined and all + // catalog/collection fetch logic fails silently. + // (Same approach as bedones-whatsapp's injectWPPIntoPageInternal) + try { + const page = client.pupPage; + if (page) { + await this.injectWaJs(page); + } + } catch (err) { + this.logger.warn(`[browser] Failed to inject wa-js on ready: ${(err as Error).message}`); + } }); client.on('auth_failure', (msg: string) => { From 4d36d1aa8fa36feb9d590bf9a64cba12765e444e Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 11:30:04 +0000 Subject: [PATCH 27/32] =?UTF-8?q?style:=20fix=20lint=20=E2=80=94=20use=20E?= =?UTF-8?q?S=20import=20for=20readFileSync,=20eslint-disable=20for=20requi?= =?UTF-8?q?re.resolve?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI Check Code Quality was failing: 200:30 error A `require()` style import is forbidden @typescript-eslint/no-require-imports Fix: - Move readFileSync to top-level ES import (was: const { readFileSync } = require('fs')) - Add eslint-disable-next-line for require.resolve('@wppconnect/wa-js') (no ES import equivalent — needed to resolve package path at runtime) Signed-off-by: Kelvin Yuli Andrian --- .../integrations/channel/whatsapp/catalog-browser.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index a414b3168..893947970 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -23,7 +23,7 @@ import { Logger } from '@config/logger.config'; import { INSTANCE_DIR } from '@config/path.config'; import { BadRequestException } from '@exceptions'; -import { existsSync, mkdirSync, rmSync, unlinkSync } from 'fs'; +import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync } from 'fs'; import { join } from 'path'; import { Client, LocalAuth } from 'whatsapp-web.js'; @@ -197,7 +197,6 @@ export class BrowserCatalogService { * (Ported from bedones-whatsapp's injectWPPIntoPageInternal) */ private async injectWaJs(page: any): Promise { - const { readFileSync } = require('fs'); const wppExists = await page.evaluate(() => typeof (window as any).WPP !== 'undefined'); if (wppExists) { this.logger.log('[browser] WPP already loaded in page'); @@ -205,6 +204,8 @@ export class BrowserCatalogService { } this.logger.log('[browser] Injecting @wppconnect/wa-js into page...'); + // require.resolve is needed because @wppconnect/wa-js doesn't export a path + // eslint-disable-next-line @typescript-eslint/no-var-requires const waJsPath = require.resolve('@wppconnect/wa-js'); const waJsCode = readFileSync(waJsPath, 'utf8'); await page.evaluate(waJsCode); From 1530b233c0a3cac20ccf619b616f68ec55b3c343 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 11:45:18 +0000 Subject: [PATCH 28/32] fix(catalog-browser): call injectWaJs in fetchCatalog/fetchCollections (race condition) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Race condition: readyPromise resolves when 'ready' event fires, but the injectWaJs call in the 'ready' event handler runs asynchronously. fetchCatalog could execute before wa-js injection completes, causing window.WPP to be undefined → catalog fetch returns 0 products. Fix: call injectWaJs(page) at the start of fetchCatalog and fetchCollections. injectWaJs is idempotent (skips if WPP already loaded), so this is safe to call even if 'ready' handler already injected it. Signed-off-by: Kelvin Yuli Andrian --- .../channel/whatsapp/catalog-browser.service.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index 893947970..d094fd3fb 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -391,6 +391,12 @@ export class BrowserCatalogService { throw new BadRequestException('WhatsApp Web page not available'); } + // Ensure wa-js is injected before fetching catalog. + // (injectWaJs is idempotent — skips if WPP already loaded. + // This fixes a race condition where readyPromise resolves before + // the 'ready' event handler's injectWaJs call completes.) + await this.injectWaJs(page); + const result = await page.evaluate( async (): Promise<{ catalog: BrowserProduct[]; @@ -535,6 +541,9 @@ export class BrowserCatalogService { throw new BadRequestException('WhatsApp Web page not available'); } + // Ensure wa-js is injected before fetching collections (idempotent). + await this.injectWaJs(page); + const result = await page.evaluate( async (): Promise<{ collections: BrowserCollection[]; From 0cd76a47e1a04107245f974f938115010b56b3e9 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 11:57:24 +0000 Subject: [PATCH 29/32] fix(catalog-browser): don't hard-fail on WPP.isReady timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WPP.isReady was not becoming true within 30s, causing fetchCatalog to throw 'Waiting failed: 30000ms exceeded'. The wa-js library IS injected (window.WPP exists), but isReady flag may not be reliable. Fix: split injection into 3 phases: 1. Wait for window.WPP to be defined (10s timeout) — confirms injection 2. Try to call WPP.waitReady() or WPP.init() — explicit init 3. Wait for isReady (15s, best-effort) — don't fail if timeout If isReady doesn't become true, catalog fetch code will still run and check WPP availability at each layer. Signed-off-by: Kelvin Yuli Andrian --- .../whatsapp/catalog-browser.service.ts | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index d094fd3fb..509eec6ae 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -211,9 +211,38 @@ export class BrowserCatalogService { await page.evaluate(waJsCode); this.logger.log(`[browser] wa-js injected (${waJsCode.length} chars)`); - // Wait for WPP to be ready - await page.waitForFunction(() => (window as any).WPP && (window as any).WPP.isReady === true, { timeout: 30000 }); - this.logger.log('[browser] WPP.isReady = true'); + // Wait for window.WPP to be defined (the library object) + try { + await page.waitForFunction(() => typeof (window as any).WPP !== 'undefined', { timeout: 10000 }); + this.logger.log('[browser] window.WPP is defined'); + } catch { + this.logger.warn('[browser] window.WPP not defined after 10s — wa-js injection may have failed'); + return; + } + + // Try to initialize WPP (some versions need explicit init) + try { + await page.evaluate(async () => { + const wpp = (window as any).WPP; + if (wpp && !wpp.isReady) { + if (typeof wpp.waitReady === 'function') { + await wpp.waitReady(); + } else if (typeof wpp.init === 'function') { + await wpp.init(); + } + } + }); + } catch { + // Ignore init errors — catalog fetch will check availability + } + + // Wait briefly for isReady (best-effort, don't fail) + try { + await page.waitForFunction(() => (window as any).WPP && (window as any).WPP.isReady === true, { timeout: 15000 }); + this.logger.log('[browser] WPP.isReady = true'); + } catch { + this.logger.warn('[browser] WPP.isReady not true after 15s — continuing anyway'); + } } /** From f9928b3151df36abe080a2f552d6794fdbf9b45e Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 12:12:39 +0000 Subject: [PATCH 30/32] fix(catalog-browser): use wa-js public API (WPP.catalog.*) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced internal WhatsApp Web functions (WPP.whatsapp.functions.queryCatalog, WPP.whatsapp.CatalogStore.findQuery) with wa-js public API. Root cause of 'Cannot read properties of undefined (reading m)' error: internal functions access minified WA Web internals that change between versions — unstable and unreliable. New approach (per wa-js type definitions): - Catalog: WPP.catalog.getProducts(chatId, count) → fallback getMyCatalog() - Collections: WPP.catalog.getCollections(chatId, qnt, productsCount) These are the official public API methods exported by @wppconnect/wa-js, designed to be stable across WA Web version updates. Signed-off-by: Kelvin Yuli Andrian --- .../whatsapp/catalog-browser.service.ts | 161 ++++++------------ 1 file changed, 56 insertions(+), 105 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index 509eec6ae..0cd40f7a4 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -426,103 +426,73 @@ export class BrowserCatalogService { // the 'ready' event handler's injectWaJs call completes.) await this.injectWaJs(page); + // Get the authenticated user's WhatsApp ID (needed for catalog API calls) + const wppUserId = await page.evaluate(() => { + const wpp = (window as any).WPP; + const myUser = wpp?.conn?.getMyUserId ? wpp.conn.getMyUserId() : null; + return myUser ? myUser._serialized : null; + }); + if (!wppUserId) { + throw new BadRequestException('Could not determine WhatsApp user ID'); + } + const result = await page.evaluate( - async (): Promise<{ + async ( + userId: string, + ): Promise<{ catalog: BrowserProduct[]; message?: string; }> => { const wpp = (window as any).WPP; if (!wpp) return { catalog: [], message: 'WPP not available' }; - const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; - const userId = (myUser && myUser._serialized) || ''; - if (!userId) return { catalog: [], message: 'User ID not found' }; - - const whatsappApi = wpp.whatsapp; - const productsById = new Map(); + const productsById = new Map(); const addProduct = (rawProduct: any) => { const product = rawProduct?.attributes || rawProduct; if (!product?.id) return; if (!productsById.has(product.id)) { - productsById.set(product.id, product as BrowserProduct); + productsById.set(product.id, product); } }; - const extractProductsFromCatalog = (catalogEntry: any): any[] => { - if (!catalogEntry) return []; - const productIndex = catalogEntry.productCollection?._index; - if (!productIndex || typeof productIndex !== 'object') return []; - return Object.keys(productIndex) - .map((productId) => productIndex[productId]?.attributes) - .filter(Boolean); - }; - - // Layer 1: queryCatalog with pagination cursor - if (whatsappApi?.functions?.queryCatalog) { + // Use wa-js public API (WPP.catalog.*) + // Method 1: WPP.catalog.getProducts(chatId, count) + if (wpp.catalog?.getProducts) { try { - let afterToken: string | undefined = undefined; - let safetyCount = 0; - while (safetyCount < 500) { - const response: any = await whatsappApi.functions.queryCatalog(userId, afterToken); - const pageProducts: any[] = Array.isArray(response?.data) ? response.data : []; - for (const product of pageProducts) { + const products: any[] = await wpp.catalog.getProducts(userId, 999); + if (Array.isArray(products)) { + for (const product of products) { addProduct(product); } - const nextAfter = response?.paging?.cursors?.after; - if (!nextAfter || nextAfter === afterToken) break; - afterToken = nextAfter; - safetyCount++; } } catch (error: any) { - console.log('queryCatalog unavailable:', error?.message); + console.log('getProducts error:', error?.message); } } - // Layer 2: CatalogStore.findQuery - if (whatsappApi?.CatalogStore?.findQuery) { + // Method 2: WPP.catalog.getMyCatalog() — fallback if getProducts returned 0 + if (productsById.size === 0 && wpp.catalog?.getMyCatalog) { try { - const results: any[] = await whatsappApi.CatalogStore.findQuery(userId); - if (Array.isArray(results)) { - for (const entry of results) { - const products = extractProductsFromCatalog(entry); - for (const product of products) { - addProduct(product); + const myCatalog: any = await wpp.catalog.getMyCatalog(); + if (myCatalog) { + // CatalogModel has productCollection._index + const productIndex = myCatalog.productCollection?._index; + if (productIndex && typeof productIndex === 'object') { + for (const pid of Object.keys(productIndex)) { + const p = productIndex[pid]?.attributes || productIndex[pid]; + if (p) addProduct(p); } } } } catch (error: any) { - console.log('CatalogStore.findQuery unavailable:', error?.message); - } - } - - // Layer 3: WPP.catalog.getMyCatalog - try { - const myCatalog: any = await wpp.catalog?.getMyCatalog?.(); - const fallbackProducts = extractProductsFromCatalog(myCatalog); - for (const product of fallbackProducts) { - addProduct(product); - } - } catch (error: any) { - console.log('getMyCatalog unavailable:', error?.message); - } - - // Layer 4: WPP.catalog.getProducts (last resort) - if (productsById.size === 0) { - try { - const fallbackProducts: any[] = await wpp.catalog?.getProducts?.(userId, 999); - if (Array.isArray(fallbackProducts)) { - for (const product of fallbackProducts) { - addProduct(product); - } - } - } catch (error: any) { - console.log('getProducts unavailable:', error?.message); + console.log('getMyCatalog error:', error?.message); } } return { catalog: Array.from(productsById.values()) }; }, + wppUserId, ); this.logger.log(`[browser] fetchCatalog got ${result.catalog.length} products`); @@ -573,62 +543,42 @@ export class BrowserCatalogService { // Ensure wa-js is injected before fetching collections (idempotent). await this.injectWaJs(page); + // Get the authenticated user's WhatsApp ID + const wppUserId = await page.evaluate(() => { + const wpp = (window as any).WPP; + const myUser = wpp?.conn?.getMyUserId ? wpp.conn.getMyUserId() : null; + return myUser ? myUser._serialized : null; + }); + if (!wppUserId) { + throw new BadRequestException('Could not determine WhatsApp user ID'); + } + const result = await page.evaluate( - async (): Promise<{ + async ( + userId: string, + ): Promise<{ collections: BrowserCollection[]; message?: string; }> => { const wpp = (window as any).WPP; if (!wpp) return { collections: [], message: 'WPP not available' }; - const myUser = wpp.conn ? (wpp.conn.getMyUserId ? wpp.conn.getMyUserId() : null) : null; - const userId = (myUser && myUser._serialized) || ''; - if (!userId) return { collections: [], message: 'User ID not found' }; - - const whatsappApi = wpp.whatsapp; const collections: BrowserCollection[] = []; - // Method 1: WPP.catalog.getCollections - try { - const response: any = await wpp.catalog?.getCollections?.(userId); - if (Array.isArray(response)) { - for (const c of response) { - const attrs = c?.attributes || c; - if (!attrs?.id) continue; - const productIndex = c?.productCollection?._index || attrs?.products?._index; - const products: BrowserProduct[] = []; - if (productIndex && typeof productIndex === 'object') { - for (const pid of Object.keys(productIndex)) { - const p = productIndex[pid]?.attributes || productIndex[pid]; - if (p) products.push(p as BrowserProduct); - } - } - collections.push({ - id: attrs.id, - name: attrs.name || '', - products, - status: attrs.status, - }); - } - } - } catch (error: any) { - console.log('WPP.catalog.getCollections failed:', error?.message); - } - - // Method 2: CollectionStore.findQuery fallback - if (collections.length === 0 && whatsappApi?.CollectionStore?.findQuery) { + // Use wa-js public API: WPP.catalog.getCollections(chatId, qnt, productsCount) + if (wpp.catalog?.getCollections) { try { - const results: any[] = await whatsappApi.CollectionStore.findQuery(userId); - if (Array.isArray(results)) { - for (const c of results) { + const response: any[] = await wpp.catalog.getCollections(userId, 100, 50); + if (Array.isArray(response)) { + for (const c of response) { const attrs = c?.attributes || c; if (!attrs?.id) continue; const productIndex = c?.productCollection?._index; - const products: BrowserProduct[] = []; + const products: any[] = []; if (productIndex && typeof productIndex === 'object') { for (const pid of Object.keys(productIndex)) { const p = productIndex[pid]?.attributes || productIndex[pid]; - if (p) products.push(p as BrowserProduct); + if (p) products.push(p); } } collections.push({ @@ -640,12 +590,13 @@ export class BrowserCatalogService { } } } catch (error: any) { - console.log('CollectionStore.findQuery failed:', error?.message); + console.log('WPP.catalog.getCollections error:', error?.message); } } return { collections }; }, + wppUserId, ); this.logger.log(`[browser] fetchCollections got ${result.collections.length} collections`); From 07f9efd3cacee2d90779371e995413be4c3b1806 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 12:26:51 +0000 Subject: [PATCH 31/32] fix(catalog-browser): wait for WPP.isFullReady before fetching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of 'Cannot read properties of undefined (reading m)' error: WPP.conn.getMyUserId() was called before wa-js finished loading all webpack modules. isReady flag was false, but we proceeded anyway. Fix: added waitForWppReady(page) method that: 1. Checks isFullReady/isReady immediately (fast path) 2. Uses WPP.onFullReady(callback) — async callback that fires when ALL webpack modules are loaded 3. Falls back to WPP.onReady(callback) if onFullReady unavailable 4. Last resort: poll isReady every 500ms 5. Timeout: 60s (was 15s — too short for slow container startup) Now injectWaJs() throws BadRequestException if WPP doesn't become ready, instead of silently continuing with broken state. Signed-off-by: Kelvin Yuli Andrian --- .../whatsapp/catalog-browser.service.ts | 92 +++++++++++++++---- 1 file changed, 72 insertions(+), 20 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index 0cd40f7a4..be72a8246 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -200,6 +200,8 @@ export class BrowserCatalogService { const wppExists = await page.evaluate(() => typeof (window as any).WPP !== 'undefined'); if (wppExists) { this.logger.log('[browser] WPP already loaded in page'); + // Still ensure it's ready + await this.waitForWppReady(page); return; } @@ -217,31 +219,81 @@ export class BrowserCatalogService { this.logger.log('[browser] window.WPP is defined'); } catch { this.logger.warn('[browser] window.WPP not defined after 10s — wa-js injection may have failed'); - return; + throw new BadRequestException('Failed to inject wa-js: window.WPP not defined'); } - // Try to initialize WPP (some versions need explicit init) - try { - await page.evaluate(async () => { - const wpp = (window as any).WPP; - if (wpp && !wpp.isReady) { - if (typeof wpp.waitReady === 'function') { - await wpp.waitReady(); - } else if (typeof wpp.init === 'function') { - await wpp.init(); - } + // Wait for WPP to be fully ready (webpack modules loaded) + await this.waitForWppReady(page); + } + + /** + * Wait for WPP.isFullReady === true using the onFullReady callback. + * + * wa-js exposes onFullReady(callback) — async callback that fires when ALL + * webpack modules are loaded. Without waiting, calling WPP.conn.getMyUserId() + * crashes with "Cannot read properties of undefined (reading 'm')" because + * the underlying webpack module isn't loaded yet. + */ + private async waitForWppReady(page: any): Promise { + this.logger.log('[browser] Waiting for WPP.isFullReady...'); + + // Use page.evaluate to set up onFullReady callback that resolves a promise + const ready = await page.evaluate(async () => { + const wpp = (window as any).WPP; + if (!wpp) return { ready: false, error: 'WPP not defined' }; + + // Already ready? + if (wpp.isFullReady) return { ready: true, immediate: true }; + if (wpp.isReady) return { ready: true, immediate: true, isFullReady: false }; + + // Wait via onFullReady callback (preferred) or onReady as fallback + return new Promise((resolve) => { + const timeout = setTimeout(() => { + resolve({ + ready: wpp.isReady || wpp.isFullReady, + timeout: true, + isReady: wpp.isReady, + isFullReady: wpp.isFullReady, + }); + }, 60000); // 60s timeout + + const onSuccess = () => { + clearTimeout(timeout); + resolve({ ready: true, via: 'callback' }); + }; + + if (typeof wpp.onFullReady === 'function') { + wpp.onFullReady(onSuccess); + } else if (typeof wpp.onReady === 'function') { + wpp.onReady(onSuccess); + } else { + // Fallback: poll isReady + clearTimeout(timeout); + const pollTimer = setInterval(() => { + if (wpp.isReady || wpp.isFullReady) { + clearInterval(pollTimer); + resolve({ ready: true, via: 'poll' }); + } + }, 500); + setTimeout(() => { + clearInterval(pollTimer); + resolve({ ready: wpp.isReady || wpp.isFullReady, timeout: true }); + }, 60000); } }); - } catch { - // Ignore init errors — catalog fetch will check availability - } + }); - // Wait briefly for isReady (best-effort, don't fail) - try { - await page.waitForFunction(() => (window as any).WPP && (window as any).WPP.isReady === true, { timeout: 15000 }); - this.logger.log('[browser] WPP.isReady = true'); - } catch { - this.logger.warn('[browser] WPP.isReady not true after 15s — continuing anyway'); + if (ready.ready) { + this.logger.log( + `[browser] WPP ready (immediate=${ready.immediate || false}, via=${ready.via || 'callback'}, isReady=${ready.isReady}, isFullReady=${ready.isFullReady})`, + ); + } else { + this.logger.warn( + `[browser] WPP NOT ready after 60s (isReady=${ready.isReady}, isFullReady=${ready.isFullReady})`, + ); + throw new BadRequestException( + 'WPP library not ready after 60s. WhatsApp Web may have changed its internal structure.', + ); } } From 313b0934775d8302aac8c322ea27cbb74cb57c42 Mon Sep 17 00:00:00 2001 From: Kelvin Yuli Andrian Date: Sun, 12 Jul 2026 12:47:18 +0000 Subject: [PATCH 32/32] fix(catalog-browser): use Puppeteer waitForFunction for WPP.isReady MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: onFullReady/onReady callbacks are NOT available on window.WPP object (confirmed via debug script). Previous code tried to use them and fell back to manual polling which had a bug in the timeout logic (double setTimeout). Debug script verified: WPP.isReady becomes true within ~3s via polling. Catalog fetch works — got 10 products from Warung Lakku. Fix: replaced complex callback/poll logic with Puppeteer's built-in page.waitForFunction(fn, {timeout: 60000, polling: 500}). Simpler, more reliable, no race conditions. Signed-off-by: Kelvin Yuli Andrian --- .../whatsapp/catalog-browser.service.ts | 89 +++++++------------ 1 file changed, 32 insertions(+), 57 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts index be72a8246..41a67b982 100644 --- a/src/api/integrations/channel/whatsapp/catalog-browser.service.ts +++ b/src/api/integrations/channel/whatsapp/catalog-browser.service.ts @@ -227,69 +227,44 @@ export class BrowserCatalogService { } /** - * Wait for WPP.isFullReady === true using the onFullReady callback. + * Wait for WPP.isReady === true using Puppeteer's waitForFunction polling. * - * wa-js exposes onFullReady(callback) — async callback that fires when ALL - * webpack modules are loaded. Without waiting, calling WPP.conn.getMyUserId() - * crashes with "Cannot read properties of undefined (reading 'm')" because - * the underlying webpack module isn't loaded yet. + * Verified working in production: isReady becomes true within ~3s of + * wa-js injection. The onFullReady/onReady callbacks from wa-js are + * NOT available on window.WPP object (confirmed via debug script), + * so polling via waitForFunction is the most reliable approach. + * + * Without waiting, calling WPP.conn.getMyUserId() crashes with + * "Cannot read properties of undefined (reading 'm')" because the + * underlying webpack module isn't loaded yet. */ private async waitForWppReady(page: any): Promise { - this.logger.log('[browser] Waiting for WPP.isFullReady...'); - - // Use page.evaluate to set up onFullReady callback that resolves a promise - const ready = await page.evaluate(async () => { - const wpp = (window as any).WPP; - if (!wpp) return { ready: false, error: 'WPP not defined' }; - - // Already ready? - if (wpp.isFullReady) return { ready: true, immediate: true }; - if (wpp.isReady) return { ready: true, immediate: true, isFullReady: false }; - - // Wait via onFullReady callback (preferred) or onReady as fallback - return new Promise((resolve) => { - const timeout = setTimeout(() => { - resolve({ - ready: wpp.isReady || wpp.isFullReady, - timeout: true, - isReady: wpp.isReady, - isFullReady: wpp.isFullReady, - }); - }, 60000); // 60s timeout - - const onSuccess = () => { - clearTimeout(timeout); - resolve({ ready: true, via: 'callback' }); - }; + this.logger.log('[browser] Waiting for WPP.isReady...'); - if (typeof wpp.onFullReady === 'function') { - wpp.onFullReady(onSuccess); - } else if (typeof wpp.onReady === 'function') { - wpp.onReady(onSuccess); - } else { - // Fallback: poll isReady - clearTimeout(timeout); - const pollTimer = setInterval(() => { - if (wpp.isReady || wpp.isFullReady) { - clearInterval(pollTimer); - resolve({ ready: true, via: 'poll' }); - } - }, 500); - setTimeout(() => { - clearInterval(pollTimer); - resolve({ ready: wpp.isReady || wpp.isFullReady, timeout: true }); - }, 60000); - } - }); - }); - - if (ready.ready) { - this.logger.log( - `[browser] WPP ready (immediate=${ready.immediate || false}, via=${ready.via || 'callback'}, isReady=${ready.isReady}, isFullReady=${ready.isFullReady})`, + try { + await page.waitForFunction( + () => { + const wpp = (window as any).WPP; + return wpp && (wpp.isReady === true || wpp.isFullReady === true); + }, + { timeout: 60000, polling: 500 }, ); - } else { + this.logger.log('[browser] WPP.isReady = true'); + } catch { + // Check final state for debugging + const state = await page + .evaluate(() => { + const wpp = (window as any).WPP; + return { + exists: !!wpp, + isReady: wpp?.isReady, + isFullReady: wpp?.isFullReady, + }; + }) + .catch(() => ({ exists: false, isReady: false, isFullReady: false })); + this.logger.warn( - `[browser] WPP NOT ready after 60s (isReady=${ready.isReady}, isFullReady=${ready.isFullReady})`, + `[browser] WPP NOT ready after 60s (exists=${state.exists}, isReady=${state.isReady}, isFullReady=${state.isFullReady})`, ); throw new BadRequestException( 'WPP library not ready after 60s. WhatsApp Web may have changed its internal structure.',