diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9512bdd --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +NODE_ENV=development +HOST=0.0.0.0 +PORT=3000 +API_KEY=berryapi_dev_key +LOG_LEVEL=info +DATABASE_URL=file:./berryapi.sqlite +BERRY_SQLITE_PATH=berrysdk.db +BERRY_AUTH_FOLDER=.berry-sessions +BERRY_WEBHOOK_TIMEOUT_MS=5000 +BERRY_WS_PATH=/ws diff --git a/.gitignore b/.gitignore index 59fe357..82da2f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,20 @@ -node_modules/ -dist/ +node_modules +dist .env -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* +*.log +.berry-dashboard.pid +*.tgz +coverage +packages/*/dist +packages/*/.tsbuildinfo +.baileys-sessions +.berry-sessions +.vscode +berryprotocol.db +berrysdk.db +berryapi.sqlite +packages/berryapi/*.sqlite +_reference_baileys2 +_vendor_whatsapp_proto +_vendor_wppconnect_wa_proto +Documentacao/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c6d40e1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:22-bookworm-slim + +WORKDIR /app + +COPY package.json tsconfig.json ./ +COPY packages ./packages +COPY .env.example ./ + +RUN npm install +RUN npm run build + +EXPOSE 3000 + +ENV HOST=0.0.0.0 +ENV PORT=3000 +ENV API_KEY=berryapi_dev_key +ENV DATABASE_URL=file:/data/berryapi.sqlite +ENV BERRY_AUTH_FOLDER=/data/auth +ENV BERRY_SQLITE_PATH=/data/berrysdk.db + +CMD ["node", "packages/berryapi/dist/server.js"] diff --git a/README.md b/README.md index 4a70409..561ef86 100644 --- a/README.md +++ b/README.md @@ -1,396 +1,230 @@ -

- BerryProtocol -

- -

BerryProtocol

- -

- Native WhatsApp interactive messaging SDK for TypeScript. -

- -

- Build modern WhatsApp experiences with native lists, buttons, carousels, - OTP flows, realtime events, and multi-session automation. -

- -

- - npm version - - - - npm downloads - - - - github repo - - - node version - - typescript - - focus -

- ---- - -## Why BerryProtocol - -BerryProtocol is a modern developer-first SDK built for creating rich WhatsApp experiences using TypeScript. - -Instead of focusing only on low-level protocol internals, BerryProtocol focuses on: - -- interactive messaging -- realtime communication -- multi-session scalability -- developer experience -- automation workflows -- clean npm integration - -The project is part of the BerrySDK ecosystem and powers modern WhatsApp automation flows with a simple and scalable API. - ---- - -# Features - -## Native Interactive Messages - -BerryProtocol includes native support for: - -- Lists -- Reply Buttons -- CTA Buttons -- Copy Buttons -- OTP Messages -- Carousels -- Polls -- Reactions -- Presence -- Rich Media -- Realtime Events - ---- - -## Multi-Session Architecture - -Designed for scalable applications. - -- Multiple WhatsApp sessions -- Independent auth states -- QR authentication -- Pairing code flows -- Session recovery -- Automatic reconnects - -Perfect for: - -- SaaS platforms -- automation systems -- support tools -- chatbot platforms -- WhatsApp integrations -- API services - ---- - -## Developer Experience - -BerryProtocol was designed to feel simple, modern, and production-ready. - -### Highlights - -- TypeScript-first API -- ESM-first package -- clean public exports -- grouped SDK modules -- strongly typed events -- lightweight integration flow -- npm-first distribution - ---- - -# Installation - -```bash -npm install berryprotocol -```` - ---- - -# Quick Start - -```ts -import BerryProtocol, { makeLogger } from "berryprotocol"; - -const client = new BerryProtocol({ - sessionId: "default", - logger: makeLogger(), - reconnectDelayMs: 1500, - reconnectMaxAttempts: 12, - printQrInTerminal: true, -}); - -client.on("auth.qr", ({ value }) => { - console.log("qr", value); -}); - -client.on("connection.open", () => { - console.log("connected"); -}); - -client.on("message.received", (message) => { - console.log("incoming", { - from: message.from, - type: message.type, - }); -}); - -await client.connectWithQr(); +# BerryAPI + +BerryAPI is a REST API and realtime platform for WhatsApp Web automation built by BerrySDK on top of BerryProtocol. + +It is designed for: +- automation tools like `n8n` +- chat builders like `Typebot` +- internal SaaS panels +- webhook-first WhatsApp integrations + +BerryAPI ships with: +- multi-instance session management +- API key authentication +- Swagger/OpenAPI docs at `/docs` +- realtime event streaming over WebSocket +- webhook dispatch per instance +- modern WhatsApp message endpoints +- SQLite by default, prepared for future PostgreSQL migration + +## Stack + +- Node.js +- TypeScript +- Fastify +- Drizzle ORM +- better-sqlite3 +- Zod +- Pino +- Swagger/OpenAPI +- WebSocket + +## Project Structure + +```text +src/ + app.ts + server.ts + config/ + database/ + docs/ + managers/ + middlewares/ + modules/ + chat/ + group/ + instance/ + message/ + profile/ + settings/ + webhook/ + providers/ + whatsapp/ + realtime/ + types/ + utils/ + webhook/ ``` ---- - -# Interactive Message Examples - -## Reply Buttons - -```ts -await client.sendButtons(chatId, { - text: "Choose an option", - buttons: [ - { - id: "buy", - text: "Buy now", - }, - { - id: "support", - text: "Support", - }, - ], -}); -``` +## Install ---- - -## Lists - -```ts -await client.sendList(chatId, { - title: "Menu", - buttonText: "Open", - sections: [ - { - title: "Pizzas", - rows: [ - { - id: "calabresa", - title: "Pizza Calabresa", - }, - ], - }, - ], -}); +```bash +npm install ``` ---- +## Run In Development -## Copy Buttons - -```ts -await client.sendCopyButton(chatId, { - text: "Your verification code", - code: "458921", -}); -``` - ---- - -## Carousel Messages - -```ts -await client.sendCarousel(chatId, { - text: "Featured products", - cards: [ - { - title: "Berry Burger", - body: "Special burger", - footer: "BerryProtocol", - }, - ], -}); +```bash +cp .env.example .env +npm run dev ``` ---- - -# Realtime Events +BerryAPI starts on: -BerryProtocol exposes typed realtime events for modern automation systems. +- `GET http://localhost:3000` +- `GET http://localhost:3000/info` +- `GET http://localhost:3000/docs` +- `GET http://localhost:3000/studio` -```ts -client.on("message.received", console.log); +## Berry Studio -client.on("message.updated", console.log); +Berry Studio is the shadcn/ui dashboard bundled with BerryAPI. It includes: -client.on("message.reaction", console.log); +- visual flow editing with React Flow +- draft saving, validation and publishing +- automatic WhatsApp triggers by instance and keyword +- automatic continuation when a contact replies to a waiting flow +- simulation without sending messages +- real execution through a connected BerryProtocol instance +- QR code and pairing-code connection management +- nested `{{variable}}` interpolation in buttons, lists, carousels and JSON payloads +- analytics, run logs and stored flow versions +- all 34 message capabilities currently exposed by BerryAPI -client.on("presence.update", console.log); +Build the frontend before starting the production API: -client.on("connection.update", console.log); +```bash +npm run build --workspace studio +npm run build --workspace berryapi +npm run start --workspace berryapi ``` ---- - -# Media Support - -Supported media flows include: +Open `http://localhost:3000/studio` and enter the configured `API_KEY`. +Static Studio pages are public so the login screen can load, while +`/studio/api/*` remains protected by bearer authentication. -* images -* videos -* audio -* voice notes -* stickers -* documents -* GIFs +## Authentication -Example: +Use an API key with every protected request: -```ts -await client.sendImage(chatId, { - url: "./image.png", - caption: "BerryProtocol", -}); +```http +Authorization: Bearer berryapi_dev_key ``` ---- - -# Ecosystem - -BerryProtocol is part of the BerrySDK ecosystem. - -## Packages - -* `berryprotocol` -* `berryotp` -* `berryapi` - -## Future Tools - -* BerryStudio -* visual message builder -* realtime flow editor -* webhook inspector -* automation designer - ---- +Change the key in `.env`. -# Repository Structure +## Create An Instance -```txt -src/ - ├── Auth/ - ├── Defaults/ - ├── Media/ - ├── Messages/ - ├── Socket/ - ├── Store/ - ├── Types/ - ├── Utils/ - └── index.ts +```bash +curl -X POST http://localhost:3000/instance/create \ + -H "Authorization: Bearer berryapi_dev_key" \ + -H "Content-Type: application/json" \ + -d '{ + "instanceName": "store-01", + "authMethod": "qr" + }' ``` ---- - -# Requirements - -* Node.js >= 20 -* npm - ---- - -# Useful Scripts +## Connect With QR Code ```bash -npm run build -npm run clean -npm run prepublishOnly +curl -X GET http://localhost:3000/instance/connect/store-01 \ + -H "Authorization: Bearer berryapi_dev_key" ``` ---- - -# Versioning - -BerryProtocol uses manual semantic versioning. - -Recommended release flow: +Then fetch the connection state: ```bash -npm version patch -git push origin main --follow-tags +curl -X GET http://localhost:3000/instance/connectionState/store-01 \ + -H "Authorization: Bearer berryapi_dev_key" ``` ---- - -# Contributing +The response contains `qrCode` or `pairingCode` when needed. -Before opening a PR: +## Send A Message -* run `npm install` -* run `npm run build` -* keep typings stable -* avoid breaking public exports -* keep documentation updated -* validate interactive message flows - ---- - -# Roadmap +```bash +curl -X POST http://localhost:3000/message/sendText/store-01 \ + -H "Authorization: Bearer berryapi_dev_key" \ + -H "Content-Type: application/json" \ + -d '{ + "to": "5511999999999@s.whatsapp.net", + "text": "Hello from BerryAPI" + }' +``` -* Native Flow Messages -* WhatsApp Forms -* Advanced Carousel Builder -* Better Media Pipeline -* Embedded AI Helpers -* BerryStudio integration -* Flow visual editor -* Webhook replay tools -* Session dashboard +## Configure A Webhook ---- +```bash +curl -X POST http://localhost:3000/webhook/set/store-01 \ + -H "Authorization: Bearer berryapi_dev_key" \ + -H "Content-Type: application/json" \ + -d '{ + "enabled": true, + "url": "https://example.com/webhooks/berryapi", + "events": ["connection.update", "messages.upsert", "send.message"] + }' +``` -# Support +## Realtime WebSocket -If BerryProtocol helps your project: +Connect to: -* star the repository -* contribute examples -* open issues -* share integrations -* help improve documentation +```text +ws://localhost:3000/ws?apiKey=berryapi_dev_key +``` ---- +Or scope to one instance: -# Contact +```text +ws://localhost:3000/ws?apiKey=berryapi_dev_key&instanceName=store-01 +``` -Need help with BerryProtocol? +Realtime events include: +- `connection.update` +- `qrcode.updated` +- `messages.upsert` +- `messages.update` +- `messages.delete` +- `chats.update` +- `contacts.update` +- `groups.update` +- `presence.update` +- `send.message` -### Email +## Docker -📧 **berrysdk@gmail.com** +```bash +docker compose up --build +``` -### Discord +The API will be available on `http://localhost:3000`. -💬 **ferronatin** +## Message Endpoints -Feel free to contact us for support, bug reports, feature requests, or partnership inquiries. +BerryAPI includes modern WhatsApp Web message support such as: ---- +- text and extended text +- reply, forward, delete, edit, react +- image, video, audio, document, sticker, gif +- buttons, template buttons, CTA buttons, copy buttons +- carousel, lists and polls +- contacts and locations +- status and view once media +- product, catalog and collection payloads +- AI labeled text and carousel routes -# Disclaimer +Dedicated modern routes include: -BerryProtocol is an independent engineering project for interoperability and automation purposes. +- `POST /message/sendCarousel/:instanceName` +- `POST /message/sendTemplateButtons/:instanceName` +- `POST /message/sendCopyButton/:instanceName` +- `POST /message/sendAiText/:instanceName` +- `POST /message/sendAiCarousel/:instanceName` -It is not affiliated with or endorsed by WhatsApp. +See Swagger docs at `/docs` for request bodies and examples. +## Notes ---- +- Some chat/profile/group operations are registered already but return `501` until BerryProtocol exposes the required runtime hooks. +- The API architecture isolates WhatsApp access behind `WhatsAppProvider`, so BerryProtocol can be swapped later without rewriting controllers. diff --git a/data/00-overview.md b/data/00-overview.md new file mode 100644 index 0000000..258e8ca --- /dev/null +++ b/data/00-overview.md @@ -0,0 +1,84 @@ +# BerryProtocol Overview + +## Purpose + +BerryProtocol is a WhatsApp Web automation SDK and protocol-oriented ecosystem focused on modern messaging, session lifecycle management, and agent-driven commercial automation. + +It is designed for developers who need: + +- multi-session connectivity +- message sending across different message types +- reconnection and QR-based authentication flows +- support for interactive WhatsApp experiences +- AI-assisted or AI-driven business workflows + +## Confirmed + +The current codebase confirms support for: + +- session-based clients through `BerryProtocol` and `BerryClient` +- QR connection +- link connection +- pairing code connection +- text messages +- image messages +- audio messages +- document messages +- lists +- buttons +- carousel messages +- reactions +- location messages +- contacts +- message editing +- presence updates +- group fetch operations +- event-driven client listeners + +## When to use + +Use BerryProtocol when you want to build: + +- support bots +- notification systems +- lead qualification flows +- AI customer service agents +- OTP verification flows with BerryOTP +- event-based integrations with an API layer such as BerryAPI + +## Practical example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "store-main", +}); + +client.on("connection.open", () => { + console.log("BerryProtocol is connected"); +}); + +await client.connectWithQr(); +``` + +## Best practices + +- treat `sessionId` as a durable identity +- persist session-related data in a stable location +- centralize message sending through your own service layer +- keep observability around connection events and send failures +- isolate AI orchestration from transport concerns + +## Common mistakes + +- assuming any string is a valid WhatsApp JID +- mixing transport code with business logic +- rebuilding a new client for every message +- ignoring reconnect and auth lifecycle events + +## Important notes + +- BerryProtocol is best viewed as the messaging runtime layer +- BerryOTP and BerryAPI extend the ecosystem for authentication and external integrations +- BerryAgent should consume this documentation as a foundation for structured answers and retrieval diff --git a/data/01-installation.md b/data/01-installation.md new file mode 100644 index 0000000..71d54fa --- /dev/null +++ b/data/01-installation.md @@ -0,0 +1,75 @@ +# Installation + +## Goal + +This file explains how to install BerryProtocol and prepare a modern Node.js environment for local development and production-style testing. + +## Confirmed + +The repository currently uses: + +- Node.js modern ESM +- npm workspaces in the monorepo +- `berryprotocol` as the public package entrypoint + +## When to use + +Consult this file when you need: + +- first-time setup +- local workspace bootstrapping +- npm-based installation guidance +- environment compatibility notes + +## Install from npm + +```bash +npm install berryprotocol +``` + +## Install in the monorepo + +```bash +npm install +``` + +## Basic environment expectations + +- Node.js 20 or newer is strongly recommended +- ESM imports should be used +- a persistent filesystem path should be available for auth/session storage + +## Practical example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "install-check", +}); + +console.log(typeof client.connectWithQr === "function"); +``` + +## Best practices + +- use exact version pinning for production until APIs stabilize +- commit a `.env.example`, not a real `.env` +- define a dedicated session storage folder for each environment + +## Common mistakes + +- using CommonJS snippets in an ESM project without adaptation +- deleting auth folders accidentally between runs +- confusing the public package repo with the private/internal monorepo source + +## Important notes + +### Implementation suggestion + +If BerryAgent is deployed in a separate app, define these environment variables: + +- `BERRY_SESSION_ID` +- `BERRY_AUTH_FOLDER` +- `BERRY_DATABASE_PATH` +- `OPENAI_API_KEY` or equivalent for AI integration diff --git a/data/02-quickstart.md b/data/02-quickstart.md new file mode 100644 index 0000000..2ce907d --- /dev/null +++ b/data/02-quickstart.md @@ -0,0 +1,85 @@ +# Quick Start + +## Goal + +This file provides a fast path from installation to a working connection and a first message send. + +## Confirmed + +BerryProtocol currently exposes: + +- `connectWithQr()` +- `connectWithLink()` +- `connectWithPairingCode()` +- `sendText()` +- event listeners through `.on(...)` + +## When to use + +Use this file when you need the minimum working setup for: + +- local testing +- demos +- first agent connection +- onboarding new developers + +## Minimal connection flow + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "quickstart-session", +}); + +client.on("auth.qr", ({ value }) => { + console.log("Scan this QR code:", value); +}); + +client.on("connection.open", () => { + console.log("Connected"); +}); + +await client.connectWithQr(); +``` + +## Minimal send flow + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "quickstart-send", +}); + +await client.connectWithQr(); +await client.sendText("5511999999999@s.whatsapp.net", "Hello from BerryProtocol"); +``` + +## Best practices + +- keep quickstart examples isolated from production orchestration +- verify the target JID format before sending +- log both `auth.qr` and `connection.open` during onboarding + +## Common mistakes + +- calling `sendText` before the connection is established +- forgetting to persist the same `sessionId` +- assuming the QR code remains valid indefinitely + +## Important notes + +### Confirmed + +Incoming event names observed in the codebase include: + +- `auth.qr` +- `connection.open` +- `connection.close` +- `message.received` +- `message.ack` + +### Example conceptual + +If you want a fast local agent bootstrap, BerryAgent can wrap the client in a service object and expose a `start()` method that internally calls `connectWithQr()`. diff --git a/data/03-client-connection.md b/data/03-client-connection.md new file mode 100644 index 0000000..0ed1258 --- /dev/null +++ b/data/03-client-connection.md @@ -0,0 +1,94 @@ +# Client Connection and Session Lifecycle + +## Goal + +This file explains how BerryProtocol manages sessions, authentication entrypoints, reconnect behavior, and lifecycle events. + +## Confirmed + +The current BerryProtocol client supports: + +- `connect(auth?)` +- `connectWithLink()` +- `connectWithQr()` +- `connectWithPairingCode(phoneNumber, customPairingCode?)` +- `disconnect()` +- `reconnect()` +- `logout()` +- `getQrCode()` + +It also stores session/auth snapshots through the auth/store layers. + +## When to use + +Use this file when working on: + +- connection bootstrapping +- QR-based onboarding +- pairing code flows +- reconnect design +- multi-session orchestration + +## QR connection example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "qr-session", + printQrInTerminal: true, +}); + +client.on("auth.qr", ({ value }) => { + console.log("QR updated", value); +}); + +await client.connectWithQr(); +``` + +## Pairing code example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "pairing-session", +}); + +await client.connectWithPairingCode("5511999999999"); +``` + +## Best practices + +- map one business identity to one `sessionId` +- never destroy auth state on transient transport errors +- keep explicit logging for auth, reconnect, and logout actions +- implement a session supervisor if you run multiple clients + +## Common mistakes + +- calling `logout()` when the goal is only a soft reconnect +- reusing the same `sessionId` in concurrent processes +- storing auth state in a temporary filesystem path + +## Important notes + +### Confirmed + +BerryProtocol emits auth-related events such as: + +- `auth.link` +- `auth.qr` +- `auth.pairing_code` +- `auth.success` +- `auth.error` + +### Implementation suggestion + +For BerryAgent, keep a `SessionRegistry` that owns: + +- active clients +- connection state +- last QR value +- reconnection metrics +- last protocol error diff --git a/data/04-message-events.md b/data/04-message-events.md new file mode 100644 index 0000000..f57488d --- /dev/null +++ b/data/04-message-events.md @@ -0,0 +1,97 @@ +# Message Events + +## Goal + +This file documents the event model that BerryProtocol exposes for real-time message-driven applications. + +## Confirmed + +The event contracts in the current codebase include: + +- `message.received` +- `message.sent` +- `message.ack` +- `presence.update` +- `chats.update` +- `sync.history` +- `sync.contacts` +- `sync.groups` +- `sync.messages` +- `protocol.error` + +## When to use + +Use this file when you are building: + +- event processors +- inbox sync flows +- AI agents reacting to inbound messages +- observability or audit pipelines + +## Example: inbound listener + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "events-session", +}); + +client.on("message.received", (message) => { + console.log("Incoming message", { + type: message.type, + from: message.from, + chatId: message.chatId, + }); +}); + +await client.connectWithQr(); +``` + +## Example: acknowledgment listener + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "ack-session", +}); + +client.on("message.ack", (ack) => { + console.log("Ack update", ack); +}); + +await client.connectWithQr(); +``` + +## Best practices + +- normalize inbound events before passing them to business logic +- keep raw transport concerns out of your domain services +- persist message IDs if you need edits, reactions, or audits later +- observe both sent events and ack events + +## Common mistakes + +- assuming all inbound messages are text +- ignoring the difference between sender and chat identifiers +- treating `message.sent` as proof of delivery + +## Important notes + +### Confirmed + +The message contracts include message metadata such as: + +- `id` +- `to` +- `from` +- `chatId` +- `remoteJid` +- `timestamp` +- `ack` +- `type` + +### Example conceptual + +BerryAgent can subscribe to `message.received`, convert inbound payloads into embeddings or structured tasks, and then decide whether to answer directly or route to a flow engine. diff --git a/data/05-send-text.md b/data/05-send-text.md new file mode 100644 index 0000000..684e8d7 --- /dev/null +++ b/data/05-send-text.md @@ -0,0 +1,88 @@ +# Sending Text Messages + +## Goal + +This file explains how to send plain text and text-centric payloads in BerryProtocol. + +## Confirmed + +The current public client supports: + +- `sendText(to, text)` +- `sendMessage(to, { text })` +- `editMessage(to, messageId, text)` + +## When to use + +Use this file when: + +- sending plain responses +- sending confirmations, alerts, or prompts +- implementing AI assistant replies +- editing a previous text message + +## Simple text example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "send-text-session", +}); + +await client.connectWithQr(); +await client.sendText("5511999999999@s.whatsapp.net", "Your order has been confirmed."); +``` + +## Using sendMessage with text + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "send-message-text", +}); + +await client.connectWithQr(); +await client.sendMessage("5511999999999@s.whatsapp.net", { + text: "This was sent through sendMessage", +}); +``` + +## Editing a message + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "edit-message-session", +}); + +await client.connectWithQr(); + +const sent = await client.sendText("5511999999999@s.whatsapp.net", "Pending..."); +await client.editMessage("5511999999999@s.whatsapp.net", sent.id, "Done."); +``` + +## Best practices + +- use `sendText` for the simplest case +- store returned message IDs if future edits are possible +- validate JIDs before sending +- standardize message tone through an application layer + +## Common mistakes + +- attempting to edit messages without storing the original message ID +- mixing user-facing text generation with transport logic +- assuming every send succeeds immediately + +## Important notes + +### Confirmed + +`sendText` delegates to the general send path but is the clearest API for plain outbound text. + +### Example conceptual + +If you need advanced quoting or forwarding metadata on text messages, use a wrapper service and mark the logic as transport-specific until a stable high-level public method exists. diff --git a/data/06-send-media.md b/data/06-send-media.md new file mode 100644 index 0000000..69ad799 --- /dev/null +++ b/data/06-send-media.md @@ -0,0 +1,104 @@ +# Sending Media + +## Goal + +This file explains how BerryProtocol sends image, audio, and document payloads, and how media loading is normalized. + +## Confirmed + +The current public client supports: + +- `sendImage(to, mediaPayload)` +- `sendAudio(to, mediaPayload)` +- `sendDocument(to, mediaPayload)` + +Media payloads can include: + +- `url` +- `path` +- `buffer` +- `fileName` +- `mimetype` +- `caption` + +## When to use + +Use this file when sending: + +- product images +- voice/audio assets +- invoices or PDFs +- onboarding documents + +## Send image example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "send-image-session", +}); + +await client.connectWithQr(); +await client.sendImage("5511999999999@s.whatsapp.net", { + url: "https://example.com/catalog/pizza.jpg", + caption: "Today special", +}); +``` + +## Send audio example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "send-audio-session", +}); + +await client.connectWithQr(); +await client.sendAudio("5511999999999@s.whatsapp.net", { + path: "./media/instructions.ogg", + mimetype: "audio/ogg", +}); +``` + +## Send document example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "send-document-session", +}); + +await client.connectWithQr(); +await client.sendDocument("5511999999999@s.whatsapp.net", { + path: "./media/invoice.pdf", + fileName: "invoice.pdf", + mimetype: "application/pdf", + caption: "Invoice attached", +}); +``` + +## Best practices + +- always provide `mimetype` when known +- set `fileName` for documents +- isolate remote media downloads from message orchestration +- prefer reusable media helper services in production + +## Common mistakes + +- sending documents without a stable MIME type +- assuming remote URLs never fail +- storing large buffers unnecessarily + +## Important notes + +### Confirmed + +BerryProtocol uses a media manager internally to normalize `url`, `path`, and `buffer` inputs before transport send. + +### Example conceptual + +If you need video, GIF, or view-once wrappers and the exact high-level method is not yet stabilized, implement them behind your own message adapter and document them as transport extensions. diff --git a/data/07-send-buttons.md b/data/07-send-buttons.md new file mode 100644 index 0000000..02bb1ad --- /dev/null +++ b/data/07-send-buttons.md @@ -0,0 +1,90 @@ +# Sending Buttons + +## Goal + +This file explains how to send interactive button-based messages in BerryProtocol. + +## Confirmed + +The current client supports: + +- `sendButtons(to, payload)` +- `sendLegacyButtons(to, payload)` +- `sendMessage(to, { buttonsMessage: payload })` + +## When to use + +Use buttons when: + +- you want a short menu +- you want explicit reply choices +- you need stronger UX than free-text input + +## Reply buttons example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "buttons-session", +}); + +await client.connectWithQr(); + +await client.sendButtons("5511999999999@s.whatsapp.net", { + text: "Choose an option", + footer: "BerryProtocol demo", + buttons: [ + { id: "order_status", title: "Order status", kind: "reply" }, + { id: "speak_agent", title: "Human agent", kind: "reply" }, + ], +}); +``` + +## Legacy buttons example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "legacy-buttons-session", +}); + +await client.connectWithQr(); + +await client.sendLegacyButtons("5511999999999@s.whatsapp.net", { + text: "Choose an option", + footer: "Legacy flow", + buttons: [ + { id: "opt1", title: "Option 1" }, + { id: "opt2", title: "Option 2" }, + ], +}); +``` + +## Best practices + +- keep button labels short +- prefer stable IDs over labels for backend routing +- log click IDs rather than display text +- do not overload a single message with too many choices + +## Common mistakes + +- using display text as the only business identifier +- creating ambiguous button titles +- forgetting to handle incoming `buttonId` or `selectedButtonId` + +## Important notes + +### Confirmed + +Incoming message contracts expose fields that may contain button interaction identifiers, such as: + +- `buttonId` +- `selectedButtonId` +- `rawButtonParamsJson` + +### Example conceptual + +If you need more advanced button flows like mixed CTA and copy actions in a single wrapper, document them as conceptual until the exact stable public abstraction is finalized. diff --git a/data/08-send-lists.md b/data/08-send-lists.md new file mode 100644 index 0000000..7c812dc --- /dev/null +++ b/data/08-send-lists.md @@ -0,0 +1,78 @@ +# Sending Lists + +## Goal + +This file explains how to send WhatsApp list messages with sections and rows. + +## Confirmed + +The current client supports: + +- `sendList(to, payload)` +- `sendMessage(to, { list: payload })` +- `sendMessage(to, { listMessage: payload })` + +## When to use + +Use list messages when: + +- you have more than a few choices +- you want grouped options +- the user needs to pick one route in a structured menu + +## Practical example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "list-session", +}); + +await client.connectWithQr(); + +await client.sendList("5511999999999@s.whatsapp.net", { + title: "Main menu", + text: "Choose one option below", + footer: "BerryProtocol list demo", + buttonText: "Open options", + sections: [ + { + title: "Orders", + rows: [ + { id: "track_order", title: "Track my order" }, + { id: "cancel_order", title: "Cancel order" }, + ], + }, + { + title: "Support", + rows: [ + { id: "billing_help", title: "Billing help" }, + ], + }, + ], +}); +``` + +## Best practices + +- group rows into meaningful sections +- keep row titles short and distinct +- treat row IDs as your canonical routing keys +- keep the top-level text focused and instructional + +## Common mistakes + +- adding too many rows in a single section +- reusing the same row ID across unrelated flows +- forgetting to handle the resulting selection event in your application logic + +## Important notes + +### Confirmed + +Lists are a first-class high-level message path in BerryProtocol. + +### Example conceptual + +If your flow engine needs dynamic lists generated from business state, build the sections just before sending and keep selection IDs stable even if labels change. diff --git a/data/09-send-carousel.md b/data/09-send-carousel.md new file mode 100644 index 0000000..e3e5232 --- /dev/null +++ b/data/09-send-carousel.md @@ -0,0 +1,89 @@ +# Sending Carousel Messages + +## Goal + +This file explains how to send modern WhatsApp carousel messages in BerryProtocol. + +## Confirmed + +The current client supports: + +- `sendCarousel(to, payload)` +- `sendMessage(to, { text, footer, cards, carouselCardType, ai })` + +The current event contracts also include carousel payload types such as: + +- `CarouselMessagePayload` +- `CarouselCard` +- `CarouselButton` +- `CarouselCardType` + +## When to use + +Use carousels when: + +- you need multiple product or offer cards +- you want richer browsing inside WhatsApp +- each card needs its own image/video and action + +## Practical example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "carousel-session", +}); + +await client.connectWithQr(); + +await client.sendCarousel("5511999999999@s.whatsapp.net", { + text: "Check out our featured products", + footer: "Swipe to see more", + carouselCardType: "image", + cards: [ + { + title: "Calabresa Pizza", + body: "Large size with premium ingredients", + footer: "R$ 39,90", + image: { + url: "https://example.com/media/calabresa.jpg", + }, + buttons: [ + { + id: "buy_calabresa", + title: "Choose", + kind: "quick_reply", + }, + ], + }, + ], +}); +``` + +## Best practices + +- keep cards visually consistent +- do not mix image and video unless the business case is strong +- define stable button IDs per card +- keep each card focused on one decision + +## Common mistakes + +- building cards without image or video +- using too many cards for a simple task +- turning a carousel into a dense text wall + +## Important notes + +### Confirmed + +BerryProtocol validates important carousel constraints internally, including maximum cards and media requirements. + +### Confirmed + +AI label can be attached at the message/container level of a carousel. + +### Example conceptual + +Per-card AI label rendering should be treated as unsupported unless a future protocol-confirmed implementation exists. diff --git a/data/10-copy-buttons.md b/data/10-copy-buttons.md new file mode 100644 index 0000000..e4e9166 --- /dev/null +++ b/data/10-copy-buttons.md @@ -0,0 +1,78 @@ +# Copy Buttons + +## Goal + +This file documents copy-code style buttons and how they fit into business flows such as OTP and coupon delivery. + +## Confirmed + +The event contracts currently include button kinds such as: + +- `copy_code` +- `cta_url` +- `quick_reply` +- `reply` + +BerryOTP also uses copy-oriented interaction patterns in its current implementation. + +## When to use + +Use copy buttons when: + +- sending one-time codes +- sharing coupons +- reducing typing errors for end users + +## Example conceptual + +The exact public high-level method for standalone copy-button composition may evolve. +Until the public API is finalized, treat this example as conceptual. + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "copy-button-session", +}); + +await client.connectWithQr(); + +await client.sendButtons("5511999999999@s.whatsapp.net", { + text: "Your verification code is ready", + footer: "Tap to copy", + buttons: [ + { + id: "copy_login_code", + title: "Copy code", + kind: "copy_code", + code: "483921", + }, + ], +}); +``` + +## Best practices + +- use copy buttons for high-friction inputs +- log the flow state separately from the button press itself +- pair copy buttons with expiration guidance when the value is temporary + +## Common mistakes + +- using copy buttons for actions that should be replies +- exposing long-lived sensitive values without expiration or revocation logic + +## Important notes + +### Confirmed + +BerryOTP is currently the best confirmed reference for copy-code style flows in the BerrySDK ecosystem. + +### Implementation suggestion + +If BerryAgent sends verification or secure action codes, it should also store: + +- issuance timestamp +- expiration timestamp +- code purpose +- denial/cancel status diff --git a/data/11-cta-buttons.md b/data/11-cta-buttons.md new file mode 100644 index 0000000..cd0614e --- /dev/null +++ b/data/11-cta-buttons.md @@ -0,0 +1,75 @@ +# CTA Buttons + +## Goal + +This file explains CTA-oriented button patterns, especially URL-driven actions used in business automation. + +## Confirmed + +The event contracts currently define button kinds that include: + +- `cta_url` +- `copy_code` +- `quick_reply` +- `reply` + +## When to use + +Use CTA buttons when: + +- sending payment links +- redirecting to order tracking +- opening forms or support pages +- escalating to self-service portals + +## Example conceptual + +This is a conceptual example that shows a likely button composition pattern. + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "cta-button-session", +}); + +await client.connectWithQr(); + +await client.sendButtons("5511999999999@s.whatsapp.net", { + text: "Open your billing portal", + footer: "BerryProtocol CTA example", + buttons: [ + { + id: "open_billing", + title: "Open portal", + kind: "cta_url", + url: "https://example.com/billing", + }, + ], +}); +``` + +## Best practices + +- prefer short trusted domains +- clearly describe what happens after click +- keep CTAs aligned with the current conversation stage + +## Common mistakes + +- mixing too many different intents in one CTA message +- not validating destination URLs before sending +- using CTA where a simple reply action would be better + +## Important notes + +### Suggestion of implementation + +For agent-driven systems, represent CTA buttons with: + +- action type +- destination URL +- label +- analytics metadata + +This makes downstream reporting easier. diff --git a/data/12-native-flows.md b/data/12-native-flows.md new file mode 100644 index 0000000..8c51665 --- /dev/null +++ b/data/12-native-flows.md @@ -0,0 +1,87 @@ +# Native Flows + +## Goal + +This file explains WhatsApp-native interaction patterns and how BerryProtocol models structured interactive payloads. + +## Confirmed + +The current contracts expose: + +- `InteractivePayload` +- `InteractiveHeader` +- `InteractiveBody` +- `InteractiveFooter` +- `InteractiveNativeButton` +- `InteractiveNativeFlowPayload` + +The client also exposes: + +- `sendMessage(to, { interactiveMessage: payload })` + +## When to use + +Use native-flow style interactions when: + +- you need richer interactive experiences +- the default button/list abstractions are too limited +- your application needs structured parameters for a flow renderer + +## Practical example + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "native-flow-session", +}); + +await client.connectWithQr(); + +await client.sendMessage("5511999999999@s.whatsapp.net", { + interactiveMessage: { + header: { + title: "Berry native flow", + }, + body: { + text: "Choose a flow action", + }, + footer: { + text: "Interactive demo", + }, + nativeFlowMessage: { + buttons: [ + { + name: "quick_reply", + buttonParamsJson: JSON.stringify({ + display_text: "Continue", + id: "continue_flow", + }), + }, + ], + }, + }, +}); +``` + +## Best practices + +- keep native-flow payload generation centralized +- validate JSON payloads before sending +- document each `name` and parameter schema internally + +## Common mistakes + +- mixing multiple incompatible button payload shapes +- building `buttonParamsJson` manually without validation +- using native flows where a simple list is enough + +## Important notes + +### Confirmed + +Native-flow payload support is present at the type and client API level. + +### Example conceptual + +If BerryAgent needs a higher-level builder for native flows, create a domain-specific abstraction above `InteractivePayload` rather than embedding raw JSON across the codebase. diff --git a/data/13-ai-agent.md b/data/13-ai-agent.md new file mode 100644 index 0000000..e8adbb7 --- /dev/null +++ b/data/13-ai-agent.md @@ -0,0 +1,139 @@ +# AI Agent Integration + +## Goal + +This file explains how BerryProtocol can be used as the transport layer for AI agents, including OpenAI-compatible APIs and Groq-compatible APIs. + +## Confirmed + +BerryProtocol currently supports: + +- inbound events through `message.received` +- text sending +- interactive sending +- AI-labeled message support at the message level + +## When to use + +Use this file when building: + +- autonomous chat agents +- assisted support bots +- AI-powered product recommendation flows +- RAG-enabled WhatsApp assistants + +## Example with OpenAI-compatible API + +```js +import BerryProtocol from "berryprotocol"; +import OpenAI from "openai"; + +const client = new BerryProtocol({ + sessionId: "berryagent-openai", +}); + +const ai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +client.on("message.received", async (message) => { + if (message.type !== "text" || !message.from) { + return; + } + + const completion = await ai.chat.completions.create({ + model: "gpt-4.1-mini", + messages: [ + { + role: "system", + content: "You are BerryAgent, a helpful WhatsApp commerce assistant.", + }, + { + role: "user", + content: message.text, + }, + ], + }); + + const reply = completion.choices[0]?.message?.content ?? "I could not generate a reply."; + + await client.sendMessage(message.chatId ?? message.from, { + text: reply, + ai: true, + }); +}); + +await client.connectWithQr(); +``` + +## Example with Groq-compatible API + +```js +import BerryProtocol from "berryprotocol"; +import OpenAI from "openai"; + +const client = new BerryProtocol({ + sessionId: "berryagent-groq", +}); + +const ai = new OpenAI({ + apiKey: process.env.GROQ_API_KEY, + baseURL: "https://api.groq.com/openai/v1", +}); + +client.on("message.received", async (message) => { + if (message.type !== "text" || !message.from) { + return; + } + + const completion = await ai.chat.completions.create({ + model: "llama-3.3-70b-versatile", + messages: [ + { + role: "system", + content: "You are BerryAgent. Keep answers concise and helpful.", + }, + { + role: "user", + content: message.text, + }, + ], + }); + + const reply = completion.choices[0]?.message?.content ?? "No answer available."; + + await client.sendText(message.chatId ?? message.from, reply); +}); + +await client.connectWithQr(); +``` + +## Best practices + +- separate AI orchestration from transport +- add guardrails before calling the model +- keep a retriever layer independent from the LLM client +- log model latency and transport latency separately + +## Common mistakes + +- sending raw prompts directly from transport handlers without normalization +- letting the model choose transport semantics without validation +- mixing RAG retrieval code with WhatsApp send code in one file + +## Important notes + +### Confirmed + +AI label is supported at the message level and can also be applied to supported carousel messages. + +### Suggestion of implementation + +BerryAgent should use a layered design: + +- retriever +- prompt composer +- model client +- policy/guardrails +- action executor +- BerryProtocol transport adapter diff --git a/data/14-berryagent-architecture.md b/data/14-berryagent-architecture.md new file mode 100644 index 0000000..d8e8b72 --- /dev/null +++ b/data/14-berryagent-architecture.md @@ -0,0 +1,84 @@ +# BerryAgent Architecture + +## Goal + +This file proposes a production-ready architecture for BerryAgent as the official AI agent built on top of BerryProtocol. + +## When to use + +Use this file when designing: + +- local RAG execution +- multi-session AI orchestration +- tool calling around WhatsApp operations +- support and sales automation + +## Confirmed + +Confirmed BerryProtocol building blocks that BerryAgent can rely on: + +- session-scoped clients +- incoming message events +- outbound text and interactive messaging +- session reconnection and auth flows + +## Implementation suggestion + +Recommended architecture: + +- `SessionRegistry` + - creates and owns BerryProtocol clients +- `InboundNormalizer` + - converts BerryProtocol events into agent-friendly tasks +- `Retriever` + - reads markdown knowledge from `/data` +- `PromptBuilder` + - assembles system, context, and user content +- `LLMAdapter` + - wraps OpenAI-compatible providers +- `PolicyEngine` + - enforces tool and response constraints +- `ActionExecutor` + - decides whether to reply with text, list, buttons, or carousel +- `MemoryStore` + - stores conversation summaries and recent turns + +## Example conceptual + +```js +class BerryAgentRuntime { + constructor({ sessionRegistry, retriever, llm, actionExecutor }) { + this.sessionRegistry = sessionRegistry; + this.retriever = retriever; + this.llm = llm; + this.actionExecutor = actionExecutor; + } + + async handleInboundMessage(event) { + const context = await this.retriever.search(event.text); + const answer = await this.llm.respond({ + userInput: event.text, + context, + }); + + await this.actionExecutor.sendText(event.chatId, answer); + } +} +``` + +## Best practices + +- use retrieval before generation +- maintain explicit conversation state per chat +- do not expose raw model output directly when business actions are involved +- gate high-risk actions behind confirmation steps + +## Common mistakes + +- treating the transport client as the whole agent +- overloading a single prompt with unrelated documentation +- skipping observability for retrieval quality + +## Important notes + +The `/data` directory requested for BerryAgent should be treated as first-class retrieval content, not as generic notes. diff --git a/data/15-berrystudio.md b/data/15-berrystudio.md new file mode 100644 index 0000000..c7fd6dd --- /dev/null +++ b/data/15-berrystudio.md @@ -0,0 +1,60 @@ +# BerryStudio + +## Goal + +This file documents the role of BerryStudio inside the ecosystem as a management, orchestration, or product-layer surface above BerryProtocol. + +## When to use + +Consult this file when: + +- describing the ecosystem to users +- deciding where flow-building logic belongs +- separating SDK concerns from UI concerns + +## Confirmed + +The BerryProtocol codebase and prior project context indicate BerryStudio as a product/application layer rather than the transport SDK itself. + +## Explanation + +BerryStudio should be understood as: + +- an operator-facing application +- a workflow, session, or automation management surface +- a place where users configure behavior instead of implementing low-level protocol code + +BerryProtocol remains the runtime SDK beneath that layer. + +## Example conceptual + +BerryStudio might manage: + +- sessions +- message templates +- automation flows +- AI assistants +- dashboards + +while BerryProtocol is responsible for: + +- connectivity +- event handling +- transport-safe message delivery + +## Best practices + +- keep BerryStudio business logic separate from core transport abstractions +- use a service layer when BerryStudio calls BerryProtocol +- store operator settings and runtime state independently + +## Common mistakes + +- putting UI concerns inside the protocol layer +- assuming BerryStudio and BerryProtocol should share the same deployment topology + +## Important notes + +### Suggestion of implementation + +BerryAgent may use BerryStudio as an orchestration and operations surface, but BerryProtocol should remain the runtime source of truth for WhatsApp session behavior. diff --git a/data/16-berryapi.md b/data/16-berryapi.md new file mode 100644 index 0000000..7cd2da2 --- /dev/null +++ b/data/16-berryapi.md @@ -0,0 +1,60 @@ +# BerryAPI + +## Goal + +This file explains the purpose of BerryAPI as the REST/API surface above BerryProtocol. + +## When to use + +Use this file when: + +- exposing BerryProtocol capabilities externally +- integrating with n8n, Typebot, or backend systems +- designing webhooks and automation endpoints + +## Confirmed + +The repository includes a BerryAPI package intended to provide an API-oriented layer around BerryProtocol. + +## Explanation + +BerryAPI should be treated as: + +- the external integration surface +- the place for REST endpoints, webhooks, and event streaming +- the bridge between product UIs and transport runtimes + +BerryProtocol should be treated as: + +- the SDK runtime +- the session and message transport engine +- the source of real-time message behavior + +## Example conceptual + +Possible layering: + +- BerryProtocol + - handles session, auth, events, and sends +- BerryAPI + - exposes instance management, webhooks, and message endpoints +- BerryAgent + - consumes both the SDK and the knowledge base for intelligent behavior + +## Best practices + +- keep BerryAPI stateless where possible +- centralize session ownership in a manager +- forward important runtime events to webhooks or WebSocket clients + +## Common mistakes + +- treating API requests as the only source of truth for runtime state +- not reconciling webhook delivery failures + +## Important notes + +BerryAgent can run: + +- behind BerryAPI as an API-powered assistant +- or directly against BerryProtocol for local/private deployments diff --git a/data/17-errors-and-troubleshooting.md b/data/17-errors-and-troubleshooting.md new file mode 100644 index 0000000..d7eac70 --- /dev/null +++ b/data/17-errors-and-troubleshooting.md @@ -0,0 +1,117 @@ +# Errors and Troubleshooting + +## Goal + +This file documents common operational problems and how to debug them in BerryProtocol-based systems. + +## When to use + +Use this file when dealing with: + +- QR issues +- connection failures +- invalid JIDs +- message send failures +- session conflicts +- AI or RAG runtime confusion around transport behavior + +## Common error categories + +### Invalid JID + +Symptom: + +- send calls fail before transport delivery + +Cause: + +- the target identifier is not a valid WhatsApp JID + +Fix: + +- use full JIDs such as `5511999999999@s.whatsapp.net` +- validate before dispatch + +### Connection not open + +Symptom: + +- send operations fail right after startup + +Cause: + +- the connection lifecycle is not complete yet + +Fix: + +- wait for `connection.open` +- queue outbound tasks until ready + +### Session conflict + +Symptom: + +- repeated disconnects +- logs mentioning replacement or conflict + +Cause: + +- the same session is being used by multiple runtime processes + +Fix: + +- guarantee single ownership per `sessionId` + +### Missing QR behavior + +Symptom: + +- no visible QR onboarding flow + +Possible causes: + +- wrong auth method +- QR not being handled in listeners +- session already registered + +## Practical example: safe send wrapper + +```js +export function assertJid(jid) { + if (!jid || !jid.includes("@")) { + throw new Error("Invalid WhatsApp JID"); + } + + return jid; +} +``` + +## Best practices + +- log auth and connection events +- keep structured logs for transport and AI separately +- persist session state durably +- test with a dedicated sandbox session before production rollout + +## Common mistakes + +- retrying infinitely without classification +- hiding low-level errors from operator logs +- mixing transport failures with model failures + +## Important notes + +### Confirmed + +BerryProtocol exposes protocol and connection-related events that should be monitored in production. + +### Suggestion of implementation + +BerryAgent should classify errors into: + +- connection +- authentication +- validation +- transport send +- AI provider +- retrieval diff --git a/data/18-best-practices.md b/data/18-best-practices.md new file mode 100644 index 0000000..c0f3d03 --- /dev/null +++ b/data/18-best-practices.md @@ -0,0 +1,76 @@ +# Best Practices + +## Goal + +This file summarizes practical recommendations for building reliable systems on top of BerryProtocol. + +## When to use + +Consult this file when designing: + +- production bots +- AI-powered assistants +- multi-session operators +- integration layers + +## Recommended practices + +### Session ownership + +- use one stable `sessionId` per logical runtime session +- avoid concurrent ownership +- persist auth state outside temporary folders + +### Messaging architecture + +- wrap transport calls in an application service +- store outbound message IDs +- separate transport payload generation from business decision logic + +### AI architecture + +- normalize inbound messages before prompting +- use retrieval before generation +- define action policies for high-risk operations +- keep prompts and transport concerns separate + +### Observability + +- log connection changes +- track send success and ack updates +- measure AI latency separately from WhatsApp transport latency + +### Security + +- sanitize external inputs before using them in prompts +- protect API keys and session storage +- avoid exposing raw internal errors to end users + +## Practical example + +```js +export class OutboundMessagingService { + constructor(client) { + this.client = client; + } + + async sendOrderConfirmation(jid, orderNumber) { + return this.client.sendText( + jid, + `Your order ${orderNumber} has been confirmed.`, + ); + } +} +``` + +## Common mistakes + +- leaking transport details into UI code +- directly embedding huge prompts in event listeners +- forgetting to debounce or rate-limit AI-heavy workflows + +## Important notes + +### Suggestion of implementation + +BerryAgent should treat this file as policy guidance and not only as syntax guidance. diff --git a/data/19-security.md b/data/19-security.md new file mode 100644 index 0000000..f437661 --- /dev/null +++ b/data/19-security.md @@ -0,0 +1,67 @@ +# Security + +## Goal + +This file documents security considerations for BerryProtocol, BerryAgent, and adjacent ecosystem components. + +## When to use + +Use this file when: + +- handling auth folders +- storing session state +- managing AI provider keys +- exposing BerryAPI to external systems + +## Main risks + +### Session theft + +If auth/session files are leaked, an attacker may impersonate the connected WhatsApp session. + +### Prompt injection + +If inbound user messages are passed directly into an AI model without policy filtering, an attacker can manipulate the agent. + +### Sensitive data leakage + +Logs, prompts, or retrieval corpora may unintentionally expose: + +- phone numbers +- order details +- support transcripts +- API keys + +## Best practices + +- protect `.berry-sessions` or equivalent auth folders +- use least-privilege access for runtime processes +- redact sensitive data from logs where possible +- isolate AI keys in environment variables +- validate outbound URLs for CTA flows +- audit who can trigger admin-grade actions + +## Example conceptual + +```js +function redactPhone(value) { + return value.replace(/\d(?=\d{4})/g, "*"); +} +``` + +## Common mistakes + +- storing secrets in source control +- using production sessions for unsafe experiments +- giving an autonomous agent permission to perform sensitive actions without confirmation + +## Important notes + +### Suggestion of implementation + +BerryAgent should include: + +- tool allowlists +- action confirmation policies +- retrieval source boundaries +- audit logs for sensitive operations diff --git a/data/20-examples.md b/data/20-examples.md new file mode 100644 index 0000000..4e895fa --- /dev/null +++ b/data/20-examples.md @@ -0,0 +1,230 @@ +# Complete Examples + +## Goal + +This file contains complete examples for common BerryProtocol and BerryAgent scenarios. + +## Example 1: Basic bot + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "basic-bot", +}); + +client.on("message.received", async (message) => { + if (message.type !== "text" || !message.from) { + return; + } + + const text = message.text.trim().toLowerCase(); + + if (text === "ping") { + await client.sendText(message.chatId ?? message.from, "pong"); + } +}); + +await client.connectWithQr(); +``` + +## Example 2: Button menu + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "button-menu", +}); + +await client.connectWithQr(); + +await client.sendButtons("5511999999999@s.whatsapp.net", { + text: "Choose a path", + footer: "BerryProtocol example", + buttons: [ + { id: "sales", title: "Sales", kind: "reply" }, + { id: "support", title: "Support", kind: "reply" }, + ], +}); +``` + +## Example 3: List menu + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "list-menu", +}); + +await client.connectWithQr(); + +await client.sendList("5511999999999@s.whatsapp.net", { + title: "Departments", + text: "Select where you want to go", + footer: "BerryProtocol example", + buttonText: "Open list", + sections: [ + { + title: "General", + rows: [ + { id: "faq", title: "FAQ" }, + { id: "billing", title: "Billing" }, + ], + }, + ], +}); +``` + +## Example 4: Carousel + +```js +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "carousel-example", +}); + +await client.connectWithQr(); + +await client.sendCarousel("5511999999999@s.whatsapp.net", { + text: "Featured items", + footer: "Swipe through the cards", + carouselCardType: "image", + cards: [ + { + title: "Product A", + body: "A short description", + footer: "R$ 49,90", + image: { url: "https://example.com/product-a.jpg" }, + buttons: [ + { id: "buy_a", title: "Buy now", kind: "quick_reply" }, + ], + }, + ], +}); +``` + +## Example 5: AI reply + +```js +import BerryProtocol from "berryprotocol"; +import OpenAI from "openai"; + +const client = new BerryProtocol({ + sessionId: "ai-example", +}); + +const ai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +client.on("message.received", async (message) => { + if (message.type !== "text" || !message.from) { + return; + } + + const completion = await ai.chat.completions.create({ + model: "gpt-4.1-mini", + messages: [ + { role: "system", content: "You are BerryAgent." }, + { role: "user", content: message.text }, + ], + }); + + const answer = completion.choices[0]?.message?.content ?? "No answer available."; + + await client.sendMessage(message.chatId ?? message.from, { + text: answer, + ai: true, + }); +}); + +await client.connectWithQr(); +``` + +## Example 6: AI + retrieval + +```js +import fs from "node:fs/promises"; +import path from "node:path"; +import BerryProtocol from "berryprotocol"; +import OpenAI from "openai"; + +const client = new BerryProtocol({ + sessionId: "rag-example", +}); + +const ai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +async function loadKnowledgeBase() { + const dataDir = path.resolve("./data"); + const names = await fs.readdir(dataDir); + const markdownFiles = names.filter((name) => name.endsWith(".md")); + const contents = await Promise.all( + markdownFiles.map(async (name) => { + const text = await fs.readFile(path.join(dataDir, name), "utf8"); + return { name, text }; + }), + ); + + return contents; +} + +function retrieveDocuments(documents, query) { + const normalized = query.toLowerCase(); + + return documents + .filter((doc) => doc.text.toLowerCase().includes(normalized)) + .slice(0, 3); +} + +const knowledgeBase = await loadKnowledgeBase(); + +client.on("message.received", async (message) => { + if (message.type !== "text" || !message.from) { + return; + } + + const docs = retrieveDocuments(knowledgeBase, message.text); + const context = docs.map((doc) => `# ${doc.name}\n${doc.text}`).join("\n\n"); + + const completion = await ai.chat.completions.create({ + model: "gpt-4.1-mini", + messages: [ + { + role: "system", + content: "You are BerryAgent. Answer using the provided BerryProtocol context.", + }, + { + role: "user", + content: `Context:\n${context}\n\nQuestion:\n${message.text}`, + }, + ], + }); + + const answer = completion.choices[0]?.message?.content ?? "No answer available."; + + await client.sendText(message.chatId ?? message.from, answer); +}); + +await client.connectWithQr(); +``` + +## Best practices + +- keep examples isolated by use case +- copy examples into a dedicated app instead of editing them in place +- validate environment variables before boot + +## Common mistakes + +- using example code as production architecture without adaptation +- mixing secrets directly into examples + +## Important notes + +Examples in this file aim to be practical, but orchestration details such as storage, retries, and policy enforcement should still be handled in your application layer. diff --git a/data/21-faq.md b/data/21-faq.md new file mode 100644 index 0000000..33a536a --- /dev/null +++ b/data/21-faq.md @@ -0,0 +1,66 @@ +# FAQ + +## What is BerryProtocol? + +BerryProtocol is the WhatsApp Web automation SDK/runtime layer in the BerrySDK ecosystem. + +## What is BerryAgent? + +BerryAgent is the official AI agent concept built on top of BerryProtocol, retrieval content, and an OpenAI-compatible model provider. + +## What is BerryOTP? + +BerryOTP is the OTP and verification layer of the ecosystem. + +## What is BerryAPI? + +BerryAPI is the API-oriented layer intended to expose BerryProtocol capabilities to external systems and automation tools. + +## Does BerryProtocol support QR login? + +### Confirmed + +Yes. The current client exposes `connectWithQr()`. + +## Does BerryProtocol support pairing code? + +### Confirmed + +Yes. The current client exposes `connectWithPairingCode(phoneNumber, customPairingCode?)`. + +## Does BerryProtocol support carousels? + +### Confirmed + +Yes. The current client supports carousel payloads and a dedicated carousel send path. + +## Does BerryProtocol support AI label? + +### Confirmed + +Yes, at the message level. + +## Does BerryProtocol support AI label per card in carousel? + +### Confirmed + +No validated per-card rendering support is confirmed. Treat that as unsupported unless future protocol behavior proves otherwise. + +## Should BerryAgent answer directly from the model? + +### Best practice + +No. Prefer retrieval, policy checks, and transport-safe execution. + +## What should the local RAG corpus include? + +- installation +- connection lifecycle +- message sending docs +- examples +- troubleshooting +- glossary + +## Important notes + +When answering end-user questions, BerryAgent should prioritize confirmed sections over conceptual examples. diff --git a/data/22-glossary.md b/data/22-glossary.md new file mode 100644 index 0000000..fcf7a08 --- /dev/null +++ b/data/22-glossary.md @@ -0,0 +1,57 @@ +# Glossary + +## BerryProtocol + +The WhatsApp Web automation SDK/runtime layer in the BerrySDK ecosystem. + +## BerryAgent + +The official AI agent concept or implementation that uses BerryProtocol as its transport and action layer. + +## BerryOTP + +The OTP and verification package in the BerrySDK ecosystem. + +## BerryAPI + +The API-facing layer built around BerryProtocol for integrations, webhooks, and instance management. + +## Session ID + +A stable identifier used to represent a persisted WhatsApp runtime session. + +## JID + +WhatsApp identifier format such as `5511999999999@s.whatsapp.net`. + +## QR authentication + +A connection method where the user scans a QR code emitted by the session runtime. + +## Pairing code + +A connection method where the runtime requests a code-based pairing flow using a phone number. + +## AI label + +A message-level marker used to identify supported AI-generated responses in compatible WhatsApp message flows. + +## Native flow + +A structured interactive payload pattern used for advanced WhatsApp interactions beyond simple buttons and lists. + +## RAG + +Retrieval-Augmented Generation, where an AI model receives retrieved documentation context before producing an answer. + +## Transport layer + +The part of the system responsible for connection, message delivery, and protocol interaction. + +## Orchestration layer + +The part of the system responsible for deciding what should happen, such as AI routing, flows, or business actions. + +## Important notes + +This glossary is useful for lightweight retrieval when the user asks terminology questions or when BerryAgent needs a compact semantic anchor. diff --git a/data/_index.md b/data/_index.md new file mode 100644 index 0000000..45a45b9 --- /dev/null +++ b/data/_index.md @@ -0,0 +1,92 @@ +# BerryAgent Knowledge Base Index + +This folder contains the local markdown knowledge base for BerryAgent, the official AI agent for BerryProtocol. + +The goal of this database is to support local RAG pipelines with: + +- SDK overview and positioning +- setup and onboarding +- connection lifecycle +- message sending patterns +- interactive WhatsApp features +- AI-agent integration patterns +- operational guidance +- troubleshooting and FAQ material + +## How to use this index + +Use this file as the routing map for retrieval. + +When a question is mostly about one topic, consult the relevant file first. +When a question spans architecture and implementation, retrieve multiple files. + +## File routing by question type + +### Project overview + +- `00-overview.md` +- `22-glossary.md` + +### Installation and getting started + +- `01-installation.md` +- `02-quickstart.md` + +### Session creation, QR, reconnect, and lifecycle + +- `03-client-connection.md` +- `17-errors-and-troubleshooting.md` +- `18-best-practices.md` + +### Real-time events and incoming messages + +- `04-message-events.md` + +### Sending regular messages + +- `05-send-text.md` +- `06-send-media.md` + +### Interactive messages + +- `07-send-buttons.md` +- `08-send-lists.md` +- `09-send-carousel.md` +- `10-copy-buttons.md` +- `11-cta-buttons.md` +- `12-native-flows.md` + +### AI agent integration + +- `13-ai-agent.md` +- `14-berryagent-architecture.md` + +### Ecosystem and platform context + +- `15-berrystudio.md` +- `16-berryapi.md` + +### Troubleshooting and operational issues + +- `17-errors-and-troubleshooting.md` +- `18-best-practices.md` +- `19-security.md` +- `21-faq.md` + +### Complete examples + +- `20-examples.md` + +### Terminology help + +- `22-glossary.md` + +## Confirmed vs conceptual content + +This knowledge base intentionally separates information into three classes: + +- `Confirmed`: behavior or methods directly confirmed by the current BerryProtocol codebase +- `Conceptual example`: an example that explains how a likely integration could look when the exact public API is not fully stabilized +- `Implementation suggestion`: guidance for product design, RAG pipelines, or runtime architecture + +BerryAgent should prefer confirmed sections first when generating answers, then use conceptual sections only when the user is explicitly asking for ideas, patterns, or future-facing integrations. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2f258ea --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: "3.9" + +services: + berryapi: + build: + context: . + environment: + HOST: 0.0.0.0 + PORT: 3000 + API_KEY: berryapi_dev_key + DATABASE_URL: file:/data/berryapi.sqlite + BERRY_SQLITE_PATH: /data/berrysdk.db + BERRY_AUTH_FOLDER: /data/auth + BERRY_WS_PATH: /ws + ports: + - "3000:3000" + volumes: + - berry_data:/data + +volumes: + berry_data: diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx new file mode 100644 index 0000000..550ee4a --- /dev/null +++ b/docs/getting-started.mdx @@ -0,0 +1,34 @@ +# Getting Started + +## Install + +```bash +npm install +cp .env.example .env +``` + +## Run + +```bash +npm run dev +``` + +## Default URLs + +- `GET http://localhost:3000` +- `GET http://localhost:3000/info` +- `GET http://localhost:3000/docs` + +## Authentication + +Use: + +```http +Authorization: Bearer your_api_key +``` + +The development default is: + +```text +berryapi_dev_key +``` diff --git a/docs/index.mdx b/docs/index.mdx new file mode 100644 index 0000000..e4e2865 --- /dev/null +++ b/docs/index.mdx @@ -0,0 +1,30 @@ +# BerryAPI + +BerryAPI is the BerrySDK automation layer for WhatsApp Web, powered by BerryProtocol. + +It gives you a clean REST surface for: + +- `n8n` +- `Typebot` +- internal dashboards +- webhook integrations +- realtime WhatsApp event streams + +## Core Features + +- Multi-instance management +- QR and pairing-code connection flows +- Modern message endpoints +- Webhook dispatching +- WebSocket realtime events +- Swagger docs at `/docs` +- SQLite by default + +## Quick Links + +- [Getting Started](./getting-started) +- [Instances](./instances) +- [Messages](./messages) +- [Webhooks](./webhooks) +- [Realtime](./realtime) +- [OpenAPI](./openapi) diff --git a/docs/instances.mdx b/docs/instances.mdx new file mode 100644 index 0000000..599a2d9 --- /dev/null +++ b/docs/instances.mdx @@ -0,0 +1,31 @@ +# Instances + +BerryAPI supports multiple WhatsApp sessions through named instances. + +## Main Endpoints + +- `POST /instance/create` +- `GET /instance/fetch` +- `GET /instance/connect/:instanceName` +- `PUT /instance/restart/:instanceName` +- `GET /instance/connectionState/:instanceName` +- `DELETE /instance/logout/:instanceName` +- `DELETE /instance/delete/:instanceName` + +## Create Example + +```json +{ + "instanceName": "store-01", + "authMethod": "qr" +} +``` + +## Connection State + +When an instance needs login, the state payload may contain: + +- `qrCode` +- `pairingCode` +- `connectionState` +- `status` diff --git a/docs/messages.mdx b/docs/messages.mdx new file mode 100644 index 0000000..5fead58 --- /dev/null +++ b/docs/messages.mdx @@ -0,0 +1,97 @@ +# Messages + +BerryAPI exposes modern WhatsApp Web message endpoints. + +## Highlights + +- Text +- Media +- Buttons +- Template Buttons +- Copy Buttons +- Carousel +- Lists +- Polls +- AI Label Text +- AI Label Carousel +- View once +- Reactions +- Location +- Contacts +- Status +- Product, catalog and collection + +## Examples + +### Send Text + +```json +{ + "to": "5511999999999@s.whatsapp.net", + "text": "Hello from BerryAPI" +} +``` + +### Send Media + +```json +{ + "to": "5511999999999@s.whatsapp.net", + "url": "https://example.com/image.jpg", + "caption": "Image from BerryAPI" +} +``` + +### Send Buttons + +```json +{ + "to": "5511999999999@s.whatsapp.net", + "text": "Choose one option", + "footer": "BerryAPI", + "buttons": [ + { + "id": "buy_now", + "title": "Buy now", + "type": "reply" + } + ] +} +``` + +### Send Carousel + +```json +{ + "to": "5511999999999@s.whatsapp.net", + "text": "Confira nossas pizzas!", + "footer": "BerryAPI carousel", + "carouselCardType": "mixed", + "cards": [ + { + "title": "Pizza Calabresa", + "body": "Calabresa premium", + "footer": "R$ 39,90", + "image": { + "url": "https://images.unsplash.com/photo-1513104890138-7c749659a591" + }, + "buttons": [ + { + "id": "pizza_calabresa", + "title": "Escolher", + "kind": "quick_reply" + } + ] + } + ] +} +``` + +### Send AI Text + +```json +{ + "to": "5511999999999@s.whatsapp.net", + "text": "Ola! Essa mensagem deve aparecer com AI label em chat privado." +} +``` diff --git a/docs/monorepo-structure.md b/docs/monorepo-structure.md new file mode 100644 index 0000000..8eea990 --- /dev/null +++ b/docs/monorepo-structure.md @@ -0,0 +1,62 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +# `berrysdk` Structure + +This repository can stay focused on the WhatsApp SDK layer while the broader `berrysdk` account grows into other products. + +## Recommended split + +Use `berrysdk` as the umbrella account and npm scope: + +- GitHub account: `berrysdk` +- npm scope: `@berrysdk/*` + +Keep this repository focused on the WhatsApp stack: + +- repository: `berry-protocol` +- packages: + - `@berrysdk/core` + - `@berrysdk/proto` + - `@berrysdk/wa-message` + - `@berrysdk/socket` + - `@berrysdk/auth` + - `@berrysdk/events` + - `@berrysdk/messages` + - `@berrysdk/media` + - `@berrysdk/store` + - `@berrysdk/gateway` + - `@berrysdk/cli` + +## Future repositories + +As new products appear, split them by product line rather than by technical layer. + +Examples: + +- `berry-ai` +- `berry-storage` +- `berry-ui` +- `berry-automation` +- `berry-cloud` + +Each repository can still publish under the same scope: + +- `@berrysdk/ai` +- `@berrysdk/storage` +- `@berrysdk/ui` +- `@berrysdk/automation` + +## Naming rule + +Use this pattern: + +- account/scope: `berrysdk` +- product/repository: `berry-protocol` +- package: `@berrysdk/` + +That gives you one public identity while keeping each product isolated and easier to maintain. diff --git a/docs/openapi.mdx b/docs/openapi.mdx new file mode 100644 index 0000000..d945dc4 --- /dev/null +++ b/docs/openapi.mdx @@ -0,0 +1,15 @@ +# OpenAPI + +BerryAPI publishes Swagger/OpenAPI documentation at: + +```text +http://localhost:3000/docs +``` + +Use Swagger to: + +- inspect schemas +- authenticate with your API key +- test requests +- copy request bodies +- validate supported message payloads diff --git a/docs/publishing.md b/docs/publishing.md new file mode 100644 index 0000000..01e8f82 --- /dev/null +++ b/docs/publishing.md @@ -0,0 +1,92 @@ +# Publishing Guide + +This repository is organized so the public Berry Protocol packages can be published without relying on local `file:` dependencies. + +## What changed + +- public packages use npm semver dependencies instead of local `file:` references +- workspace packages include `exports`, `files`, `type`, `license`, `author`, and `publishConfig` +- local test, session, database, and vendor folders are ignored from Git +- the repo now includes an Apache-2.0 `LICENSE` + +## Public packages + +- `@berrysdk/auth` +- `@berrysdk/berry-otp` +- `@berrysdk/cli` +- `@berrysdk/core` +- `@berrysdk/events` +- `@berrysdk/gateway` +- `@berrysdk/media` +- `@berrysdk/messages` +- `@berrysdk/proto` +- `@berrysdk/socket` +- `@berrysdk/store` +- `@berrysdk/wa-message` +- `berryotp` +- `berryprotocol` + +## Before publishing + +Run: + +```bash +npm install +npm run prepare:publish +npm run pack:check +``` + +## GitHub checklist + +1. Create a fresh Berry Protocol repository. +2. Copy only Berry-owned source, docs, scripts, and package metadata. +3. Do not commit: + - `.baileys-sessions` + - `.berry-sessions` + - `berrysdk.db` + - `_reference_baileys2` + - `_vendor_whatsapp_proto` + - `_vendor_wppconnect_wa_proto` +4. Push the cleaned repository. + +## npm checklist + +1. Log in with `npm login`. +2. Make sure the `@berrysdk` scope exists and is public. +3. Publish packages in dependency order. + +Suggested order: + +1. `@berrysdk/events` +2. `@berrysdk/store` +3. `@berrysdk/auth` +4. `@berrysdk/media` +5. `@berrysdk/messages` +6. `@berrysdk/proto` +7. `@berrysdk/wa-message` +8. `@berrysdk/socket` +9. `@berrysdk/core` +10. `@berrysdk/berry-otp` +11. `berryotp` +12. `berryprotocol` +13. `@berrysdk/gateway` +14. `@berrysdk/cli` + +Example: + +```bash +npm publish --workspace packages/events --access public +``` + +Repeat for each package in order. + +## Upstream dependency note + +Berry Protocol no longer needs local vendored folders to be publishable. + +The public packages depend on: + +- `baileys` +- `@wppconnect/wa-proto` + +through normal npm versions, which keeps the public Berry repository clean and avoids shipping cloned repositories as part of the package source tree. diff --git a/docs/realtime.mdx b/docs/realtime.mdx new file mode 100644 index 0000000..9c85c81 --- /dev/null +++ b/docs/realtime.mdx @@ -0,0 +1,28 @@ +# Realtime + +BerryAPI exposes a WebSocket stream for live instance events. + +## Connect + +```text +ws://localhost:3000/ws?apiKey=berryapi_dev_key +``` + +To filter by instance: + +```text +ws://localhost:3000/ws?apiKey=berryapi_dev_key&instanceName=store-01 +``` + +## Event Types + +- `connection.update` +- `qrcode.updated` +- `messages.upsert` +- `messages.update` +- `messages.delete` +- `chats.update` +- `contacts.update` +- `groups.update` +- `presence.update` +- `send.message` diff --git a/docs/webhooks.mdx b/docs/webhooks.mdx new file mode 100644 index 0000000..d749783 --- /dev/null +++ b/docs/webhooks.mdx @@ -0,0 +1,24 @@ +# Webhooks + +Configure one webhook per instance. + +## Endpoints + +- `POST /webhook/set/:instanceName` +- `GET /webhook/find/:instanceName` + +## Example + +```json +{ + "enabled": true, + "url": "https://example.com/webhooks/berryapi", + "events": [ + "connection.update", + "messages.upsert", + "send.message" + ] +} +``` + +BerryAPI dispatches normalized envelopes for supported events. diff --git a/docs/whatsapp-rendering-notes.md b/docs/whatsapp-rendering-notes.md new file mode 100644 index 0000000..0884666 --- /dev/null +++ b/docs/whatsapp-rendering-notes.md @@ -0,0 +1,443 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +# WhatsApp Rendering Notes + +This document records the BerryProtocol changes that made legacy list messages, reply buttons, copy-code buttons, and CTA URL buttons render correctly in WhatsApp Web and on mobile devices, plus the current status of native-flow interactivity. + +## Goal + +Fix the WhatsApp client error: + +- `Não foi possível carregar a mensagem. Use seu celular para acessá-la.` + +The problem was not basic delivery. Text messages already reached `delivered`. The issue was the message rendering path used by rich message types such as: + +- `interactiveMessage` +- `nativeFlowMessage` +- `buttonsMessage` +- `listMessage` + +## What worked + +### Text + +Normal text sent through `sock.sendMessage(...)` delivered correctly. + +### Legacy list + +Legacy `listMessage` started rendering after Berry switched to: + +1. `proto.Message.ListMessage.fromObject(...)` +2. `listType: SINGLE_SELECT` +3. `relayMessage(...)` with `additionalNodes`: + +```xml + + + +``` + +This was the breakthrough that made the list render on both WhatsApp Web and mobile. + +## Current status + +### Legacy list + +Status: working + +Berry now sends lists through the legacy path by default: + +- file: [packages/wa-message/src/index.ts](C:/Users/felip/BerryProtocol/packages/wa-message/src/index.ts) +- file: [packages/socket/src/index.ts](C:/Users/felip/BerryProtocol/packages/socket/src/index.ts) + +Relevant builder: + +```ts +listMessage: proto.Message.ListMessage.fromObject({ + title, + description, + buttonText, + listType: proto.Message.ListMessage.ListType.SINGLE_SELECT, + footerText, + sections, +}) +``` + +Relevant relay metadata: + +```ts +additionalNodes: [ + { + tag: "biz", + attrs: {}, + content: [ + { + tag: "list", + attrs: { type: "product_list", v: "2" }, + }, + ], + }, +] +``` + +### Reply buttons + +Status: working + +Berry reply buttons render correctly when sent through native-flow interactive payloads using `quick_reply` buttons plus the `biz/interactive/native_flow` relay metadata. + +Working builder: + +```ts +interactivePayloadToMessageContent({ + body: { + text, + }, + footer: footer ? { text: footer } : undefined, + nativeFlowMessage: { + buttons: buttons.map((button) => ({ + name: "quick_reply", + buttonParamsJson: JSON.stringify({ + display_text: button.title, + id: button.id, + }), + })), + messageParamsJson: "", + messageVersion: 1, + }, +}) +``` + +Working relay metadata: + +```ts +additionalNodes: [ + { + tag: "biz", + attrs: {}, + content: [ + { + tag: "interactive", + attrs: { type: "native_flow", v: "1" }, + content: [ + { + tag: "native_flow", + attrs: { v: "9", name: "mixed" }, + }, + ], + }, + ], + }, +] +``` + +Legacy builder kept as historical reference: + +```ts +buttonsMessage: proto.Message.ButtonsMessage.fromObject({ + contentText: text, + footerText: footer, + buttons: [ + { + buttonId, + buttonText: { displayText }, + type: proto.Message.ButtonsMessage.Button.Type.RESPONSE, + }, + ], + headerType: proto.Message.ButtonsMessage.HeaderType.EMPTY, +}) +``` + +### Copy code buttons + +Status: working + +Berry copy-code buttons render correctly when sent through the same native-flow interactive path used for reply buttons, but with the native button name `cta_copy`. + +Working builder: + +```ts +interactivePayloadToMessageContent({ + body: { + text, + }, + footer: footer ? { text: footer } : undefined, + nativeFlowMessage: { + buttons: [ + { + name: "cta_copy", + buttonParamsJson: JSON.stringify({ + display_text: title, + copy_code: code, + }), + }, + ], + messageParamsJson: "", + messageVersion: 1, + }, +}) +``` + +Working relay metadata: + +```ts +additionalNodes: [ + { + tag: "biz", + attrs: {}, + content: [ + { + tag: "interactive", + attrs: { type: "native_flow", v: "1" }, + content: [ + { + tag: "native_flow", + attrs: { v: "9", name: "mixed" }, + }, + ], + }, + ], + }, +] +``` + +### CTA URL buttons + +Status: working + +Berry CTA URL buttons render correctly when sent through the same native-flow interactive path used for reply and copy-code buttons, but with the native button name `cta_url`. + +Working builder: + +```ts +interactivePayloadToMessageContent({ + body: { + text, + }, + footer: footer ? { text: footer } : undefined, + nativeFlowMessage: { + buttons: [ + { + name: "cta_url", + buttonParamsJson: JSON.stringify({ + display_text: title, + url, + merchant_url: url, + }), + }, + ], + messageParamsJson: "", + messageVersion: 1, + }, +}) +``` + +Working relay metadata: + +```ts +additionalNodes: [ + { + tag: "biz", + attrs: {}, + content: [ + { + tag: "interactive", + attrs: { type: "native_flow", v: "1" }, + content: [ + { + tag: "native_flow", + attrs: { v: "9", name: "mixed" }, + }, + ], + }, + ], + }, +] +``` + +### Native-flow interactive messages + +Status: still unstable for current clients + +Berry now uses: + +1. `proto.Message.InteractiveMessage.create(...)` +2. `relayMessage(...)` +3. `additionalNodes`: + +```xml + + + + + +``` + +Current relay metadata: + +```ts +additionalNodes: [ + { + tag: "biz", + attrs: {}, + content: [ + { + tag: "interactive", + attrs: { type: "native_flow", v: "1" }, + content: [ + { + tag: "native_flow", + attrs: { v: "9", name: "mixed" }, + }, + ], + }, + ], + }, +] +``` + +Important note: + +- Berry no longer wraps this interactive payload in `viewOnceMessage` for the native-flow path. +- This change was based on recent reports that the wrapper can break rendering in newer clients. +- The same native-flow relay path is now also used for working reply, copy-code, and CTA URL buttons. + +## Files involved + +### Core builders + +- [packages/wa-message/src/index.ts](C:/Users/felip/BerryProtocol/packages/wa-message/src/index.ts) + +Contains: + +- `listToLegacyListMessageContent` +- `legacyListAdditionalNodes` +- `buttonsPayloadToNativeFlowInteractiveContent` +- `buttonsPayloadToLegacyButtonsMessageContent` +- `interactivePayloadToMessageContent` +- `interactiveNativeFlowAdditionalNodes` + +### Transport + +- [packages/socket/src/index.ts](C:/Users/felip/BerryProtocol/packages/socket/src/index.ts) + +Contains: + +- `sendListMessage(...)` +- `sendReplyButtonsMessage(...)` +- `sendInteractiveMessage(...)` + +These methods call `generateWAMessageFromContent(...)` and then `relayMessage(...)` with the required extra nodes. + +## Why list started working + +The most important changes were: + +1. stop using `interactiveMessage + single_select` as the default list format +2. use legacy `listMessage` +3. set `listType: SINGLE_SELECT` +4. inject the `biz/list` node through `additionalNodes` + +That combination was what finally rendered correctly in current WhatsApp Web and mobile tests. + +## Safe defaults for Berry right now + +Use these as the default public behavior: + +- `list` -> legacy `listMessage` +- `listMessage` -> legacy `listMessage` +- `buttonsMessage` -> native-flow interactive buttons +- `interactiveMessage` -> explicit advanced path only + +Working button variants already validated: + +- `quick_reply` +- `cta_copy` +- `cta_url` + +This keeps the public API stable while steering users into the paths that currently render best. + +## Test flow used + +Primary test script: + +- [examples/sdk/test-berry-new-ways.js](C:/Users/felip/BerryProtocol/examples/sdk/test-berry-new-ways.js) + +Baileys reference test: + +- [examples/sdk/test-baileys-same-session.js](C:/Users/felip/BerryProtocol/examples/sdk/test-baileys-same-session.js) + +Payload comparison: + +- [examples/sdk/compare-berry-vs-baileys.js](C:/Users/felip/BerryProtocol/examples/sdk/compare-berry-vs-baileys.js) + +## Next steps + +Priority order: + +1. document and stabilize the validated button variants +2. add documented helpers for working button types +3. expand to other advanced interactive formats +4. keep a compatibility matrix for Web/Desktop/mobile behavior + +## Practical conclusion + +To make WhatsApp rich messages render in current clients, payload shape alone is not enough. The relay path and the extra business nodes matter. + +The working rule discovered so far is: + +- use legacy list for list UIs +- use native-flow quick reply for reply buttons +- use native-flow `cta_copy` for copy-code buttons +- use native-flow `cta_url` for CTA URL buttons +- use native-flow only as an explicit advanced path with the correct `additionalNodes` + +## AI label + +Status: working in real private-chat tests + +Berry now supports an experimental AI label path in the patched local Baileys flow. + +Working usage: + +```ts +await sock.sendMessage("5511999999999@s.whatsapp.net", { + text: "Ola! Essa mensagem deve aparecer com label de AI.", + ai: true, +}); +``` + +Rules validated so far: + +- only private chats are allowed +- groups, newsletters, status, and non-user JIDs must be blocked +- the message must receive `messageContextInfo.supportPayload` +- the relay must include: + +```xml + +``` + +Implementation status in this workspace: + +- `ai?: boolean` was added to the accepted message content type in the local patched Baileys install +- `sendMessage(...)` removes `content.ai` before the final payload is generated +- `relayMessage(...)` injects the `bot` additional node +- BerryProtocol forwards `ai: true` on supported `sendMessage(...)` paths + +Local test files: + +- [examples/sdk/test-baileys-ai-label.js](C:/Users/felip/BerryProtocol/examples/sdk/test-baileys-ai-label.js) +- [examples/sdk/test-berry-ai-label.ts](C:/Users/felip/BerryProtocol/examples/sdk/test-berry-ai-label.ts) + +Distribution note: + +- the publish-safe strategy now lives in [packages/socket/scripts/patch-installed-baileys.mjs](C:/Users/felip/BerryProtocol/packages/socket/scripts/patch-installed-baileys.mjs) +- `@berrysdk/socket` runs this patch in `postinstall` +- this makes the AI label patch travel with the published Berry packages that depend on `@berrysdk/socket` +- the feature should still be treated as experimental because WhatsApp can change the required metadata at any time diff --git a/examples/ai-agent.js b/examples/ai-agent.js new file mode 100644 index 0000000..eb6de9d --- /dev/null +++ b/examples/ai-agent.js @@ -0,0 +1,41 @@ +import BerryProtocol from "berryprotocol"; +import OpenAI from "openai"; + +const client = new BerryProtocol({ + sessionId: "ai-agent-example", +}); + +const ai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +client.on("message.received", async (message) => { + if (message.type !== "text" || !message.from) { + return; + } + + const response = await ai.chat.completions.create({ + model: process.env.OPENAI_MODEL ?? "gpt-4.1-mini", + messages: [ + { + role: "system", + content: "You are BerryAgent, the official AI assistant for BerryProtocol.", + }, + { + role: "user", + content: message.text, + }, + ], + }); + + const answer = response.choices[0]?.message?.content ?? "I could not generate a reply."; + const chatId = message.chatId ?? message.from; + + await client.sendMessage(chatId, { + text: answer, + ai: true, + }); +}); + +await client.connectWithQr(); +await client.waitUntilConnected(); diff --git a/examples/basic-bot.js b/examples/basic-bot.js new file mode 100644 index 0000000..2531e46 --- /dev/null +++ b/examples/basic-bot.js @@ -0,0 +1,25 @@ +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "basic-bot-example", +}); + +client.on("auth.qr", ({ value }) => { + console.log("Scan this QR code:", value); +}); + +client.on("message.received", async (message) => { + if (message.type !== "text" || !message.from) { + return; + } + + const chatId = message.chatId ?? message.from; + const text = message.text.trim().toLowerCase(); + + if (text === "ping") { + await client.sendText(chatId, "pong"); + } +}); + +await client.connectWithQr(); +await client.waitUntilConnected(); diff --git a/examples/npm-consumer/package-lock.json b/examples/npm-consumer/package-lock.json new file mode 100644 index 0000000..adb41b1 --- /dev/null +++ b/examples/npm-consumer/package-lock.json @@ -0,0 +1,1792 @@ +{ + "name": "berrysdk-npm-consumer-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "berrysdk-npm-consumer-example", + "dependencies": { + "@berrysdk/core": "^0.1.0" + } + }, + "node_modules/@berrysdk/auth": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@berrysdk/auth/-/auth-0.1.0.tgz", + "integrity": "sha512-Sn9zQLXqeOvWR2PEvrbqxy+PsLBqCLgW/wco+Jnqe3VoYpCjxsQaDBs/ywnTkU499JgZZLhYrp77ympxWB6i0Q==", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/events": "0.1.0", + "@berrysdk/store": "0.1.0" + } + }, + "node_modules/@berrysdk/core": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@berrysdk/core/-/core-0.1.0.tgz", + "integrity": "sha512-6uNFIZ+WUiZh1b+FwhWv2IA1pbKsx4+fsW9l+hieNGot44mDuPdgXuLHKCTjVqqNutnYMGyvk7Q3M3sfjCxZsQ==", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/auth": "0.1.0", + "@berrysdk/events": "0.1.0", + "@berrysdk/media": "0.1.0", + "@berrysdk/messages": "0.1.0", + "@berrysdk/socket": "0.1.0", + "@berrysdk/store": "0.1.0", + "pino": "^9.6.0" + } + }, + "node_modules/@berrysdk/events": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@berrysdk/events/-/events-0.1.0.tgz", + "integrity": "sha512-UOBmat6mHliU5xxD3gw4zOZ/zGyGX41ewG6i70qIaRTNyY1rMmeMcryoLpzeOQH4l5mjEFsSmriBrQKc/bc7hA==", + "license": "Apache-2.0" + }, + "node_modules/@berrysdk/media": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@berrysdk/media/-/media-0.1.0.tgz", + "integrity": "sha512-tLSSRj4QpE26yFywjFVtPCsKaSE07ph7ONzs0omLwXzyRhp0621yUbrsJm2cIiaKM533u25luc5kXsdoDakb5w==", + "license": "Apache-2.0" + }, + "node_modules/@berrysdk/messages": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@berrysdk/messages/-/messages-0.1.0.tgz", + "integrity": "sha512-96HqJ+x7iFQWIwd5UemqryNeELyJ9MJxhwSNRCJh/xOXAEO6nw/Txmek0bZkSZXjtaSbe+93L0BSxFkl+syLsg==", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/events": "0.1.0" + } + }, + "node_modules/@berrysdk/proto": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@berrysdk/proto/-/proto-0.1.0.tgz", + "integrity": "sha512-Qsje4jm1PU2R3Fjz+sfgZMIiFrtd6cwuRgJ3+jfah09YV9hCectOZBMNDn6o6c7TDlFg+j4XDxVagNCRX+wi7A==", + "license": "Apache-2.0", + "dependencies": { + "@wppconnect/wa-proto": "^2.3000.1030818676", + "protobufjs": "^7.4.0" + } + }, + "node_modules/@berrysdk/socket": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@berrysdk/socket/-/socket-0.1.0.tgz", + "integrity": "sha512-5kRG9Smk1GGxR6fdngW69lyBDkprSflAIL2kqeg40QQ7nc8NNxxOMrTxKWh3j/9VyIDD0wOqKRlTzT2TniVD9A==", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/events": "0.1.0", + "@berrysdk/proto": "0.1.0", + "@berrysdk/wa-message": "0.1.0", + "baileys": "7.0.0-rc10", + "pino": "^9.6.0", + "ws": "^8.18.1" + } + }, + "node_modules/@berrysdk/store": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@berrysdk/store/-/store-0.1.0.tgz", + "integrity": "sha512-f8kS3SbaEWvTCI33ZqqXfOIpXkZGTwNdBkZcqKsYvviOpn2zxNsx7zoPbZphUaZgpiMel92zfY2+O2g3IX9T+A==", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/events": "0.1.0", + "better-sqlite3": "^11.10.0" + } + }, + "node_modules/@berrysdk/wa-message": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@berrysdk/wa-message/-/wa-message-0.1.0.tgz", + "integrity": "sha512-5v7V+d+D+oGU+FPS1KmATm6tkbvGcjanZ4fEpuP9pTCC/cteFjFWX7FxdlabsELD042lPiQauy1jdVD+WeyOZQ==", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/events": "0.1.0", + "@berrysdk/proto": "0.1.0", + "baileys": "7.0.0-rc10" + } + }, + "node_modules/@borewit/text-codec": { + "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", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.8.tgz", + "integrity": "sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==", + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.4.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/node-cache": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz", + "integrity": "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==", + "license": "MIT", + "dependencies": { + "cacheable": "^2.3.1", + "hookified": "^1.14.0", + "keyv": "^5.5.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", + "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@hapi/boom": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", + "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "9.x.x" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "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==", + "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==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "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/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "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", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.21.0" + } + }, + "node_modules/@wppconnect/wa-proto": { + "version": "2.3000.1030818676", + "resolved": "https://registry.npmjs.org/@wppconnect/wa-proto/-/wa-proto-2.3000.1030818676.tgz", + "integrity": "sha512-HiSaUvkJ2sdbZZXLV7mL1EYnnNlACqclFbD8iyG+G3n9uNFPM/j4S4pQW7dEw4k/V1TxUc5gbfucDfpDwHj9Og==", + "license": "Apache-2.0", + "dependencies": { + "long": "^5.3.2", + "protobufjs": "^7.4.0" + } + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/baileys": { + "version": "7.0.0-rc10", + "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc10.tgz", + "integrity": "sha512-B1ShVyha4iZP7aNyz4QsXFceExzkvLFgISQGaDGHQaYFxIdNqoen+xKmlo1iWEsaexvQORAvUK44VicRi1T94g==", + "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", + "lru-cache": "^11.1.0", + "music-metadata": "^11.12.3", + "p-queue": "^9.0.0", + "pino": "^9.6", + "protobufjs": "^7.5.6", + "whatsapp-rust-bridge": "0.5.3", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "audio-decode": "^2.1.3", + "jimp": "^1.6.1", + "link-preview-js": "^3.0.0", + "sharp": "*" + }, + "peerDependenciesMeta": { + "audio-decode": { + "optional": true + }, + "jimp": { + "optional": true + }, + "link-preview-js": { + "optional": true + } + } + }, + "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==", + "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" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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.1.13" + } + }, + "node_modules/cacheable": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz", + "integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==", + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.0.8", + "@cacheable/utils": "^2.4.1", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/curve25519-js": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz", + "integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=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/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-type": { + "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": { + "@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/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/libsignal": { + "version": "6.0.0", + "resolved": "git+ssh://git@github.com/whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7", + "license": "GPL-3.0", + "dependencies": { + "curve25519-js": "^0.0.4", + "protobufjs": "^7.5.5" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/music-metadata": { + "version": "11.12.3", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.12.3.tgz", + "integrity": "sha512-n6hSTZkuD59qWgHh6IP5dtDlDZQXoxk/bcA85Jywg8Z1iFrlNgl2+GTFgjZyn52W5UgQpV42V4XqrQZZAMbZTQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.2", + "@tokenizer/token": "^0.3.0", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "file-type": "^21.3.1", + "media-typer": "^1.1.0", + "strtok3": "^10.3.4", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0", + "win-guid": "^0.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-queue": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.2.0.tgz", + "integrity": "sha512-dWgLE8AH0HjQ9fe74pUkKkvzzYT18Inp4zra3lKHnnwqGvcfcUBrvF2EAVX+envufDNBOzpPq/IBUONDbI7+3g==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", + "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.1", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strtok3": { + "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" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/token-types": { + "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": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/whatsapp-rust-bridge": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/whatsapp-rust-bridge/-/whatsapp-rust-bridge-0.5.3.tgz", + "integrity": "sha512-Xb3GAgtWQQJ30oI4a4pjM4+YUeli9CMLTwTIewUrb+AJMFElIkiT5uo+j1Zhc+amiV0Jj+LfX76c/EEZirJbGA==", + "license": "MIT" + }, + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/examples/npm-consumer/package.json b/examples/npm-consumer/package.json new file mode 100644 index 0000000..cfb1cdb --- /dev/null +++ b/examples/npm-consumer/package.json @@ -0,0 +1,13 @@ +{ + "name": "berrysdk-npm-consumer-example", + "private": true, + "type": "module", + "description": "Example consumer app that installs Berry SDK packages from npm.", + "scripts": { + "start": "node test-from-npm.mjs" + }, + "dependencies": { + "berryprotocol": "^0.1.10", + "berryotp": "^0.1.9" + } +} diff --git a/examples/npm-consumer/test-from-npm.mjs b/examples/npm-consumer/test-from-npm.mjs new file mode 100644 index 0000000..5e81c79 --- /dev/null +++ b/examples/npm-consumer/test-from-npm.mjs @@ -0,0 +1,83 @@ +import BerryProtocol from "berryprotocol"; +import { BerryOTP } from "berryotp"; + +const recipient = process.env.BERRY_TEST_TO; + +if (!recipient) { + console.error("Set BERRY_TEST_TO before running this example."); + console.error('Example: $env:BERRY_TEST_TO="5511999999999@s.whatsapp.net"'); + process.exit(1); +} + +const client = new BerryProtocol({ + sessionId: "npm-consumer-session", +}); + +client.on("auth.qr", ({ value }) => { + console.log("\nQR payload received:\n"); + console.log(value); +}); + +client.on("connection.open", (state) => { + console.log("CONNECTED:", state); +}); + +client.on("auth.success", (session) => { + console.log("AUTH SUCCESS:", session); +}); + +client.on("message.ack", (ack) => { + console.log("MESSAGE ACK:", ack); +}); + +async function main() { + await client.connectWithQr(); + await client.waitUntilConnected(); + + const sent = await client.sendText(recipient, "Hello from berryprotocol on npm"); + console.log("TEXT SENT:", sent); + + const listSent = await client.sendList(recipient, { + title: "Menu Berry SDK", + text: "Escolha uma opcao", + footer: "via npm", + buttonText: "Abrir lista", + sections: [ + { + title: "Opcoes", + rows: [ + { + id: "opcao_1", + title: "Opcao 1", + description: "Primeira opcao", + }, + { + id: "opcao_2", + title: "Opcao 2", + description: "Segunda opcao", + }, + ], + }, + ], + }); + + console.log("LIST SENT:", listSent); + + const otp = BerryOTP.createLoginFlow(client, { + issuer: "BerryProtocol", + ttlMs: 2 * 60 * 1000, + mode: "copy-code", + }); + + const otpSent = await otp.sendLoginCode(recipient, { + userId: "npm-consumer-user", + metadata: { source: "npm-consumer" }, + }); + + console.log("OTP SENT:", otpSent); +} + +main().catch((error) => { + console.error("NPM consumer example failed:", error); + process.exitCode = 1; +}); diff --git a/examples/rag-agent.js b/examples/rag-agent.js new file mode 100644 index 0000000..9264018 --- /dev/null +++ b/examples/rag-agent.js @@ -0,0 +1,79 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import BerryProtocol from "berryprotocol"; +import OpenAI from "openai"; + +const client = new BerryProtocol({ + sessionId: "rag-agent-example", +}); + +const ai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, + baseURL: process.env.OPENAI_BASE_URL || undefined, +}); + +async function loadKnowledgeBase() { + const dataDir = path.resolve("./data"); + const files = await fs.readdir(dataDir); + const markdownFiles = files.filter((name) => name.endsWith(".md")); + + return Promise.all( + markdownFiles.map(async (name) => ({ + name, + content: await fs.readFile(path.join(dataDir, name), "utf8"), + })), + ); +} + +function retrieveTopMatches(documents, query) { + const lowered = query.toLowerCase(); + + return documents + .map((doc) => { + const score = lowered + .split(/\s+/) + .filter(Boolean) + .reduce((acc, token) => acc + (doc.content.toLowerCase().includes(token) ? 1 : 0), 0); + + return { ...doc, score }; + }) + .filter((doc) => doc.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, 4); +} + +const knowledgeBase = await loadKnowledgeBase(); + +client.on("message.received", async (message) => { + if (message.type !== "text" || !message.from) { + return; + } + + const chatId = message.chatId ?? message.from; + const matches = retrieveTopMatches(knowledgeBase, message.text); + const context = matches.map((doc) => `File: ${doc.name}\n${doc.content}`).join("\n\n---\n\n"); + + const completion = await ai.chat.completions.create({ + model: process.env.OPENAI_MODEL ?? "gpt-4.1-mini", + messages: [ + { + role: "system", + content: "You are BerryAgent. Answer only from the provided BerryProtocol knowledge base when possible.", + }, + { + role: "user", + content: `Knowledge base context:\n${context}\n\nQuestion:\n${message.text}`, + }, + ], + }); + + const answer = completion.choices[0]?.message?.content ?? "No answer available."; + + await client.sendMessage(chatId, { + text: answer, + ai: true, + }); +}); + +await client.connectWithQr(); +await client.waitUntilConnected(); diff --git a/examples/rest/requests.http b/examples/rest/requests.http new file mode 100644 index 0000000..810cf34 --- /dev/null +++ b/examples/rest/requests.http @@ -0,0 +1,93 @@ +POST http://localhost:3000/instances +Content-Type: application/json + +{ + "sessionId": "store-001", + "authMethod": "link" +} + +### + +GET http://localhost:3000/instances/store-001/link + +### + +POST http://localhost:3000/instances +Content-Type: application/json + +{ + "sessionId": "store-qr", + "authMethod": "qr" +} + +### + +GET http://localhost:3000/instances/store-qr/qr + +### + +POST http://localhost:3000/instances +Content-Type: application/json + +{ + "sessionId": "store-code", + "authMethod": "pairing_code", + "phoneNumber": "5516999999999" +} + +### + +GET http://localhost:3000/instances/store-code/pairing-code + +### + +POST http://localhost:3000/instances/store-001/send-text +Content-Type: application/json + +{ + "to": "5516999999999@s.whatsapp.net", + "text": "Olá pelo BerryProtocol!" +} + +### + +POST http://localhost:3000/instances/store-001/send-image +Content-Type: application/json + +{ + "to": "5516999999999@s.whatsapp.net", + "media": { + "url": "https://example.com/banner.jpg", + "caption": "Banner BerryProtocol" + } +} + +### + +POST http://localhost:3000/instances/store-001/send-list +Content-Type: application/json + +{ + "to": "5516999999999@s.whatsapp.net", + "list": { + "title": "Cardápio", + "text": "Escolha uma opção", + "buttonText": "Ver opções", + "sections": [ + { + "title": "Lanches", + "rows": [ + { + "id": "xburger", + "title": "X-Burger", + "description": "Pão, carne e queijo" + } + ] + } + ] + } +} + +### + +DELETE http://localhost:3000/instances/store-001/logout diff --git a/examples/sdk/basic.ts b/examples/sdk/basic.ts new file mode 100644 index 0000000..c2a10ae --- /dev/null +++ b/examples/sdk/basic.ts @@ -0,0 +1,16 @@ +import { BerryClient } from "@berrysdk/core"; + +async function main(): Promise { + const client = new BerryClient({ + sessionId: "store-001", + }); + + client.on("qr", (qr) => console.log(qr)); + client.on("message.received", (message) => console.log(message)); + + await client.connect(); + + await client.sendText("5516999999999@s.whatsapp.net", "Olá pelo BerryProtocol!"); +} + +void main(); diff --git a/examples/sdk/berryotp.js b/examples/sdk/berryotp.js new file mode 100644 index 0000000..52fdd4d --- /dev/null +++ b/examples/sdk/berryotp.js @@ -0,0 +1,389 @@ +import crypto from "node:crypto"; +import { EventEmitter } from "node:events"; + +export class BerryOTP extends EventEmitter { + constructor(client, options = {}) { + super(); + + if (!client) { + throw new Error("BerryOTP precisa de uma instância de BerryClient."); + } + + this.client = client; + + this.options = { + issuer: options.issuer ?? "BerryOTP", + codeLength: options.codeLength ?? 6, + ttlMs: options.ttlMs ?? 5 * 60 * 1000, + footer: options.footer ?? "Este código dura {minutes} minutos.", + editOnExpire: options.editOnExpire ?? true, + mode: options.mode ?? "stable", + maxAttempts: options.maxAttempts ?? 5, + rateLimitMs: options.rateLimitMs ?? 60 * 1000, + autoReplyOnDenied: options.autoReplyOnDenied ?? true, + }; + + this.records = new Map(); + this.lastSentByTo = new Map(); + + this._bindIncomingMessages(); + this._startCleaner(); + } + + async send(to, input = {}) { + this._assertTo(to); + this._checkRateLimit(to); + + const otpId = this._id(); + const ttlMs = input.ttlMs ?? this.options.ttlMs; + const code = String(input.code ?? this._generateCode(this.options.codeLength)); + const expiresAt = Date.now() + ttlMs; + + const salt = this._id(); + const codeHash = this._hashCode(code, salt); + + const purpose = input.purpose ?? "verificação"; + const footer = this._formatFooter(input.footer ?? this.options.footer, ttlMs); + + const text = [ + "🔐 Código de verificação", + "", + `Use o código abaixo para ${purpose}:`, + "", + `*${code}*`, + ].join("\n"); + + let sent; + + if (this.options.mode === "experimental-copy-code") { + sent = await this._sendExperimentalCopyCode(to, { + otpId, + text, + footer, + code, + }); + } else { + sent = await this._sendStableReplyButton(to, { + otpId, + text, + footer, + }); + } + + const messageId = sent?.id ?? sent?.key?.id ?? sent?.messageId; + + if (!messageId) { + throw new Error("Não foi possível obter o messageId da mensagem OTP enviada."); + } + + const record = { + id: otpId, + to, + codeHash, + salt, + messageId, + expiresAt, + status: "active", + attempts: 0, + metadata: input.metadata ?? {}, + createdAt: Date.now(), + }; + + this.records.set(otpId, record); + this.lastSentByTo.set(to, Date.now()); + + setTimeout(() => { + this._expire(otpId).catch((error) => { + this.emit("error", error); + }); + }, ttlMs); + + return { + id: otpId, + to, + code, + expiresAt: new Date(expiresAt), + messageId, + status: "sent", + }; + } + + async verify(to, code) { + this._assertTo(to); + + const record = this._findActiveByCode(to, code); + + if (!record) { + return { valid: false, reason: "not_found" }; + } + + if (record.status !== "active") { + return { valid: false, reason: record.status }; + } + + if (Date.now() > record.expiresAt) { + await this._expire(record.id); + return { valid: false, reason: "expired" }; + } + + if (record.attempts >= this.options.maxAttempts) { + record.status = "blocked"; + return { valid: false, reason: "too_many_attempts" }; + } + + const ok = this._safeCompare( + this._hashCode(String(code), record.salt), + record.codeHash, + ); + + record.attempts += 1; + + if (!ok) { + return { valid: false, reason: "invalid_code" }; + } + + record.status = "used"; + record.usedAt = Date.now(); + + this.emit("used", { + otpId: record.id, + to: record.to, + metadata: record.metadata, + }); + + return { + valid: true, + metadata: record.metadata, + }; + } + + async _sendStableReplyButton(to, { otpId, text, footer }) { + return this.client.sendMessage(to, { + buttonsMessage: { + text, + footer, + buttons: [ + { + id: `berryotp:not_requested:${otpId}`, + title: "Não pedi nenhum código", + }, + ], + }, + }); + } + + async _sendExperimentalCopyCode(to, { otpId, text, footer, code }) { + return this.client.sendMessage(to, { + buttonsMessage: { + text, + footer, + buttons: [ + { + id: `berryotp:copy:${otpId}`, + title: "Copiar código", + kind: "copy_code", + code, + }, + { + id: `berryotp:not_requested:${otpId}`, + title: "Não pedi nenhum código", + kind: "quick_reply", + }, + ], + }, + }); + } + + async _expire(otpId) { + const record = this.records.get(otpId); + + if (!record || record.status !== "active") { + return; + } + + if (Date.now() < record.expiresAt) { + return; + } + + record.status = "expired"; + record.expiredAt = Date.now(); + + this.emit("expired", { + otpId: record.id, + to: record.to, + metadata: record.metadata, + }); + + if (!this.options.editOnExpire) { + return; + } + + const expiredText = [ + "🔒 Código expirado", + "", + "Este código não é mais válido.", + "Solicite um novo código para continuar.", + ].join("\n"); + + try { + await this._editMessage(record.to, record.messageId, expiredText); + } catch (error) { + this.emit("warning", { + type: "edit_failed", + otpId: record.id, + error, + }); + } + } + + async _editMessage(to, messageId, text) { + if (typeof this.client.editMessage === "function") { + return this.client.editMessage(to, messageId, { text }); + } + + return this.client.sendMessage(to, { + text, + edit: messageId, + }); + } + + _bindIncomingMessages() { + if (typeof this.client.on !== "function") { + return; + } + + this.client.on("message.received", async (message) => { + const buttonId = + message?.buttonId ?? + message?.selectedButtonId ?? + message?.message?.buttonsResponseMessage?.selectedButtonId; + + if (!buttonId?.startsWith("berryotp:not_requested:")) { + return; + } + + const otpId = buttonId.replace("berryotp:not_requested:", ""); + const record = this.records.get(otpId); + + if (!record || record.status !== "active") { + return; + } + + record.status = "denied"; + record.deniedAt = Date.now(); + + this.emit("denied", { + otpId, + to: record.to, + metadata: record.metadata, + }); + + if (this.options.autoReplyOnDenied) { + await this.client.sendMessage(record.to, { + text: "Entendido. Esse código foi cancelado e não poderá ser usado.", + }); + } + }); + } + + _findActiveByCode(to, code) { + for (const record of this.records.values()) { + if (record.to !== to || record.status !== "active") { + continue; + } + + if (Date.now() > record.expiresAt) { + continue; + } + + const hash = this._hashCode(String(code), record.salt); + + if (this._safeCompare(hash, record.codeHash)) { + return record; + } + } + + return null; + } + + _checkRateLimit(to) { + const lastSentAt = this.lastSentByTo.get(to); + + if (!lastSentAt) { + return; + } + + const elapsed = Date.now() - lastSentAt; + + if (elapsed < this.options.rateLimitMs) { + const waitMs = this.options.rateLimitMs - elapsed; + throw new Error(`Aguarde ${Math.ceil(waitMs / 1000)}s antes de enviar outro OTP.`); + } + } + + _generateCode(length) { + let code = ""; + + for (let i = 0; i < length; i += 1) { + code += crypto.randomInt(0, 10).toString(); + } + + return code; + } + + _hashCode(code, salt) { + return crypto + .createHash("sha256") + .update(`${salt}:${code}`) + .digest("hex"); + } + + _safeCompare(a, b) { + const left = Buffer.from(String(a)); + const right = Buffer.from(String(b)); + + if (left.length !== right.length) { + return false; + } + + return crypto.timingSafeEqual(left, right); + } + + _formatFooter(template, ttlMs) { + const minutes = Math.max(1, Math.ceil(ttlMs / 60000)); + + return String(template) + .replaceAll("{minutes}", String(minutes)) + .replaceAll("{seconds}", String(Math.ceil(ttlMs / 1000))); + } + + _assertTo(to) { + if (!to || typeof to !== "string") { + throw new Error("Informe o destino do WhatsApp."); + } + } + + _id() { + return crypto.randomBytes(16).toString("hex"); + } + + _startCleaner() { + this.cleaner = setInterval(() => { + const now = Date.now(); + + for (const [id, record] of this.records.entries()) { + const keepUsedForMs = 10 * 60 * 1000; + + if (record.status !== "active" && now - record.createdAt > keepUsedForMs) { + this.records.delete(id); + } + } + }, 60 * 1000); + + if (typeof this.cleaner.unref === "function") { + this.cleaner.unref(); + } + } +} + +export default BerryOTP; \ No newline at end of file diff --git a/examples/sdk/carousel-ai-card-experimental.ts b/examples/sdk/carousel-ai-card-experimental.ts new file mode 100644 index 0000000..25b9f35 --- /dev/null +++ b/examples/sdk/carousel-ai-card-experimental.ts @@ -0,0 +1,206 @@ +import { BerryProtocol } from "../../packages/core/src/index.ts"; +import { + generateWAMessageFromContent, + prepareWAMessageMedia, + proto, +} from "../../packages/transport/src/index.ts"; +import { interactiveNativeFlowAdditionalNodes } from "../../packages/wa-message/src/index.ts"; + +const to = process.env.BERRY_TEST_TO; +const imageUrl = + process.env.BERRY_TEST_IMAGE_URL ?? + "https://images.unsplash.com/photo-1513104890138-7c749659a591?auto=format&fit=crop&w=900&q=80"; + +if (!to) { + console.error("Defina BERRY_TEST_TO antes de executar o teste experimental de AI por card."); + process.exit(1); +} + +const client = new BerryProtocol({ + sessionId: "carousel-ai-card-experimental", +}); + +const botNode = { + tag: "bot", + attrs: { + biz_bot: "1", + }, +}; + +const buildCard = async ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sock: any, + title: string, + body: string, + footer: string, + id: string, +) => { + const media = await prepareWAMessageMedia( + { + image: { + url: imageUrl, + }, + }, + { + upload: sock.waUploadToServer, + }, + ); + + return proto.Message.InteractiveMessage.create({ + header: { + title, + hasMediaAttachment: true, + imageMessage: media.imageMessage, + }, + body: { + text: body, + }, + footer: { + text: footer, + }, + nativeFlowMessage: { + buttons: [ + { + name: "quick_reply", + buttonParamsJson: JSON.stringify({ + display_text: "Escolher", + id, + }), + }, + ], + messageParamsJson: "", + messageVersion: 1, + }, + contextInfo: { + isForwarded: true, + forwardingScore: 1, + forwardedAiBotMessageInfo: { + botName: "Berry AI Card", + botJid: "berry-ai-card@bot", + creatorName: "BerrySDK", + }, + }, + }); +}; + +const sendExperimentalCarousel = async ( + variant: + | "per-card-only" + | "top-level-plus-card" + | "interactive-context-only" + | "full-stack", +) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const berrySocket = (client as any).socket; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sock = berrySocket?.sock as any; + + if (!sock?.user?.id) { + throw new Error("Socket interno nao esta pronto para o teste experimental."); + } + + const cards = await Promise.all([ + buildCard(sock, "Card AI 1", "Tentativa de metadata por card", "BerrySDK", "ai_card_1"), + buildCard(sock, "Card AI 2", "Segunda variacao experimental", "BerrySDK", "ai_card_2"), + ]); + + const outerMessageContextInfo: Record = { + deviceListMetadata: {}, + deviceListMetadataVersion: 2, + }; + + const additionalNodes = [...interactiveNativeFlowAdditionalNodes()]; + const interactiveContextInfo = + variant === "interactive-context-only" || variant === "full-stack" + ? { + isForwarded: true, + forwardingScore: 1, + forwardedAiBotMessageInfo: { + botName: "Berry AI Carousel", + botJid: "berry-ai-carousel@bot", + creatorName: "BerrySDK", + }, + } + : undefined; + + if (variant === "top-level-plus-card" || variant === "full-stack") { + outerMessageContextInfo.supportPayload = "{}"; + additionalNodes.push(botNode); + } + + const interactiveMessageAdditionalMetadata = + variant === "full-stack" + ? { + isGalaxyFlowCompleted: true, + } + : undefined; + + const bodyTextByVariant: Record = { + "per-card-only": "Experimental: metadata de IA somente nos cards", + "top-level-plus-card": "Experimental: AI label no topo + metadata nos cards", + "interactive-context-only": "Experimental: contextInfo no interactiveMessage", + "full-stack": "Experimental: topo + interactive context + card metadata", + }; + + const message = generateWAMessageFromContent( + to, + { + viewOnceMessage: { + message: { + messageContextInfo: outerMessageContextInfo, + ...(interactiveMessageAdditionalMetadata + ? { + interactiveMessageAdditionalMetadata, + } + : {}), + interactiveMessage: proto.Message.InteractiveMessage.create({ + body: { + text: bodyTextByVariant[variant], + }, + footer: { + text: "BerryProtocol experimental", + }, + ...(interactiveContextInfo ? { contextInfo: interactiveContextInfo } : {}), + carouselMessage: { + cards, + messageVersion: 1, + }, + }), + }, + }, + }, + { + userJid: sock.user.id, + }, + ); + + await sock.relayMessage(to, message.message, { + messageId: message.key.id, + additionalNodes, + }); + + return message.key.id; +}; + +client.once("connection.open", async () => { + try { + const perCardOnly = await sendExperimentalCarousel("per-card-only"); + console.log("EXPERIMENTAL CARD AI ONLY:", perCardOnly); + + const topLevelPlusCard = await sendExperimentalCarousel("top-level-plus-card"); + console.log("EXPERIMENTAL TOP LEVEL + CARD AI:", topLevelPlusCard); + + const interactiveContextOnly = await sendExperimentalCarousel("interactive-context-only"); + console.log("EXPERIMENTAL INTERACTIVE CONTEXT AI:", interactiveContextOnly); + + const fullStack = await sendExperimentalCarousel("full-stack"); + console.log("EXPERIMENTAL FULL STACK AI:", fullStack); + } catch (error) { + console.error("Falha no teste experimental de AI por card:", error); + } finally { + await client.disconnect().catch(() => undefined); + process.exit(0); + } +}); + +await client.connectWithQr(); diff --git a/examples/sdk/carousel-ai.ts b/examples/sdk/carousel-ai.ts new file mode 100644 index 0000000..10f694c --- /dev/null +++ b/examples/sdk/carousel-ai.ts @@ -0,0 +1,51 @@ +import { BerryProtocol } from "../../packages/core/src/index.ts"; + +const to = process.env.BERRY_TEST_TO; +const imageUrl = + process.env.BERRY_TEST_IMAGE_URL ?? + "https://images.unsplash.com/photo-1513104890138-7c749659a591?auto=format&fit=crop&w=900&q=80"; + +if (!to) { + console.error("Defina BERRY_TEST_TO antes de executar o exemplo de carousel com AI label."); + process.exit(1); +} + +const client = new BerryProtocol({ + sessionId: "carousel-ai-example", +}); + +client.once("connection.open", async () => { + try { + const sent = await client.sendCarousel(to, { + text: "Confira nossas pizzas com sugestoes por IA", + footer: "BerryProtocol AI", + ai: true, + carouselCardType: "image", + cards: [ + { + title: "Pizza Calabresa", + body: "Recomendada pela IA", + footer: "R$ 39,90", + image: { url: imageUrl }, + buttons: [{ id: "pizza_calabresa", title: "Escolher", kind: "quick_reply" }], + }, + { + title: "Pizza Frango", + body: "Sugestao equilibrada", + footer: "R$ 42,90", + image: { url: imageUrl }, + buttons: [{ title: "Abrir site", kind: "cta_url", url: "https://example.com/cardapio" }], + }, + ], + }); + + console.log("CAROUSEL AI:", sent.id); + } catch (error) { + console.error("Falha ao enviar carousel com AI label:", error); + } finally { + await client.disconnect().catch(() => undefined); + process.exit(0); + } +}); + +await client.connectWithQr(); diff --git a/examples/sdk/carousel-mixed.ts b/examples/sdk/carousel-mixed.ts new file mode 100644 index 0000000..8e8eaff --- /dev/null +++ b/examples/sdk/carousel-mixed.ts @@ -0,0 +1,64 @@ +import { BerryProtocol } from "../../packages/core/src/index.ts"; + +const to = process.env.BERRY_TEST_TO; + +if (!to) { + console.log("Set BERRY_TEST_TO before running this example."); + process.exit(0); +} + +const client = new BerryProtocol({ + sessionId: "carousel-mixed-example", +}); + +client.once("connection.open", async () => { + await client.sendCarousel(to, { + text: "Cards mistos com imagem, video e botoes nativos", + footer: "BerryProtocol carousel", + carouselCardType: "mixed", + cards: [ + { + title: "Combo familia", + body: "Imagem do combo com refrigerante", + footer: "R$ 74,90", + image: { + url: "https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?auto=format&fit=crop&w=900&q=80", + }, + buttons: [ + { + kind: "copy_code", + title: "Copiar cupom", + code: "FAMILIA10", + }, + ], + }, + { + title: "Teaser forno", + body: "Video dos bastidores", + footer: "Veja agora", + video: { + url: "https://www.w3schools.com/html/movie.mp4", + mimetype: "video/mp4", + }, + buttons: [ + { + name: "quick_reply", + buttonParamsJson: JSON.stringify({ + display_text: "Pedir agora", + id: "pedido_agora", + }), + }, + { + kind: "cta_url", + title: "Ver cardapio", + url: "https://example.com/menu", + }, + ], + }, + ], + }); + + console.log("Carousel misto enviado."); +}); + +await client.connectWithQr(); diff --git a/examples/sdk/carousel-video.ts b/examples/sdk/carousel-video.ts new file mode 100644 index 0000000..6394beb --- /dev/null +++ b/examples/sdk/carousel-video.ts @@ -0,0 +1,42 @@ +import { BerryProtocol } from "../../packages/core/src/index.ts"; + +const to = process.env.BERRY_TEST_TO; + +if (!to) { + console.log("Set BERRY_TEST_TO before running this example."); + process.exit(0); +} + +const client = new BerryProtocol({ + sessionId: "carousel-video-example", +}); + +client.once("connection.open", async () => { + await client.sendCarousel(to, { + text: "Confira nossos videos promocionais", + footer: "Cada card pode ter video nativo", + carouselCardType: "video", + cards: [ + { + title: "Promo da casa", + body: "Video teaser da pizzaria", + footer: "Toque para ver", + video: { + url: "https://www.w3schools.com/html/mov_bbb.mp4", + mimetype: "video/mp4", + }, + buttons: [ + { + kind: "quick_reply", + id: "promo_casa", + title: "Quero essa promo", + }, + ], + }, + ], + }); + + console.log("Carousel de video enviado."); +}); + +await client.connectWithQr(); diff --git a/examples/sdk/carousel.ts b/examples/sdk/carousel.ts new file mode 100644 index 0000000..51bfa4d --- /dev/null +++ b/examples/sdk/carousel.ts @@ -0,0 +1,56 @@ +import { BerryProtocol } from "../../packages/core/src/index.ts"; + +const to = process.env.BERRY_TEST_TO; + +if (!to) { + console.log("Set BERRY_TEST_TO before running this example."); + process.exit(0); +} + +const client = new BerryProtocol({ + sessionId: "carousel-example", +}); + +client.once("connection.open", async () => { + await client.sendCarousel(to, { + text: "Confira nossas pizzas!", + footer: "Deslize para ver mais", + carouselCardType: "image", + cards: [ + { + title: "Pizza Calabresa", + body: "Calabresa premium com cebola roxa", + footer: "R$ 39,90", + image: { + url: "https://images.unsplash.com/photo-1513104890138-7c749659a591?auto=format&fit=crop&w=900&q=80", + }, + buttons: [ + { + kind: "quick_reply", + id: "pizza_calabresa", + title: "Escolher", + }, + ], + }, + { + title: "Pizza Margherita", + body: "Molho artesanal, mussarela e manjericao", + footer: "R$ 36,90", + image: { + url: "https://images.unsplash.com/photo-1574071318508-1cdbab80d002?auto=format&fit=crop&w=900&q=80", + }, + buttons: [ + { + kind: "cta_url", + title: "Abrir site", + url: "https://example.com/cardapio", + }, + ], + }, + ], + }); + + console.log("Carousel de imagem enviado."); +}); + +await client.connectWithQr(); diff --git a/examples/sdk/compare-berry-vs-baileys.js b/examples/sdk/compare-berry-vs-baileys.js new file mode 100644 index 0000000..208002b --- /dev/null +++ b/examples/sdk/compare-berry-vs-baileys.js @@ -0,0 +1,177 @@ +import { generateWAMessageFromContent } from "baileys"; +import { + buttonsPayloadToLegacyButtonsMessageContent, + buttonsPayloadToTemplateMessageContent, + interactivePayloadToMessageContent, + listToLegacyListMessageContent, + listToInteractiveMessageContent, + listToInteractivePayload, +} from "../../packages/wa-message/dist/index.js"; + +const recipient = process.env.BERRY_TEST_TO ?? "5511999999999@s.whatsapp.net"; +const userJid = process.env.BERRY_TEST_USER_JID ?? "5511999999999:31@s.whatsapp.net"; + +const sampleList = { + title: "Menu BerryProtocol", + text: "Escolha uma opcao na lista", + footer: "BerryProtocol", + buttonText: "Abrir lista", + sections: [ + { + title: "Lanches", + rows: [ + { + id: "xburger", + title: "X-Burger", + description: "Pao, carne e queijo", + }, + { + id: "xbacon", + title: "X-Bacon", + description: "Com bacon crocante", + }, + ], + }, + ], +}; + +const sampleButtons = { + text: "Escolha uma opcao", + footer: "BerryProtocol", + buttons: [ + { id: "reply_1", title: "Opcao 1" }, + { id: "reply_2", title: "Opcao 2" }, + ], +}; + +function printComparison(label, berryContent, baileysContent) { + const berry = generateWAMessageFromContent(recipient, berryContent, { userJid }); + const baileys = generateWAMessageFromContent(recipient, baileysContent, { userJid }); + + const berryJson = JSON.stringify(berry.message, null, 2); + const baileysJson = JSON.stringify(baileys.message, null, 2); + + console.log(`\n=== ${label} ===`); + console.log("\nBerry message:"); + console.log(berryJson); + console.log("\nBaileys message:"); + console.log(baileysJson); + console.log("\nEqual:", berryJson === baileysJson); +} + +function main() { + const berryInteractiveList = listToInteractiveMessageContent(sampleList); + const berryLegacyList = listToLegacyListMessageContent(sampleList); + const baileysInteractiveList = { + viewOnceMessage: { + message: { + messageContextInfo: { + deviceListMetadata: {}, + deviceListMetadataVersion: 2, + }, + interactiveMessage: { + header: { + title: sampleList.title, + hasMediaAttachment: false, + }, + body: { + text: sampleList.text, + }, + footer: { + text: sampleList.footer, + }, + nativeFlowMessage: { + buttons: [ + { + name: "single_select", + buttonParamsJson: JSON.stringify({ + title: sampleList.buttonText, + sections: sampleList.sections, + }), + }, + ], + messageParamsJson: "", + messageVersion: 1, + }, + }, + }, + }, + }; + const baileysLegacyList = { + listMessage: { + title: sampleList.title, + description: sampleList.text, + buttonText: sampleList.buttonText, + footerText: sampleList.footer, + sections: sampleList.sections.map((section) => ({ + title: section.title, + rows: section.rows.map((row) => ({ + rowId: row.id, + title: row.title, + description: row.description ?? "", + })), + })), + }, + }; + + const berryTemplateButtons = buttonsPayloadToTemplateMessageContent(sampleButtons); + const baileysTemplateButtons = { + templateMessage: { + hydratedTemplate: { + hydratedContentText: sampleButtons.text, + hydratedFooterText: sampleButtons.footer, + hydratedButtons: [ + { + index: 0, + quickReplyButton: { + displayText: "Opcao 1", + id: "reply_1", + }, + }, + { + index: 1, + quickReplyButton: { + displayText: "Opcao 2", + id: "reply_2", + }, + }, + ], + }, + }, + }; + const berryLegacyButtons = buttonsPayloadToLegacyButtonsMessageContent(sampleButtons); + const baileysLegacyButtons = { + buttonsMessage: { + contentText: sampleButtons.text, + footerText: sampleButtons.footer, + buttons: [ + { + buttonId: "reply_1", + buttonText: { + displayText: "Opcao 1", + }, + type: "RESPONSE", + }, + { + buttonId: "reply_2", + buttonText: { + displayText: "Opcao 2", + }, + type: "RESPONSE", + }, + ], + headerType: "EMPTY", + }, + }; + + const berryInteractive = interactivePayloadToMessageContent(listToInteractivePayload(sampleList)); + const baileysInteractive = baileysInteractiveList; + + printComparison("list legacy", berryLegacyList, baileysLegacyList); + printComparison("list interactive", berryInteractiveList, baileysInteractiveList); + printComparison("buttons legacy", berryLegacyButtons, baileysLegacyButtons); + printComparison("buttons template", berryTemplateButtons, baileysTemplateButtons); + printComparison("interactive", berryInteractive, baileysInteractive); +} + +main(); diff --git a/examples/sdk/list-and-reply.js b/examples/sdk/list-and-reply.js new file mode 100644 index 0000000..f61ed84 --- /dev/null +++ b/examples/sdk/list-and-reply.js @@ -0,0 +1,142 @@ +import { BerryClient } from "../../packages/core/dist/index.js"; +import qrcode from "qrcode-terminal"; + +const sessionId = process.env.BERRY_TEST_SESSION_ID ?? "test-session-qr"; +const recipient = process.env.BERRY_TEST_TO ?? "5519997530219@s.whatsapp.net"; +const authMethod = process.env.BERRY_TEST_AUTH ?? "qr"; +const pairingPhone = process.env.BERRY_TEST_PHONE; + +function waitForOpen(client) { + return new Promise((resolve) => { + client.once("connection.open", (state) => { + console.log("CONNECTED:", state); + resolve(state); + }); + }); +} + +async function safeTest(name, fn) { + try { + console.log(`\nTesting: ${name}`); + const result = await fn(); + console.log(`OK: ${name}`, result ?? ""); + } catch (error) { + console.log(`ERROR in ${name}:`, error instanceof Error ? error.message : error); + } +} + +function buildClient() { + return new BerryClient({ + sessionId, + authFolder: process.env.BERRY_AUTH_FOLDER, + auth: { + method: authMethod, + phoneNumber: pairingPhone, + }, + }); +} + +async function connectClient(client) { + if (authMethod === "pairing_code") { + if (!pairingPhone) { + throw new Error("Define BERRY_TEST_PHONE para usar pairing_code."); + } + + await client.connectWithPairingCode(pairingPhone); + return; + } + + if (authMethod === "link") { + await client.connectWithLink(); + return; + } + + await client.connectWithQr(); +} + +async function main() { + const client = buildClient(); + + client.on("auth.link", ({ value }) => { + console.log("\nLink gerado:"); + console.log(value); + }); + + client.on("auth.qr", ({ value }) => { + console.log("\nEscaneie este QR Code:"); + qrcode.generate(value, { small: true }); + }); + + client.on("auth.pairing_code", ({ code }) => { + console.log(`\nPairing code: ${code}`); + }); + + client.on("auth.success", (data) => { + console.log("AUTH SUCCESS:", data); + }); + + client.on("auth.error", (error) => { + console.log("AUTH ERROR:", error); + }); + + client.on("connection.close", (state) => { + console.log("DISCONNECTED:", state); + }); + + client.on("message.received", (message) => { + console.log("MESSAGE RECEIVED:", message); + }); + + console.log(`Connecting with auth=${authMethod}...`); + const opened = waitForOpen(client); + await connectClient(client); + await opened; + + await safeTest("sendMessage(list)", () => + client.sendMessage(recipient, { + list: { + title: "Menu BerryProtocol", + text: "Escolha uma opcao na lista", + footer: "BerryProtocol", + buttonText: "Abrir lista", + sections: [ + { + title: "Lanches", + rows: [ + { + id: "xburger", + title: "X-Burger", + description: "Pao, carne e queijo", + }, + { + id: "xbacon", + title: "X-Bacon", + description: "Com bacon crocante", + }, + ], + }, + ], + }, + }), + ); + + await safeTest("sendMessage(reply button)", () => + client.sendMessage(recipient, { + buttonsMessage: { + text: "Escolha uma opcao", + footer: "BerryProtocol", + buttons: [ + { id: "reply_1", title: "Opcao 1" }, + { id: "reply_2", title: "Opcao 2" }, + ], + }, + }), + ); + + console.log("\nDone."); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exitCode = 1; +}); diff --git a/examples/sdk/list-berry-jids.ts b/examples/sdk/list-berry-jids.ts new file mode 100644 index 0000000..b24bbb2 --- /dev/null +++ b/examples/sdk/list-berry-jids.ts @@ -0,0 +1,97 @@ +import { BerryProtocol } from "../../packages/core/src/index.ts"; + +type InternalSocket = { + newsletterMetadata?: ( + type: "invite" | "jid", + key: string, + ) => Promise< + | { + id?: string; + name?: string; + invite?: string; + subscribersCount?: number; + } + | null + >; +}; + +const client = new BerryProtocol({ + sessionId: "list-berry-jids", +}); + +const newsletterJid = process.env.BERRY_TEST_NEWSLETTER_JID?.trim(); +const newsletterInvite = process.env.BERRY_TEST_NEWSLETTER_INVITE?.trim(); + +const getInternalSock = (): InternalSocket | undefined => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const berrySocket = (client as any).socket; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return berrySocket?.sock as InternalSocket | undefined; +}; + +const printGroups = async () => { + const groups = await client.fetchGroups(); + + console.log(""); + console.log("=== GRUPOS ==="); + + if (!groups.length) { + console.log("Nenhum grupo encontrado nessa sessao."); + return; + } + + for (const group of groups.sort((a, b) => a.subject.localeCompare(b.subject))) { + console.log(`${group.subject} => ${group.id}`); + } +}; + +const printNewsletter = async () => { + const sock = getInternalSock(); + + if (!sock?.newsletterMetadata) { + console.log(""); + console.log("=== NEWSLETTER ==="); + console.log("O runtime atual nao expoe newsletterMetadata nessa sessao."); + return; + } + + if (!newsletterJid && !newsletterInvite) { + console.log(""); + console.log("=== NEWSLETTER ==="); + console.log( + "Para resolver JID de newsletter, defina BERRY_TEST_NEWSLETTER_JID ou BERRY_TEST_NEWSLETTER_INVITE.", + ); + return; + } + + const metadata = newsletterJid + ? await sock.newsletterMetadata("jid", newsletterJid) + : await sock.newsletterMetadata("invite", newsletterInvite!); + + console.log(""); + console.log("=== NEWSLETTER ==="); + + if (!metadata) { + console.log("Nao foi possivel resolver a newsletter informada."); + return; + } + + console.log(`Nome: ${metadata.name ?? "(sem nome)"}`); + console.log(`JID: ${metadata.id ?? "(nao retornado)"}`); + console.log(`Invite: ${metadata.invite ?? "(nao retornado)"}`); + if (typeof metadata.subscribersCount === "number") { + console.log(`Inscritos: ${metadata.subscribersCount}`); + } +}; + +client.once("connection.open", async () => { + try { + await printGroups(); + await printNewsletter(); + } finally { + await client.disconnect().catch(() => undefined); + process.exit(0); + } +}); + +await client.connectWithQr(); diff --git a/examples/sdk/list.ts b/examples/sdk/list.ts new file mode 100644 index 0000000..fd1f508 --- /dev/null +++ b/examples/sdk/list.ts @@ -0,0 +1,29 @@ +import { BerryClient } from "@berrysdk/core"; + +async function main(): Promise { + const client = new BerryClient({ + sessionId: "store-001", + }); + + await client.connect(); + + await client.sendList("5516999999999@s.whatsapp.net", { + title: "Cardápio", + text: "Escolha uma opção", + buttonText: "Ver opções", + sections: [ + { + title: "Lanches", + rows: [ + { + id: "xburger", + title: "X-Burger", + description: "Pão, carne e queijo", + }, + ], + }, + ], + }); +} + +void main(); diff --git a/examples/sdk/test-all.js b/examples/sdk/test-all.js new file mode 100644 index 0000000..1901064 --- /dev/null +++ b/examples/sdk/test-all.js @@ -0,0 +1,189 @@ +import { BerryClient } from "../../packages/core/dist/index.js"; +import qrcode from "qrcode-terminal"; + +const sessionId = process.env.BERRY_TEST_SESSION_ID ?? "test-session-qr"; +const recipient = process.env.BERRY_TEST_TO ?? "55199991466943@s.whatsapp.net"; +const authMethod = process.env.BERRY_TEST_AUTH ?? "qr"; +const pairingPhone = process.env.BERRY_TEST_PHONE; + +async function safeTest(name, fn) { + try { + console.log(`\n[Teste] ${name}`); + const result = await fn(); + console.log(`[OK] ${name}`, result ?? ""); + } catch (error) { + console.log( + `[ERRO] ${name}:`, + error instanceof Error ? error.message : error, + ); + } +} + +function buildClient() { + return new BerryClient({ + sessionId, + authFolder: process.env.BERRY_AUTH_FOLDER, + auth: { + method: authMethod, + phoneNumber: pairingPhone, + }, + }); +} + +async function connectClient(client) { + if (authMethod === "pairing_code") { + if (!pairingPhone) { + throw new Error("Defina BERRY_TEST_PHONE para usar pairing_code."); + } + + await client.connectWithPairingCode(pairingPhone); + return; + } + + if (authMethod === "link") { + await client.connectWithLink(); + return; + } + + await client.connectWithQr(); +} + +async function main() { + const client = buildClient(); + + client.on("auth.link", ({ value }) => { + console.log("\n[LINK]"); + console.log(value); + }); + + client.on("auth.qr", ({ value }) => { + console.log("\n[QR] Escaneie este QR Code:"); + qrcode.generate(value, { small: true }); + }); + + client.on("auth.pairing_code", ({ code }) => { + console.log(`\n[PAIRING CODE] ${code}`); + }); + + client.on("auth.success", (data) => { + console.log("[AUTH SUCCESS]", data); + }); + + client.on("auth.error", (error) => { + console.log("[AUTH ERROR]", error); + }); + + client.on("connection.open", (state) => { + console.log("[CONECTADO]", state); + }); + + client.on("connection.close", (state) => { + console.log("[DESCONECTADO]", state); + }); + + client.on("message.received", (message) => { + console.log("[MESSAGE RECEIVED]", message); + }); + + console.log(`[INICIO] Conectando com auth=${authMethod}...`); + + await connectClient(client); + await client.waitUntilConnected(); + + await safeTest("sendText", () => + client.sendText(recipient, "Teste de texto BerryProtocol"), + ); + + await safeTest("sendImage", () => + client.sendImage(recipient, { + url: "https://picsum.photos/600/400", + caption: "Teste de imagem BerryProtocol", + }), + ); + + await safeTest("sendAudio", () => + client.sendAudio(recipient, { + url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", + mimetype: "audio/mpeg", + }), + ); + + await safeTest("sendDocument", () => + client.sendDocument(recipient, { + url: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", + fileName: "teste.pdf", + mimetype: "application/pdf", + caption: "Teste de documento BerryProtocol", + }), + ); + + await safeTest("sendButtons", () => + client.sendButtons(recipient, { + text: "Escolha uma opcao", + footer: "BerryProtocol", + buttons: [ + { id: "btn_1", title: "Opcao 1" }, + { id: "btn_2", title: "Opcao 2" }, + ], + }), + ); + + await safeTest("sendList", () => + client.sendList(recipient, { + title: "Cardapio", + text: "Escolha um lanche", + footer: "BerryProtocol", + buttonText: "Abrir lista", + sections: [ + { + title: "Lanches", + rows: [ + { + id: "xburger", + title: "X-Burger", + description: "Pao, carne e queijo", + }, + { + id: "xbacon", + title: "X-Bacon", + description: "Com bacon crocante", + }, + ], + }, + ], + }), + ); + + await safeTest("sendLocation", () => + client.sendLocation(recipient, { + latitude: -21.7104, + longitude: -47.478, + name: "BerryProtocol HQ", + address: "Santa Rita do Passa Quatro - SP", + }), + ); + + await safeTest("sendContact", () => + client.sendContact(recipient, { + displayName: "BerryProtocol", + vcard: [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:BerryProtocol", + "TEL;type=CELL;type=VOICE;waid=5511999999999:+55 11 99999-9999", + "END:VCARD", + ].join("\n"), + }), + ); + + await safeTest("subscribePresence", () => client.subscribePresence(recipient)); + await safeTest("sendPresence", () => client.sendPresence("composing", recipient)); + await safeTest("fetchGroups", () => client.fetchGroups()); + + console.log("\n[FINALIZADO] Todos os testes executados."); +} + +main().catch((error) => { + console.error("[FATAL]", error); + process.exitCode = 1; +}); diff --git a/examples/sdk/test-baileys-ai-label.js b/examples/sdk/test-baileys-ai-label.js new file mode 100644 index 0000000..ae61562 --- /dev/null +++ b/examples/sdk/test-baileys-ai-label.js @@ -0,0 +1,101 @@ +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import pino from "pino"; +import qrcode from "qrcode-terminal"; +import makeWASocket, { + fetchLatestBaileysVersion, + makeCacheableSignalKeyStore, + useMultiFileAuthState, +} from "baileys"; + +const sessionId = process.env.BAILEYS_TEST_SESSION_ID ?? "baileys-test-session"; +const recipient = process.env.BAILEYS_TEST_TO ?? process.env.BERRY_TEST_TO; +const authBaseFolder = + process.env.BAILEYS_AUTH_FOLDER ?? join(process.cwd(), ".baileys-sessions"); +const sessionDir = join(authBaseFolder, sessionId); +const logger = pino({ level: "silent" }); + +if (!recipient) { + throw new Error('Set BAILEYS_TEST_TO or BERRY_TEST_TO, for example: $env:BAILEYS_TEST_TO="5511999999999@s.whatsapp.net"'); +} + +async function ensureSessionDir() { + await mkdir(sessionDir, { recursive: true }); +} + +function waitForOpen(sock) { + return new Promise((resolve, reject) => { + sock.ev.on("connection.update", (update) => { + if (update.qr) { + console.log("\nEscaneie este QR Code:"); + qrcode.generate(update.qr, { small: true }); + } + + if (update.connection === "open") { + console.log("CONNECTED"); + resolve(); + } + + if (update.connection === "close") { + reject(update.lastDisconnect?.error ?? new Error("Connection closed")); + } + }); + }); +} + +async function createSocket() { + await ensureSessionDir(); + + const { state, saveCreds } = await useMultiFileAuthState(sessionDir); + const { version } = await fetchLatestBaileysVersion(); + + const sock = makeWASocket({ + version, + logger, + auth: { + creds: state.creds, + keys: makeCacheableSignalKeyStore(state.keys, logger), + }, + printQRInTerminal: false, + syncFullHistory: false, + markOnlineOnConnect: false, + shouldSyncHistoryMessage: () => false, + }); + + sock.ev.on("creds.update", saveCreds); + sock.ev.on("messages.update", (updates) => { + for (const item of updates) { + console.log("ACK UPDATE:", { + id: item.key.id, + remoteJid: item.key.remoteJid, + status: item.update.status, + }); + } + }); + + return sock; +} + +async function main() { + console.log("Sessao do Baileys:", sessionId); + console.log("Destino:", recipient); + + const sock = await createSocket(); + await waitForOpen(sock); + + const result = await sock.sendMessage(recipient, { + text: "Ola! Essa mensagem deve aparecer com label de AI.", + ai: true, + }); + + console.log("AI MESSAGE SENT:", { + id: result?.key?.id, + remoteJid: result?.key?.remoteJid, + message: result?.message, + }); +} + +main().catch((error) => { + console.error("AI label test failed:", error); + process.exitCode = 1; +}); diff --git a/examples/sdk/test-baileys-same-session.js b/examples/sdk/test-baileys-same-session.js new file mode 100644 index 0000000..751800d --- /dev/null +++ b/examples/sdk/test-baileys-same-session.js @@ -0,0 +1,364 @@ +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import pino from "pino"; +import qrcode from "qrcode-terminal"; +import makeWASocket, { + fetchLatestBaileysVersion, + generateWAMessageFromContent, + makeCacheableSignalKeyStore, + proto, + useMultiFileAuthState, +} from "baileys"; + +const sessionId = process.env.BAILEYS_TEST_SESSION_ID ?? "baileys-test-session"; +const recipient = process.env.BAILEYS_TEST_TO ?? process.env.BERRY_TEST_TO ?? "55199991466943@s.whatsapp.net"; +const authBaseFolder = + process.env.BAILEYS_AUTH_FOLDER ?? join(process.cwd(), ".baileys-sessions"); +const sessionDir = join(authBaseFolder, sessionId); +const logger = pino({ level: "silent" }); +const shouldDebugGeneratedMessage = process.env.BAILEYS_DEBUG_WA_MESSAGE === "1"; + +async function ensureSessionDir() { + await mkdir(sessionDir, { recursive: true }); +} + +function waitForOpen(sock) { + return new Promise((resolve, reject) => { + sock.ev.on("connection.update", (update) => { + if (update.qr) { + console.log("\nEscaneie este QR Code:"); + qrcode.generate(update.qr, { small: true }); + } + + if (update.connection === "open") { + console.log("CONNECTED"); + resolve(); + } + + if (update.connection === "close") { + reject(update.lastDisconnect?.error ?? new Error("Connection closed")); + } + }); + }); +} + +async function createSocket() { + await ensureSessionDir(); + + const { state, saveCreds } = await useMultiFileAuthState(sessionDir); + const { version } = await fetchLatestBaileysVersion(); + + const sock = makeWASocket({ + version, + logger, + auth: { + creds: state.creds, + keys: makeCacheableSignalKeyStore(state.keys, logger), + }, + printQRInTerminal: false, + syncFullHistory: false, + markOnlineOnConnect: false, + shouldSyncHistoryMessage: () => false, + }); + + sock.ev.on("creds.update", saveCreds); + return sock; +} + +async function safeSend(name, fn) { + try { + console.log(`\nTesting: ${name}`); + const result = await fn(); + console.log(`OK: ${name}`, result?.key?.id ?? ""); + } catch (error) { + console.error(`ERROR in ${name}:`, error instanceof Error ? error.message : error); + } +} + +async function relayProtoContent(sock, content) { + const fullMessage = generateWAMessageFromContent(recipient, content, { + userJid: sock.user.id, + }); + + if (shouldDebugGeneratedMessage) { + console.log("\nBAILEYS GENERATED MESSAGE:"); + console.log(JSON.stringify(fullMessage.message, null, 2)); + } + + await sock.relayMessage(recipient, fullMessage.message, { + messageId: fullMessage.key.id, + }); + + return fullMessage; +} + +async function sendText(sock) { + const result = await sock.sendMessage(recipient, { + text: "Baileys text delivery probe", + }); + + if (shouldDebugGeneratedMessage) { + console.log("\nBAILEYS GENERATED TEXT MESSAGE:"); + console.log(JSON.stringify(result.message, null, 2)); + } + + return result; +} + +async function sendList(sock) { + return relayProtoContent(sock, { + viewOnceMessage: { + message: { + messageContextInfo: { + deviceListMetadata: {}, + deviceListMetadataVersion: 2, + }, + interactiveMessage: proto.Message.InteractiveMessage.fromObject({ + header: { + title: "Menu Baileys", + hasMediaAttachment: false, + }, + body: { + text: "Escolha uma opcao na lista", + }, + footer: { + text: "Baileys direto", + }, + nativeFlowMessage: { + buttons: [ + { + name: "single_select", + buttonParamsJson: JSON.stringify({ + title: "Abrir lista", + sections: [ + { + title: "Lanches", + rows: [ + { + id: "xburger", + title: "X-Burger", + description: "Pao, carne e queijo", + }, + { + id: "xbacon", + title: "X-Bacon", + description: "Com bacon crocante", + }, + ], + }, + ], + }), + }, + ], + messageParamsJson: "", + messageVersion: 1, + }, + }), + }, + }, + }); +} + +async function sendHydratedQuickReplyButtons(sock) { + return relayProtoContent(sock, { + templateMessage: { + hydratedTemplate: { + hydratedContentText: "Quick reply buttons", + hydratedFooterText: "Baileys direto", + hydratedButtons: [ + { + index: 0, + quickReplyButton: { + displayText: "Opcao 1", + id: "reply_1", + }, + }, + { + index: 1, + quickReplyButton: { + displayText: "Opcao 2", + id: "reply_2", + }, + }, + ], + }, + }, + }); +} + +async function sendHydratedUrlButton(sock) { + return relayProtoContent(sock, { + templateMessage: { + hydratedTemplate: { + hydratedContentText: "URL button", + hydratedFooterText: "Baileys direto", + hydratedButtons: [ + { + index: 0, + urlButton: { + displayText: "Abrir site", + url: "https://example.com", + }, + }, + ], + }, + }, + }); +} + +async function sendHydratedCallButton(sock) { + return relayProtoContent(sock, { + templateMessage: { + hydratedTemplate: { + hydratedContentText: "Call button", + hydratedFooterText: "Baileys direto", + hydratedButtons: [ + { + index: 0, + callButton: { + displayText: "Ligar agora", + phoneNumber: "+5511999999999", + }, + }, + ], + }, + }, + }); +} + +async function sendHydratedMixedButtons(sock) { + return relayProtoContent(sock, { + templateMessage: { + hydratedTemplate: { + hydratedContentText: "Mixed buttons", + hydratedFooterText: "Baileys direto", + hydratedButtons: [ + { + index: 0, + quickReplyButton: { + displayText: "Resposta", + id: "mixed_reply_1", + }, + }, + { + index: 1, + urlButton: { + displayText: "Site", + url: "https://example.com", + }, + }, + { + index: 2, + callButton: { + displayText: "Ligar", + phoneNumber: "+5511999999999", + }, + }, + ], + }, + }, + }); +} + +async function sendLegacyButtonsMessage(sock) { + return relayProtoContent(sock, { + buttonsMessage: proto.Message.ButtonsMessage.fromObject({ + contentText: "Legacy buttonsMessage", + footerText: "Baileys direto", + headerType: proto.Message.ButtonsMessage.HeaderType.EMPTY, + buttons: [ + { + buttonId: "legacy_1", + buttonText: { displayText: "Opcao 1" }, + type: proto.Message.ButtonsMessage.Button.Type.RESPONSE, + }, + { + buttonId: "legacy_2", + buttonText: { displayText: "Opcao 2" }, + type: proto.Message.ButtonsMessage.Button.Type.RESPONSE, + }, + ], + }), + }); +} + +async function sendLegacyListMessage(sock) { + return relayProtoContent(sock, { + listMessage: proto.Message.ListMessage.fromObject({ + title: "Legacy listMessage", + description: "Escolha uma opcao na lista", + buttonText: "Abrir lista", + footerText: "Baileys direto", + sections: [ + { + title: "Lanches", + rows: [ + { + rowId: "xburger", + title: "X-Burger", + description: "Pao, carne e queijo", + }, + { + rowId: "xbacon", + title: "X-Bacon", + description: "Com bacon crocante", + }, + ], + }, + ], + }), + }); +} + +async function sendButtonsMessageNativeFlow(sock) { + return relayProtoContent(sock, { + buttonsMessage: proto.Message.ButtonsMessage.fromObject({ + contentText: "ButtonsMessage native flow", + footerText: "Baileys direto", + headerType: proto.Message.ButtonsMessage.HeaderType.EMPTY, + buttons: [ + { + buttonId: "native_url_1", + buttonText: { displayText: "Abrir site" }, + type: proto.Message.ButtonsMessage.Button.Type.NATIVE_FLOW, + nativeFlowInfo: { + name: "cta_url", + paramsJson: JSON.stringify({ + display_text: "Abrir site", + url: "https://example.com", + }), + }, + }, + ], + }), + }); +} + +async function main() { + console.log(`Usando sessao do Baileys: ${sessionId}`); + console.log(`Pasta da sessao do Baileys: ${sessionDir}`); + console.log(`Destino: ${recipient}`); + console.log("Este teste usa uma sessao separada do BerryProtocol.\n"); + + const sock = await createSocket(); + await waitForOpen(sock); + + await safeSend("baileys text", () => sendText(sock)); + await safeSend("baileys list", () => sendList(sock)); + await safeSend("hydrated quick reply buttons", () => sendHydratedQuickReplyButtons(sock)); + await safeSend("hydrated url button", () => sendHydratedUrlButton(sock)); + await safeSend("hydrated call button", () => sendHydratedCallButton(sock)); + await safeSend("hydrated mixed buttons", () => sendHydratedMixedButtons(sock)); + await safeSend("legacy listMessage", () => sendLegacyListMessage(sock)); + await safeSend("legacy buttonsMessage", () => sendLegacyButtonsMessage(sock)); + await safeSend("buttonsMessage native flow", () => sendButtonsMessageNativeFlow(sock)); + + console.log("\nDone."); + sock.end(new Error("done")); + sock.ws.close(); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exitCode = 1; +}); diff --git a/examples/sdk/test-berry-advanced-proto-types.ts b/examples/sdk/test-berry-advanced-proto-types.ts new file mode 100644 index 0000000..e53c019 --- /dev/null +++ b/examples/sdk/test-berry-advanced-proto-types.ts @@ -0,0 +1,451 @@ +import { randomBytes } from "node:crypto"; +import { BerryProtocol } from "../../packages/core/src/index.ts"; +import { + generateWAMessageFromContent, + prepareWAMessageMedia, + proto, + type WAMessage, +} from "../../packages/transport/src/index.ts"; + +const to = process.env.BERRY_TEST_TO; +const imageUrl = + process.env.BERRY_TEST_IMAGE_URL ?? + "https://images.unsplash.com/photo-1513104890138-7c749659a591?auto=format&fit=crop&w=900&q=80"; + +if (!to) { + console.error("Defina BERRY_TEST_TO antes de executar a suite de proto types avancados."); + process.exit(1); +} + +type InternalSocket = { + user?: { id?: string }; + waUploadToServer: unknown; + groupInviteCode?: (jid: string) => Promise; + newsletterMetadata?: ( + type: "invite" | "jid", + key: string, + ) => Promise< + | { + id?: string; + name?: string; + invite?: string; + } + | null + >; + relayMessage: ( + jid: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + message: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options: any, + ) => Promise; +}; + +const client = new BerryProtocol({ + sessionId: "advanced-proto-types", +}); + +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +const log = (label: string, status: "OK" | "SKIP" | "FAIL", detail: string) => { + console.log(`[${status}] ${label}: ${detail}`); +}; + +const getInternalSock = (): InternalSocket => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const berrySocket = (client as any).socket; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sock = berrySocket?.sock as InternalSocket | undefined; + + if (!sock?.user?.id) { + throw new Error("Socket interno nao esta pronto."); + } + + return sock; +}; + +const sendRaw = async ( + label: string, + content: Record, +): Promise => { + const sock = getInternalSock(); + const message = generateWAMessageFromContent( + to, + content, + { + userJid: sock.user!.id!, + }, + ); + + await sock.relayMessage(to, message.message, { + messageId: message.key.id, + }); + + log(label, "OK", String(message.key.id)); + await wait(500); + return message; +}; + +const maybePrepareImage = async () => { + const sock = getInternalSock(); + return prepareWAMessageMedia( + { + image: { + url: imageUrl, + }, + }, + { + upload: sock.waUploadToServer as never, + }, + ); +}; + +const sendPollCreation = async () => { + const poll = proto.Message.PollCreationMessage.create({ + encKey: randomBytes(32), + name: "Qual pizza voce quer hoje?", + options: [ + { optionName: "Calabresa" }, + { optionName: "Frango com catupiry" }, + { optionName: "Marguerita" }, + ], + selectableOptionsCount: 1, + pollType: proto.Message.PollType.POLL, + }); + + return sendRaw("POLL CREATION", { + pollCreationMessageV5: poll, + }); +}; + +const sendPollResultSnapshot = async () => { + const snapshot = proto.Message.PollResultSnapshotMessage.create({ + name: "Parcial da enquete", + pollVotes: [ + { optionName: "Calabresa", optionVoteCount: 7 }, + { optionName: "Frango com catupiry", optionVoteCount: 3 }, + ], + pollType: proto.Message.PollType.POLL, + }); + + return sendRaw("POLL RESULT SNAPSHOT", { + pollResultSnapshotMessage: snapshot, + }); +}; + +const sendPollUpdateProbe = async (pollMessageId?: string) => { + if (!pollMessageId) { + log("POLL UPDATE", "SKIP", "pollCreationMessage id ausente"); + return; + } + + const update = proto.Message.PollUpdateMessage.create({ + pollCreationMessageKey: { + remoteJid: to, + fromMe: true, + id: pollMessageId, + }, + vote: { + encPayload: randomBytes(24), + encIv: randomBytes(12), + }, + senderTimestampMs: Date.now(), + }); + + return sendRaw("POLL UPDATE", { + pollUpdateMessage: update, + }); +}; + +const sendEventMessage = async () => { + const now = Date.now(); + const eventMessage = proto.Message.EventMessage.create({ + name: "Noite da Pizza Berry", + description: "Evento teste do protocolo", + startTime: now + 60 * 60 * 1000, + endTime: now + 2 * 60 * 60 * 1000, + extraGuestsAllowed: true, + hasReminder: true, + reminderOffsetSec: 30 * 60, + location: { + degreesLatitude: -22.9068, + degreesLongitude: -43.1729, + name: "Berry HQ", + address: "Rio de Janeiro", + }, + }); + + return sendRaw("EVENT MESSAGE", { + eventMessage, + }); +}; + +const sendScheduledCallCreation = async () => { + const scheduled = proto.Message.ScheduledCallCreationMessage.create({ + scheduledTimestampMs: Date.now() + 30 * 60 * 1000, + callType: proto.Message.ScheduledCallCreationMessage.CallType.VIDEO, + title: "Chamada agendada Berry", + }); + + return sendRaw("SCHEDULED CALL CREATION", { + scheduledCallCreationMessage: scheduled, + }); +}; + +const sendScheduledCallCancel = async (messageId?: string) => { + if (!messageId) { + log("SCHEDULED CALL CANCEL", "SKIP", "scheduledCallCreationMessage id ausente"); + return; + } + + const cancel = proto.Message.ScheduledCallEditMessage.create({ + key: { + remoteJid: to, + fromMe: true, + id: messageId, + }, + editType: proto.Message.ScheduledCallEditMessage.EditType.CANCEL, + }); + + return sendRaw("SCHEDULED CALL CANCEL", { + scheduledCallEditMessage: cancel, + }); +}; + +const sendGroupInvite = async () => { + const groupJid = process.env.BERRY_TEST_GROUP_JID; + const inviteCode = process.env.BERRY_TEST_GROUP_INVITE_CODE; + + if (!groupJid) { + log("GROUP INVITE", "SKIP", "defina pelo menos BERRY_TEST_GROUP_JID"); + return; + } + + const sock = getInternalSock(); + const resolvedInviteCode = inviteCode ?? (await sock.groupInviteCode?.(groupJid)); + + if (!resolvedInviteCode) { + log("GROUP INVITE", "SKIP", "nao foi possivel resolver invite code do grupo"); + return; + } + + const invite = proto.Message.GroupInviteMessage.create({ + groupJid, + inviteCode: resolvedInviteCode, + inviteExpiration: + Number(process.env.BERRY_TEST_GROUP_INVITE_EXPIRATION) || + Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, + groupName: process.env.BERRY_TEST_GROUP_NAME ?? "Grupo Berry", + caption: "Convite de grupo experimental", + }); + + return sendRaw("GROUP INVITE", { + groupInviteMessage: invite, + }); +}; + +const sendRequestPhoneNumber = async () => + sendRaw("REQUEST PHONE NUMBER", { + requestPhoneNumberMessage: proto.Message.RequestPhoneNumberMessage.create({}), + }); + +const sendProductMessage = async () => { + const prepared = await maybePrepareImage(); + const ownerJid = process.env.BERRY_TEST_BIZ_OWNER_JID ?? getInternalSock().user?.id ?? ""; + + const productMessage = proto.Message.ProductMessage.create({ + businessOwnerJid: ownerJid, + body: "Produto experimental Berry", + footer: "BerrySDK product probe", + product: { + productImage: prepared.imageMessage, + productId: process.env.BERRY_TEST_PRODUCT_ID ?? "berry-product-001", + title: "Pizza Experimental", + description: "Produto de teste do protocolo", + currencyCode: "BRL", + priceAmount1000: 39900, + retailerId: "berry-retailer-001", + url: "https://example.com/produto", + productImageCount: 1, + firstImageId: "berry-image-001", + }, + catalog: { + catalogImage: prepared.imageMessage, + title: "Catalogo Berry", + description: "Probe de ProductMessage", + }, + }); + + return sendRaw("PRODUCT MESSAGE", { + productMessage, + }); +}; + +const sendCollectionMessage = async () => { + const bizJid = process.env.BERRY_TEST_COLLECTION_BIZ_JID; + const id = process.env.BERRY_TEST_COLLECTION_ID; + + if (!bizJid || !id) { + log("COLLECTION MESSAGE", "SKIP", "defina BERRY_TEST_COLLECTION_BIZ_JID e BERRY_TEST_COLLECTION_ID"); + return; + } + + return sendRaw("COLLECTION MESSAGE", { + viewOnceMessage: { + message: { + interactiveMessage: proto.Message.InteractiveMessage.create({ + body: { + text: "Collection experimental", + }, + footer: { + text: "BerrySDK collection probe", + }, + collectionMessage: { + bizJid, + id, + messageVersion: 1, + }, + }), + }, + }, + }); +}; + +const sendShopStorefrontMessage = async () => { + const id = process.env.BERRY_TEST_SHOP_ID; + + if (!id) { + log("SHOP STOREFRONT", "SKIP", "defina BERRY_TEST_SHOP_ID"); + return; + } + + return sendRaw("SHOP STOREFRONT", { + viewOnceMessage: { + message: { + interactiveMessage: proto.Message.InteractiveMessage.create({ + body: { + text: "Shop storefront experimental", + }, + footer: { + text: "BerrySDK shop probe", + }, + shopStorefrontMessage: { + id, + surface: proto.Message.InteractiveMessage.ShopMessage.Surface.WA, + messageVersion: 1, + }, + }), + }, + }, + }); +}; + +const sendNewsletterAdminInvite = async () => { + const newsletterJid = process.env.BERRY_TEST_NEWSLETTER_JID; + const newsletterInvite = process.env.BERRY_TEST_NEWSLETTER_INVITE; + + if (!newsletterJid && !newsletterInvite) { + log("NEWSLETTER ADMIN INVITE", "SKIP", "defina BERRY_TEST_NEWSLETTER_JID ou BERRY_TEST_NEWSLETTER_INVITE"); + return; + } + + const sock = getInternalSock(); + const metadata = + (newsletterJid + ? await sock.newsletterMetadata?.("jid", newsletterJid) + : newsletterInvite + ? await sock.newsletterMetadata?.("invite", newsletterInvite) + : null) ?? null; + const resolvedNewsletterJid = newsletterJid ?? metadata?.id; + + if (!resolvedNewsletterJid) { + log("NEWSLETTER ADMIN INVITE", "SKIP", "nao foi possivel resolver o newsletterJid"); + return; + } + + const prepared = await maybePrepareImage(); + return sendRaw("NEWSLETTER ADMIN INVITE", { + newsletterAdminInviteMessage: proto.Message.NewsletterAdminInviteMessage.create({ + newsletterJid: resolvedNewsletterJid, + newsletterName: process.env.BERRY_TEST_NEWSLETTER_NAME ?? metadata?.name ?? "Newsletter Berry", + jpegThumbnail: prepared.imageMessage?.jpegThumbnail, + caption: "Convite admin experimental", + inviteExpiration: + Number(process.env.BERRY_TEST_NEWSLETTER_INVITE_EXPIRATION) || Date.now() + 24 * 60 * 60 * 1000, + }), + }); +}; + +const sendNewsletterFollowerInvite = async () => { + const newsletterJid = process.env.BERRY_TEST_NEWSLETTER_JID; + const newsletterInvite = process.env.BERRY_TEST_NEWSLETTER_INVITE; + + if (!newsletterJid && !newsletterInvite) { + log("NEWSLETTER FOLLOWER INVITE", "SKIP", "defina BERRY_TEST_NEWSLETTER_JID ou BERRY_TEST_NEWSLETTER_INVITE"); + return; + } + + const sock = getInternalSock(); + const metadata = + (newsletterJid + ? await sock.newsletterMetadata?.("jid", newsletterJid) + : newsletterInvite + ? await sock.newsletterMetadata?.("invite", newsletterInvite) + : null) ?? null; + const resolvedNewsletterJid = newsletterJid ?? metadata?.id; + + if (!resolvedNewsletterJid) { + log("NEWSLETTER FOLLOWER INVITE", "SKIP", "nao foi possivel resolver o newsletterJid"); + return; + } + + const prepared = await maybePrepareImage(); + return sendRaw("NEWSLETTER FOLLOWER INVITE", { + newsletterFollowerInviteMessageV2: proto.Message.NewsletterFollowerInviteMessage.create({ + newsletterJid: resolvedNewsletterJid, + newsletterName: process.env.BERRY_TEST_NEWSLETTER_NAME ?? metadata?.name ?? "Newsletter Berry", + jpegThumbnail: prepared.imageMessage?.jpegThumbnail, + caption: "Convite follower experimental", + }), + }); +}; + +const attempt = async ( + label: string, + runner: () => Promise, +): Promise => { + try { + return await runner(); + } catch (error) { + log(label, "FAIL", error instanceof Error ? error.message : String(error)); + return undefined; + } +}; + +client.once("connection.open", async () => { + try { + console.log("Running advanced proto message suite..."); + + const poll = await attempt("POLL CREATION", sendPollCreation); + await attempt("POLL RESULT SNAPSHOT", sendPollResultSnapshot); + await attempt("POLL UPDATE", () => sendPollUpdateProbe(poll?.key?.id)); + + await attempt("EVENT MESSAGE", sendEventMessage); + + const scheduled = await attempt("SCHEDULED CALL CREATION", sendScheduledCallCreation); + await attempt("SCHEDULED CALL CANCEL", () => sendScheduledCallCancel(scheduled?.key?.id)); + + await attempt("GROUP INVITE", sendGroupInvite); + await attempt("REQUEST PHONE NUMBER", sendRequestPhoneNumber); + await attempt("PRODUCT MESSAGE", sendProductMessage); + await attempt("COLLECTION MESSAGE", sendCollectionMessage); + await attempt("SHOP STOREFRONT", sendShopStorefrontMessage); + await attempt("NEWSLETTER ADMIN INVITE", sendNewsletterAdminInvite); + await attempt("NEWSLETTER FOLLOWER INVITE", sendNewsletterFollowerInvite); + } finally { + await client.disconnect().catch(() => undefined); + process.exit(0); + } +}); + +await client.connectWithQr(); diff --git a/examples/sdk/test-berry-ai-label.ts b/examples/sdk/test-berry-ai-label.ts new file mode 100644 index 0000000..f178502 --- /dev/null +++ b/examples/sdk/test-berry-ai-label.ts @@ -0,0 +1,35 @@ +import { BerryProtocol } from "../../packages/core/src/index.ts"; + +function waitForOpen(client: BerryProtocol) { + return new Promise((resolve) => { + client.once("connection.open", resolve); + }); +} + +async function main(): Promise { + const to = process.env.BERRY_TEST_TO; + if (!to) { + throw new Error('Set BERRY_TEST_TO, for example: $env:BERRY_TEST_TO="5511999999999@s.whatsapp.net"'); + } + + const client = new BerryProtocol({ + sessionId: "test-session-qr", + }); + + client.on("message.ack", (ack) => { + console.log("MESSAGE ACK:", ack); + }); + + const opened = waitForOpen(client); + await client.connectWithQr(); + await opened; + + const sent = await client.sendMessage(to, { + text: "Ola! Essa mensagem deve aparecer com label de AI.", + ai: true, + }); + + console.log("AI LABEL SENT:", sent); +} + +void main(); diff --git a/examples/sdk/test-berry-compat-matrix.ts b/examples/sdk/test-berry-compat-matrix.ts new file mode 100644 index 0000000..0ccbf15 --- /dev/null +++ b/examples/sdk/test-berry-compat-matrix.ts @@ -0,0 +1,291 @@ +import { BerryProtocol } from "../../packages/core/src/index.ts"; +import { BerryOTP } from "../../packages/berry-otp/src/index.ts"; +import { + buttonsPayloadToNativeFlowInteractiveContent, + carouselPayloadToMessageContent, + listToLegacyListMessageContent, + normalizeIncomingMessage, +} from "../../packages/wa-message/src/index.ts"; + +const DEFAULT_IMAGE_URL = + "https://images.unsplash.com/photo-1513104890138-7c749659a591?auto=format&fit=crop&w=900&q=80"; +const DEFAULT_VIDEO_URL = "https://www.w3schools.com/html/mov_bbb.mp4"; + +const assert = (condition: unknown, message: string) => { + if (!condition) { + throw new Error(message); + } +}; + +const section = (title: string, rows: Array<{ id: string; title: string; description?: string }>) => ({ + title, + rows, +}); + +const runLocalMatrix = () => { + console.log("Running local compatibility matrix..."); + + const buttonsPayload = buttonsPayloadToNativeFlowInteractiveContent({ + text: "Escolha uma opcao", + footer: "BerryProtocol", + buttons: [ + { id: "reply_1", title: "Opcao 1", kind: "quick_reply" }, + { id: "coupon", title: "Copiar cupom", kind: "copy_code", code: "BERRY10" }, + { title: "Abrir site", kind: "cta_url", url: "https://example.com" }, + ], + }); + + const interactiveButtons = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (buttonsPayload as any).interactiveMessage?.nativeFlowMessage?.buttons ?? []; + assert(interactiveButtons.length === 3, "buttons matrix should generate three native flow buttons"); + assert(interactiveButtons[0]?.name === "quick_reply", "first button should be quick_reply"); + assert(interactiveButtons[1]?.name === "cta_copy", "second button should be cta_copy"); + assert(interactiveButtons[2]?.name === "cta_url", "third button should be cta_url"); + + const listPayload = listToLegacyListMessageContent({ + title: "Menu", + text: "Escolha uma categoria", + footer: "BerryProtocol", + buttonText: "Abrir lista", + sections: [ + section("Pizzas", [{ id: "calabresa", title: "Calabresa" }]), + section("Bebidas", [{ id: "cola", title: "Coca-Cola" }]), + ], + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const listMessage = (listPayload as any).listMessage; + assert(listMessage?.sections?.length === 2, "legacy list should preserve two sections"); + assert(listMessage?.buttonText === "Abrir lista", "legacy list should preserve button text"); + + const carouselPayload = carouselPayloadToMessageContent({ + text: "Confira nossas pizzas", + footer: "Deslize para ver mais", + cards: [ + { + header: { + title: "Pizza Calabresa", + hasMediaAttachment: true, + imageMessage: {}, + }, + body: { text: "Calabresa premium" }, + footer: { text: "R$ 39,90" }, + nativeFlowMessage: { + buttons: [ + { + name: "quick_reply", + buttonParamsJson: JSON.stringify({ + display_text: "Escolher", + id: "pizza_calabresa", + }), + }, + ], + messageParamsJson: "", + messageVersion: 1, + }, + }, + ], + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const carouselMessage = (carouselPayload as any).viewOnceMessage?.message?.interactiveMessage?.carouselMessage; + assert(carouselMessage?.cards?.length === 1, "carousel wrapper should preserve one card"); + assert(carouselMessage?.messageVersion === 1, "carousel should use messageVersion 1"); + + const normalized = normalizeIncomingMessage({ + key: { + id: "abc123", + remoteJid: "5511999999999@s.whatsapp.net", + fromMe: false, + }, + messageTimestamp: 1, + status: 1, + message: { + interactiveResponseMessage: { + nativeFlowResponseMessage: { + paramsJson: JSON.stringify({ + id: "pizza_calabresa", + }), + }, + }, + }, + } as never); + assert(normalized?.chatId === "5511999999999@s.whatsapp.net", "incoming message should expose chatId"); + assert(normalized?.buttonId === "pizza_calabresa", "incoming message should parse interactive button id"); + + console.log("Local compatibility matrix passed."); +}; + +const waitForOpen = (client: BerryProtocol) => + new Promise((resolve) => { + client.once("connection.open", () => resolve()); + }); + +const runLiveMatrix = async () => { + const to = process.env.BERRY_TEST_TO; + if (!to) { + console.log("BERRY_TEST_TO not set. Skipping live compatibility matrix."); + return; + } + + console.log("Running live compatibility matrix..."); + + const client = new BerryProtocol({ + sessionId: "compat-matrix-live", + }); + + const ackLog = new Map(); + + client.on("message.ack", (ack) => { + const previous = ackLog.get(ack.messageId) ?? []; + previous.push(ack.ack); + ackLog.set(ack.messageId, previous); + console.log("ACK:", ack.messageId, ack.ack, ack.remoteJid); + }); + + const opened = waitForOpen(client); + await client.connectWithQr(); + await opened; + + const sentText = await client.sendText(to, "BerryProtocol compatibility matrix"); + console.log("TEXT:", sentText.id); + + const edited = await client.editMessage( + to, + sentText.id, + "BerryProtocol compatibility matrix (edited)", + ); + console.log("EDIT:", edited.id); + + const aiText = await client.sendMessage(to, { + text: "Mensagem com AI label", + ai: true, + }); + console.log("AI:", aiText.id); + + const list = await client.sendList(to, { + title: "Menu completo", + text: "Escolha uma categoria", + footer: "BerryProtocol", + buttonText: "Abrir menu", + sections: [ + section("Pizzas", [ + { id: "calabresa", title: "Calabresa" }, + { id: "marguerita", title: "Marguerita" }, + ]), + section("Bebidas", [{ id: "cola", title: "Coca-Cola" }]), + ], + }); + console.log("LIST:", list.id); + + const buttons = await client.sendButtons(to, { + text: "Acoes rapidas", + footer: "BerryProtocol", + buttons: [ + { id: "reply_1", title: "Escolher", kind: "quick_reply" }, + { title: "Copiar cupom", kind: "copy_code", code: "BERRY10" }, + { title: "Abrir site", kind: "cta_url", url: "https://example.com" }, + ], + }); + console.log("BUTTONS:", buttons.id); + + const carousel = await client.sendCarousel(to, { + text: "Carousel image matrix", + footer: "BerryProtocol", + carouselCardType: "image", + cards: [ + { + title: "Pizza Calabresa", + body: "Calabresa premium", + footer: "R$ 39,90", + image: { url: process.env.BERRY_TEST_IMAGE_URL ?? DEFAULT_IMAGE_URL }, + buttons: [{ id: "pizza_calabresa", title: "Escolher", kind: "quick_reply" }], + }, + { + title: "Pizza Frango", + body: "Frango com catupiry", + footer: "R$ 42,90", + image: { url: process.env.BERRY_TEST_IMAGE_URL ?? DEFAULT_IMAGE_URL }, + buttons: [{ title: "Abrir site", kind: "cta_url", url: "https://example.com/cardapio" }], + }, + ], + }); + console.log("CAROUSEL IMAGE:", carousel.id); + + const aiCarousel = await client.sendCarousel(to, { + text: "Carousel com AI label", + footer: "BerryProtocol AI", + ai: true, + carouselCardType: "image", + cards: [ + { + title: "Pizza AI Calabresa", + body: "Card com selo de IA", + footer: "R$ 39,90", + image: { url: process.env.BERRY_TEST_IMAGE_URL ?? DEFAULT_IMAGE_URL }, + buttons: [{ id: "pizza_ai_calabresa", title: "Escolher", kind: "quick_reply" }], + }, + ], + }); + console.log("CAROUSEL AI:", aiCarousel.id); + + const mixedCarousel = await client.sendCarousel(to, { + text: "Carousel mixed matrix", + footer: "BerryProtocol", + carouselCardType: "mixed", + cards: [ + { + title: "Combo familia", + body: "Imagem do combo", + footer: "R$ 74,90", + image: { url: process.env.BERRY_TEST_IMAGE_URL ?? DEFAULT_IMAGE_URL }, + buttons: [{ title: "Copiar cupom", kind: "copy_code", code: "FAMILIA10" }], + }, + { + title: "Teaser forno", + body: "Video da cozinha", + footer: "Video", + video: { + url: process.env.BERRY_TEST_VIDEO_URL ?? DEFAULT_VIDEO_URL, + mimetype: "video/mp4", + }, + buttons: [{ id: "pedido_agora", title: "Pedir agora", kind: "quick_reply" }], + }, + ], + }); + console.log("CAROUSEL MIXED:", mixedCarousel.id); + + await client.sendLocation(to, { + latitude: -22.9068, + longitude: -43.1729, + name: "Berry HQ", + address: "Rio de Janeiro", + }); + console.log("LOCATION: ok"); + + await client.sendContact(to, { + displayName: "Berry Suporte", + vcard: "BEGIN:VCARD\nVERSION:3.0\nFN:Berry Suporte\nTEL;type=CELL;type=VOICE;waid=5511999999999:+55 11 99999-9999\nEND:VCARD", + }); + console.log("CONTACT: ok"); + + await client.sendReaction(to, "🍕", sentText.id); + console.log("REACTION: ok"); + + const otp = BerryOTP.createLoginFlow(client, { + issuer: "BerryProtocol", + ttlMs: 2 * 60 * 1000, + mode: "copy-code", + editOnExpire: true, + }); + + const otpSent = await otp.sendLoginCode(to, { + userId: "compat-matrix-user", + metadata: { source: "compat-matrix" }, + }); + console.log("OTP:", otpSent.messageId); + + console.log("Live compatibility matrix finished."); +}; + +runLocalMatrix(); +await runLiveMatrix(); diff --git a/examples/sdk/test-berry-message-regression.ts b/examples/sdk/test-berry-message-regression.ts new file mode 100644 index 0000000..9f6952d --- /dev/null +++ b/examples/sdk/test-berry-message-regression.ts @@ -0,0 +1,139 @@ +import { BerryProtocol } from "../../packages/core/src/index.ts"; + +const assertRejects = async (label: string, fn: () => Promise) => { + try { + await fn(); + console.error(`FAIL: ${label} should have rejected`); + } catch (error) { + console.log(`OK: ${label}`, error instanceof Error ? error.message : error); + } +}; + +const runLocalValidationSuite = async () => { + const client = new BerryProtocol({ + sessionId: "regression-validation", + printQrInTerminal: false, + }); + + await assertRejects("invalid jid", () => + client.sendMessage("invalido", { + text: "oi", + }), + ); + + await assertRejects("carousel without media", () => + client.sendMessage("5511999999999@s.whatsapp.net", { + text: "cards", + cards: [ + { + title: "Sem midia", + }, + ], + }), + ); + + await assertRejects("carousel over limit", () => + client.sendMessage("5511999999999@s.whatsapp.net", { + text: "cards", + cards: Array.from({ length: 11 }, (_, index) => ({ + title: `Card ${index + 1}`, + image: { + url: "https://example.com/image.png", + }, + })), + }), + ); + + await assertRejects("carousel type mismatch", () => + client.sendMessage("5511999999999@s.whatsapp.net", { + text: "cards", + carouselCardType: "image", + cards: [ + { + title: "Video em carousel de imagem", + video: { + url: "https://www.w3schools.com/html/mov_bbb.mp4", + }, + }, + ], + }), + ); +}; + +const runLiveSuite = async () => { + const to = process.env.BERRY_TEST_TO; + if (!to) { + console.log("BERRY_TEST_TO not set. Skipping live send suite."); + return; + } + + const client = new BerryProtocol({ + sessionId: "regression-live", + }); + + client.on("message.ack", (ack) => { + console.log("ACK:", ack); + }); + + client.once("connection.open", async () => { + console.log("Connected. Running live send suite..."); + + await client.sendText(to, "BerryProtocol regression suite"); + + await client.sendList(to, { + title: "Menu", + text: "Escolha uma categoria", + footer: "BerryProtocol", + buttonText: "Abrir lista", + sections: [ + { + title: "Pizzas", + rows: [{ id: "calabresa", title: "Calabresa" }], + }, + ], + }); + + await client.sendButtons(to, { + text: "Escolha uma acao", + footer: "BerryProtocol", + buttons: [ + { id: "reply_1", title: "Opcao 1", kind: "quick_reply" }, + { id: "coupon", title: "Copiar cupom", kind: "copy_code", code: "BERRY10" }, + ], + }); + + await client.sendCarousel(to, { + text: "Regression carousel", + footer: "BerryProtocol", + carouselCardType: "mixed", + cards: [ + { + title: "Imagem", + body: "Card de imagem", + footer: "R$ 39,90", + image: { + url: "https://images.unsplash.com/photo-1513104890138-7c749659a591?auto=format&fit=crop&w=900&q=80", + }, + buttons: [{ id: "img_1", title: "Escolher", kind: "quick_reply" }], + }, + { + title: "Video", + body: "Card de video", + footer: "Promo", + video: { + url: "https://www.w3schools.com/html/mov_bbb.mp4", + mimetype: "video/mp4", + }, + buttons: [{ title: "Abrir site", kind: "cta_url", url: "https://example.com" }], + }, + ], + }); + + console.log("Live send suite finished."); + }); + + await client.connectWithQr(); +}; + +await runLocalValidationSuite(); +await runLiveSuite(); diff --git a/examples/sdk/test-berry-new-ways.js b/examples/sdk/test-berry-new-ways.js new file mode 100644 index 0000000..9945eb4 --- /dev/null +++ b/examples/sdk/test-berry-new-ways.js @@ -0,0 +1,317 @@ +import { BerryClient } from "../../packages/core/dist/index.js"; +import { + buttonsPayloadToLegacyButtonsMessageContent, + buttonsPayloadToNativeFlowInteractiveContent, + buttonsPayloadToTemplateMessageContent, + interactivePayloadToMessageContent, + listToLegacyListMessageContent, + listToInteractiveMessageContent, + listToInteractivePayload, +} from "../../packages/wa-message/dist/index.js"; +import qrcode from "qrcode-terminal"; + +const sessionId = process.env.BERRY_TEST_SESSION_ID ?? "test-session-qr"; +const recipient = process.env.BERRY_TEST_TO; +const authMethod = process.env.BERRY_TEST_AUTH ?? "qr"; +const pairingPhone = process.env.BERRY_TEST_PHONE; + +const sampleList = { + title: "Menu BerryProtocol", + text: "Escolha uma opcao na lista", + footer: "BerryProtocol", + buttonText: "Abrir lista", + sections: [ + { + title: "Lanches", + rows: [ + { + id: "xburger", + title: "X-Burger", + description: "Pao, carne e queijo", + }, + { + id: "xbacon", + title: "X-Bacon", + description: "Com bacon crocante", + }, + ], + }, + ], +}; + +const sampleButtons = { + text: "Escolha uma opcao", + footer: "BerryProtocol", + buttons: [ + { id: "reply_1", title: "Opcao 1" }, + { id: "reply_2", title: "Opcao 2" }, + ], +}; + +const sampleCopyCodeButtons = { + text: "Use este cupom", + footer: "BerryProtocol", + buttons: [ + { id: "copy_code_1", title: "Copiar cupom", kind: "copy_code", code: "BERRY10" }, + ], +}; + +const sampleUrlButtons = { + text: "Acesse nosso site", + footer: "BerryProtocol", + buttons: [ + { + id: "cta_url_1", + title: "Abrir site", + kind: "cta_url", + url: "https://example.com", + }, + ], +}; + +function waitForOpen(client) { + return new Promise((resolve) => { + client.once("connection.open", (state) => { + console.log("CONNECTED:", state); + resolve(state); + }); + }); +} + +function waitForAck(client, messageId, timeoutMs = 15000) { + return new Promise((resolve, reject) => { + let settled = false; + const timeout = setTimeout(() => { + settled = true; + reject(new Error(`Timeout esperando ACK final para ${messageId}`)); + }, timeoutMs); + + const listener = (ack) => { + if (settled) { + return; + } + + if (ack.messageId !== messageId) { + return; + } + + if (ack.ack === "delivered" || ack.ack === "read") { + settled = true; + clearTimeout(timeout); + resolve(ack); + } + }; + + client.on("message.ack", listener); + }); +} + +async function safeTest(name, fn) { + try { + console.log(`\nTesting: ${name}`); + const result = await fn(); + console.log(`OK: ${name}`, result ?? ""); + } catch (error) { + console.log(`ERROR in ${name}:`, error instanceof Error ? error.message : error); + } +} + +function buildClient() { + return new BerryClient({ + sessionId, + authFolder: process.env.BERRY_AUTH_FOLDER, + auth: { + method: authMethod, + phoneNumber: pairingPhone, + }, + }); +} + +async function connectClient(client) { + if (authMethod === "pairing_code") { + if (!pairingPhone) { + throw new Error("Define BERRY_TEST_PHONE para usar pairing_code."); + } + + await client.connectWithPairingCode(pairingPhone); + return; + } + + if (authMethod === "link") { + await client.connectWithLink(); + return; + } + + await client.connectWithQr(); +} + +function printPreview(label, value) { + console.log(`\nPreview: ${label}`); + console.log(JSON.stringify(value, null, 2)); +} + +async function main() { + if (!recipient) { + throw new Error( + "Define BERRY_TEST_TO com outro numero de teste. Exemplo: 5511999999999@s.whatsapp.net", + ); + } + + const client = buildClient(); + + client.on("auth.link", ({ value }) => { + console.log("\nLink gerado:"); + console.log(value); + }); + + client.on("auth.qr", ({ value }) => { + console.log("\nEscaneie este QR Code:"); + qrcode.generate(value, { small: true }); + }); + + client.on("auth.pairing_code", ({ code }) => { + console.log(`\nPairing code: ${code}`); + }); + + client.on("auth.success", (data) => { + console.log("AUTH SUCCESS:", data); + }); + + client.on("auth.error", (error) => { + console.log("AUTH ERROR:", error); + }); + + client.on("connection.close", (state) => { + console.log("DISCONNECTED:", state); + }); + + client.on("message.received", (message) => { + console.log("MESSAGE RECEIVED:", message); + }); + + client.on("message.ack", (ack) => { + console.log("MESSAGE ACK:", ack); + }); + + await safeTest("wa-message listToInteractivePayload", async () => { + const payload = listToInteractivePayload(sampleList); + printPreview("interactive payload from list", payload); + return "payload generated"; + }); + + await safeTest("wa-message listToInteractiveMessageContent", async () => { + const content = listToInteractiveMessageContent(sampleList); + printPreview("interactive message content from list", content); + return "content generated"; + }); + + await safeTest("wa-message listToLegacyListMessageContent", async () => { + const content = listToLegacyListMessageContent(sampleList); + printPreview("legacy list message content from list", content); + return "content generated"; + }); + + await safeTest("wa-message buttonsPayloadToTemplateMessageContent", async () => { + const content = buttonsPayloadToTemplateMessageContent(sampleButtons); + printPreview("template message content from buttons", content); + return "content generated"; + }); + + await safeTest("wa-message buttonsPayloadToLegacyButtonsMessageContent", async () => { + const content = buttonsPayloadToLegacyButtonsMessageContent(sampleButtons); + printPreview("legacy buttons message content from buttons", content); + return "content generated"; + }); + + await safeTest("wa-message buttonsPayloadToNativeFlowInteractiveContent", async () => { + const content = buttonsPayloadToNativeFlowInteractiveContent(sampleCopyCodeButtons); + printPreview("native flow interactive content from buttons", content); + return "content generated"; + }); + + await safeTest("wa-message buttonsPayloadToNativeFlowInteractiveContent cta_url", async () => { + const content = buttonsPayloadToNativeFlowInteractiveContent(sampleUrlButtons); + printPreview("native flow interactive content from url buttons", content); + return "content generated"; + }); + + await safeTest("wa-message interactivePayloadToMessageContent", async () => { + const interactivePayload = { + body: { + text: "Escolha uma opcao", + }, + footer: { + text: "BerryProtocol", + }, + nativeFlowMessage: { + buttons: [ + { + name: "single_select", + buttonParamsJson: JSON.stringify({ + title: "Abrir lista", + sections: sampleList.sections, + }), + }, + ], + }, + }; + const content = interactivePayloadToMessageContent(interactivePayload); + printPreview("interactive message content", content); + return "content generated"; + }); + + console.log(`\nConnecting with auth=${authMethod}...`); + console.log(`Recipient under test: ${recipient}`); + const opened = waitForOpen(client); + await connectClient(client); + await opened; + + await safeTest("BerryClient sendText with delivered/read wait", async () => { + const sent = await client.sendText(recipient, "BerryProtocol text delivery probe"); + const ack = await waitForAck(client, sent.id, 20000); + return { sent, ack }; + }); + + await safeTest("BerryClient sendMessage(list)", () => + client.sendMessage(recipient, { + list: sampleList, + }), + ); + + await safeTest("BerryClient sendMessage(listMessage)", () => + client.sendMessage(recipient, { + listMessage: sampleList, + }), + ); + + await safeTest("BerryClient sendMessage(buttonsMessage)", () => + client.sendMessage(recipient, { + buttonsMessage: sampleButtons, + }), + ); + + await safeTest("BerryClient sendMessage(interactiveMessage)", () => + client.sendMessage(recipient, { + interactiveMessage: listToInteractivePayload(sampleList), + }), + ); + + await safeTest("BerryClient sendMessage(buttonsMessage copy_code)", () => + client.sendMessage(recipient, { + buttonsMessage: sampleCopyCodeButtons, + }), + ); + + await safeTest("BerryClient sendMessage(buttonsMessage cta_url)", () => + client.sendMessage(recipient, { + buttonsMessage: sampleUrlButtons, + }), + ); + + console.log("\nDone."); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exitCode = 1; +}); diff --git a/examples/sdk/test-berry-otp.js b/examples/sdk/test-berry-otp.js new file mode 100644 index 0000000..08285dc --- /dev/null +++ b/examples/sdk/test-berry-otp.js @@ -0,0 +1,69 @@ +import { createRequire } from "node:module"; +import BerryOTP from "./berryotp.js"; + +const require = createRequire(import.meta.url); +import { BerryClient } from "../../packages/core/src/index.ts"; + +const client = new BerryClient({ + sessionId: "test-session-qr", +}); + +client.on("auth.qr", ({ value }) => { + console.log("QR:", value); +}); + +client.on("connection.open", () => { + console.log("Conectado!"); +}); + +await client.connectWithQr(); + +// cria o BerryOTP +const otp = new BerryOTP(client, { + issuer: "BerryProtocol", + ttlMs: 2 * 60 * 1000, + mode: "experimental-copy-code", +}); + +// eventos +otp.on("expired", (data) => { + console.log("OTP expirou:", data); +}); + +otp.on("used", (data) => { + console.log("OTP usado:", data); +}); + +otp.on("denied", (data) => { + console.log("Usuário clicou em NÃO PEDI:", data); +}); + +// número +const to = "5519997530219@s.whatsapp.net"; + +// envia OTP +const sent = await otp.send(to, { + purpose: "entrar na conta", +}); + +console.log("OTP enviado:"); +console.log(sent); + +/* +Retorno: + +{ + id, + to, + code, + expiresAt, + messageId, + status: "sent" +} +*/ + +// verificar código depois +const result = await otp.verify(to, sent.code); + +console.log("VERIFY:"); +console.log(result); \ No newline at end of file diff --git a/examples/sdk/test-berry-otp.ts b/examples/sdk/test-berry-otp.ts new file mode 100644 index 0000000..d241341 --- /dev/null +++ b/examples/sdk/test-berry-otp.ts @@ -0,0 +1,49 @@ +import { BerryClient } from "../../packages/core/src/index.ts"; +import { BerryOTP } from "../../packages/berry-otp/src/index.ts"; + +function waitForOpen(client: BerryClient) { + return new Promise((resolve) => { + client.once("connection.open", resolve); + }); +} + +async function main(): Promise { + const to = process.env.BERRY_TEST_TO; + if (!to) { + throw new Error('Set BERRY_TEST_TO, for example: $env:BERRY_TEST_TO="5511999999999@s.whatsapp.net"'); + } + + const client = new BerryClient({ + sessionId: "test-session-qr", + }); + + const opened = waitForOpen(client); + await client.connectWithQr(); + await opened; + + const otp = BerryOTP.createLoginFlow(client, { + issuer: "BerryProtocol", + ttlMs: 2 * 60 * 1000, + mode: "copy-code", + editOnExpire: true, + }); + + otp.on("sent", console.log); + otp.on("used", console.log); + otp.on("expired", console.log); + otp.on("denied", console.log); + otp.on("warning", console.log); + otp.on("error", console.error); + + const sent = await otp.sendLoginCode(to, { + userId: "test-user", + metadata: { source: "sdk-test" }, + }); + + console.log("OTP enviado:", sent); + + // Para testar verify manualmente, chame: + // const result = await otp.verifyLoginCode(to, sent.code); +} + +void main(); diff --git a/examples/sdk/test-berry-self-contained-proto-types.ts b/examples/sdk/test-berry-self-contained-proto-types.ts new file mode 100644 index 0000000..2e03317 --- /dev/null +++ b/examples/sdk/test-berry-self-contained-proto-types.ts @@ -0,0 +1,359 @@ +import { randomBytes, randomUUID } from "node:crypto"; +import { BerryProtocol } from "../../packages/core/src/index.ts"; +import { + generateWAMessageFromContent, + prepareWAMessageMedia, + proto, + type WAMessage, +} from "../../packages/transport/src/index.ts"; + +const to = process.env.BERRY_TEST_TO; +const imageUrl = + process.env.BERRY_TEST_IMAGE_URL ?? + "https://images.unsplash.com/photo-1513104890138-7c749659a591?auto=format&fit=crop&w=900&q=80"; + +if (!to) { + console.error("Defina BERRY_TEST_TO antes de executar a suite self-contained."); + process.exit(1); +} + +type InternalSocket = { + user?: { id?: string }; + waUploadToServer: unknown; + relayMessage: ( + jid: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + message: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options: any, + ) => Promise; +}; + +const client = new BerryProtocol({ + sessionId: "carousel-ai-example", +}); + +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +const log = (label: string, status: "OK" | "SKIP" | "FAIL", detail: string) => { + console.log(`[${status}] ${label}: ${detail}`); +}; + +const getInternalSock = (): InternalSocket => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const berrySocket = (client as any).socket; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sock = berrySocket?.sock as InternalSocket | undefined; + + if (!sock?.user?.id) { + throw new Error("Socket interno nao esta pronto."); + } + + return sock; +}; + +const sendRaw = async ( + label: string, + content: Record, +): Promise => { + const sock = getInternalSock(); + const message = generateWAMessageFromContent( + to, + content, + { + userJid: sock.user!.id!, + }, + ); + + await sock.relayMessage(to, message.message, { + messageId: message.key.id, + }); + + log(label, "OK", String(message.key.id)); + await wait(450); + return message; +}; + +const maybePrepareImage = async () => { + const sock = getInternalSock(); + return prepareWAMessageMedia( + { + image: { + url: imageUrl, + }, + }, + { + upload: sock.waUploadToServer as never, + }, + ); +}; + +const createPaymentBackground = () => + proto.PaymentBackground.create({ + id: "berry-payment-bg", + type: proto.PaymentBackground.Type.DEFAULT, + placeholderArgb: 0x1f2937, + textArgb: 0xffffff, + subtextArgb: 0xd1d5db, + }); + +const createMoney = (value: number) => + proto.Money.create({ + value, + offset: 1000, + currencyCode: "BRL", + }); + +const sendAnchorText = async () => + sendRaw("ANCHOR TEXT", { + conversation: "Mensagem ancora para testes proto self-contained.", + }); + +const sendPollCreation = async () => + sendRaw("POLL CREATION", { + pollCreationMessageV5: proto.Message.PollCreationMessage.create({ + encKey: randomBytes(32), + name: "Qual sabor experimental?", + options: [ + { optionName: "Calabresa" }, + { optionName: "Frango" }, + ], + selectableOptionsCount: 1, + pollType: proto.Message.PollType.POLL, + }), + }); + +const sendPollVote = async () => + sendRaw("POLL VOTE", { + pollVoteMessage: proto.Message.PollVoteMessage.create({ + selectedOptions: [randomBytes(32)], + }), + }); + +const sendPollResultSnapshot = async () => + sendRaw("POLL RESULT SNAPSHOT", { + pollResultSnapshotMessage: proto.Message.PollResultSnapshotMessage.create({ + name: "Parcial self-contained", + pollVotes: [ + { optionName: "Calabresa", optionVoteCount: 5 }, + { optionName: "Frango", optionVoteCount: 2 }, + ], + pollType: proto.Message.PollType.POLL, + }), + }); + +const sendEventResponse = async () => + sendRaw("EVENT RESPONSE", { + eventResponseMessage: proto.Message.EventResponseMessage.create({ + response: proto.Message.EventResponseMessage.EventResponseType.GOING, + timestampMs: Date.now(), + extraGuestCount: 1, + }), + }); + +const sendCallLogMessage = async () => + sendRaw("CALL LOG MESSAGE", { + callLogMessage: proto.Message.CallLogMessage.create({ + isVideo: true, + callOutcome: proto.Message.CallLogMessage.CallOutcome.MISSED, + durationSecs: 0, + callType: proto.Message.CallLogMessage.CallType.REGULAR, + participants: [ + { + jid: to, + callOutcome: proto.Message.CallLogMessage.CallOutcome.MISSED, + }, + ], + }), + }); + +const sendPaymentInvite = async () => + sendRaw("PAYMENT INVITE", { + paymentInviteMessage: proto.Message.PaymentInviteMessage.create({ + serviceType: proto.Message.PaymentInviteMessage.ServiceType.UPI, + expiryTimestamp: Date.now() + 24 * 60 * 60 * 1000, + }), + }); + +const sendInvoiceMessage = async () => { + const prepared = await maybePrepareImage(); + + return sendRaw("INVOICE MESSAGE", { + invoiceMessage: proto.Message.InvoiceMessage.create({ + note: "Fatura experimental Berry", + token: randomUUID(), + attachmentType: proto.Message.InvoiceMessage.AttachmentType.IMAGE, + attachmentMimetype: "image/jpeg", + attachmentJpegThumbnail: prepared.imageMessage?.jpegThumbnail, + }), + }); +}; + +const sendRequestPayment = async () => + sendRaw("REQUEST PAYMENT", { + requestPaymentMessage: proto.Message.RequestPaymentMessage.create({ + noteMessage: proto.Message.create({ + conversation: "Cobranca experimental Berry", + }), + currencyCodeIso4217: "BRL", + amount1000: 49900, + requestFrom: to, + expiryTimestamp: Date.now() + 60 * 60 * 1000, + amount: createMoney(49900), + background: createPaymentBackground(), + }), + }); + +const sendSendPayment = async (requestMessage?: WAMessage) => { + if (!requestMessage?.key?.id) { + log("SEND PAYMENT", "SKIP", "requestPaymentMessage id ausente"); + return; + } + + return sendRaw("SEND PAYMENT", { + sendPaymentMessage: proto.Message.SendPaymentMessage.create({ + noteMessage: proto.Message.create({ + conversation: "Pagamento experimental enviado", + }), + requestMessageKey: { + remoteJid: to, + fromMe: true, + id: requestMessage.key.id, + }, + background: createPaymentBackground(), + transactionData: JSON.stringify({ + txid: randomUUID(), + amount1000: 49900, + currency: "BRL", + }), + }), + }); +}; + +const sendOrderMessage = async () => { + const prepared = await maybePrepareImage(); + const sellerJid = getInternalSock().user?.id ?? ""; + + return sendRaw("ORDER MESSAGE", { + orderMessage: proto.Message.OrderMessage.create({ + orderId: `berry-order-${Date.now()}`, + thumbnail: prepared.imageMessage?.jpegThumbnail, + itemCount: 2, + status: proto.Message.OrderMessage.OrderStatus.INQUIRY, + surface: proto.Message.OrderMessage.OrderSurface.CATALOG, + message: "Pedido experimental Berry", + orderTitle: "2 pizzas experimentais", + sellerJid, + token: randomUUID(), + totalAmount1000: 79900, + totalCurrencyCode: "BRL", + messageVersion: 1, + catalogType: "native", + }), + }); +}; + +const sendStickerPackMessage = async () => { + const prepared = await maybePrepareImage(); + + return sendRaw("STICKER PACK MESSAGE", { + stickerPackMessage: proto.Message.StickerPackMessage.create({ + stickerPackId: "berry-pack-001", + name: "Berry Pack Experimental", + publisher: "BerrySDK", + stickers: [ + { + fileName: "pizza-love.webp", + isAnimated: false, + emojis: ["🍕", "🔥"], + mimetype: "image/webp", + }, + ], + caption: "Sticker pack experimental", + packDescription: "Pacote de stickers para validar renderizacao", + thumbnailSha256: prepared.imageMessage?.fileSha256, + thumbnailEncSha256: prepared.imageMessage?.fileEncSha256, + thumbnailDirectPath: prepared.imageMessage?.directPath, + thumbnailHeight: prepared.imageMessage?.height, + thumbnailWidth: prepared.imageMessage?.width, + stickerPackSize: 1, + stickerPackOrigin: proto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED, + }), + }); +}; + +const sendPinInChat = async (anchor?: WAMessage) => { + if (!anchor?.key?.id) { + log("PIN IN CHAT", "SKIP", "anchor message id ausente"); + return; + } + + return sendRaw("PIN IN CHAT", { + pinInChatMessage: proto.Message.PinInChatMessage.create({ + key: { + remoteJid: to, + fromMe: true, + id: anchor.key.id, + }, + type: proto.Message.PinInChatMessage.Type.PIN_FOR_ALL, + senderTimestampMs: Date.now(), + }), + }); +}; + +const sendKeepInChat = async (anchor?: WAMessage) => { + if (!anchor?.key?.id) { + log("KEEP IN CHAT", "SKIP", "anchor message id ausente"); + return; + } + + return sendRaw("KEEP IN CHAT", { + keepInChatMessage: proto.Message.KeepInChatMessage.create({ + key: { + remoteJid: to, + fromMe: true, + id: anchor.key.id, + }, + keepType: proto.KeepType.KEEP_FOR_ALL, + timestampMs: Date.now(), + }), + }); +}; + +const attempt = async ( + label: string, + runner: () => Promise, +): Promise => { + try { + return await runner(); + } catch (error) { + log(label, "FAIL", error instanceof Error ? error.message : String(error)); + return undefined; + } +}; + +client.once("connection.open", async () => { + try { + console.log("Running self-contained proto message suite..."); + + const anchor = await attempt("ANCHOR TEXT", sendAnchorText); + await attempt("POLL CREATION", sendPollCreation); + await attempt("POLL VOTE", sendPollVote); + await attempt("POLL RESULT SNAPSHOT", sendPollResultSnapshot); + await attempt("EVENT RESPONSE", sendEventResponse); + await attempt("CALL LOG MESSAGE", sendCallLogMessage); + await attempt("PAYMENT INVITE", sendPaymentInvite); + await attempt("INVOICE MESSAGE", sendInvoiceMessage); + const requestPayment = await attempt("REQUEST PAYMENT", sendRequestPayment); + await attempt("SEND PAYMENT", () => sendSendPayment(requestPayment)); + await attempt("ORDER MESSAGE", sendOrderMessage); + await attempt("STICKER PACK MESSAGE", sendStickerPackMessage); + await attempt("PIN IN CHAT", () => sendPinInChat(anchor)); + await attempt("KEEP IN CHAT", () => sendKeepInChat(anchor)); + } finally { + await client.disconnect().catch(() => undefined); + process.exit(0); + } +}); + +await client.connectWithQr(); diff --git a/examples/send-buttons.js b/examples/send-buttons.js new file mode 100644 index 0000000..2244556 --- /dev/null +++ b/examples/send-buttons.js @@ -0,0 +1,23 @@ +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "send-buttons-example", +}); + +await client.connectWithQr(); +await client.waitUntilConnected(); + +const to = process.env.BERRY_TEST_TO; + +if (!to) { + throw new Error("Set BERRY_TEST_TO with a valid WhatsApp JID."); +} + +await client.sendButtons(to, { + text: "Choose a path", + footer: "BerryProtocol button example", + buttons: [ + { id: "sales", title: "Sales", kind: "reply" }, + { id: "support", title: "Support", kind: "reply" }, + ], +}); diff --git a/examples/send-carousel.js b/examples/send-carousel.js new file mode 100644 index 0000000..8c3ab20 --- /dev/null +++ b/examples/send-carousel.js @@ -0,0 +1,33 @@ +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "send-carousel-example", +}); + +await client.connectWithQr(); +await client.waitUntilConnected(); + +const to = process.env.BERRY_TEST_TO; + +if (!to) { + throw new Error("Set BERRY_TEST_TO with a valid WhatsApp JID."); +} + +await client.sendCarousel(to, { + text: "Featured products", + footer: "Swipe to browse", + carouselCardType: "image", + cards: [ + { + title: "Product A", + body: "A short description", + footer: "R$ 49,90", + image: { + url: process.env.BERRY_TEST_IMAGE_URL ?? "https://picsum.photos/640/480", + }, + buttons: [ + { id: "buy_product_a", title: "Buy", kind: "quick_reply" }, + ], + }, + ], +}); diff --git a/examples/send-list.js b/examples/send-list.js new file mode 100644 index 0000000..285d21d --- /dev/null +++ b/examples/send-list.js @@ -0,0 +1,30 @@ +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "send-list-example", +}); + +await client.connectWithQr(); +await client.waitUntilConnected(); + +const to = process.env.BERRY_TEST_TO; + +if (!to) { + throw new Error("Set BERRY_TEST_TO with a valid WhatsApp JID."); +} + +await client.sendList(to, { + title: "Departments", + text: "Pick one department", + footer: "BerryProtocol list example", + buttonText: "Open menu", + sections: [ + { + title: "Main", + rows: [ + { id: "billing", title: "Billing" }, + { id: "orders", title: "Orders" }, + ], + }, + ], +}); diff --git a/examples/send-text.js b/examples/send-text.js new file mode 100644 index 0000000..b36b330 --- /dev/null +++ b/examples/send-text.js @@ -0,0 +1,16 @@ +import BerryProtocol from "berryprotocol"; + +const client = new BerryProtocol({ + sessionId: "send-text-example", +}); + +await client.connectWithQr(); +await client.waitUntilConnected(); + +const to = process.env.BERRY_TEST_TO; + +if (!to) { + throw new Error("Set BERRY_TEST_TO with a valid WhatsApp JID."); +} + +await client.sendText(to, "Hello from BerryProtocol send-text example."); diff --git a/iniciar-dashboard.cmd b/iniciar-dashboard.cmd new file mode 100644 index 0000000..40130b5 --- /dev/null +++ b/iniciar-dashboard.cmd @@ -0,0 +1,116 @@ +@echo off +setlocal +title BerryProtocol - Iniciar Dashboard + +cd /d "%~dp0" + +set "BERRY_DASHBOARD_ROOT=%~dp0" +set "BERRY_DASHBOARD_PID=%~dp0.berry-dashboard.pid" +set "BERRY_DASHBOARD_URL=http://127.0.0.1:3000/studio?apiKey=berryapi_local_dashboard" +set "PORT=3000" +set "HOST=127.0.0.1" +set "API_KEY=berryapi_local_dashboard" + +powershell -NoProfile -ExecutionPolicy Bypass -Command "try { $headers = @{ Authorization = 'Bearer ' + $env:API_KEY }; $response = Invoke-RestMethod -Uri 'http://127.0.0.1:3000/studio/api/capabilities' -Headers $headers -TimeoutSec 2; $listener = Get-NetTCPConnection -LocalAddress '127.0.0.1' -LocalPort 3000 -State Listen -ErrorAction Stop | Select-Object -First 1; $process = Get-CimInstance Win32_Process -Filter ('ProcessId = ' + $listener.OwningProcess) -ErrorAction Stop; if ($response.success -and $process.CommandLine -like '*packages\berryapi\dist\server.js*') { Set-Content -LiteralPath $env:BERRY_DASHBOARD_PID -Value $listener.OwningProcess -Encoding ascii; exit 0 } } catch {}; exit 1" +if not errorlevel 1 ( + echo. + echo A dashboard ja esta funcionando em http://127.0.0.1:3000/studio + if /I not "%BERRY_NO_BROWSER%"=="1" start "" "%BERRY_DASHBOARD_URL%" + exit /b 0 +) + +powershell -NoProfile -ExecutionPolicy Bypass -Command "Remove-Item -LiteralPath $env:BERRY_DASHBOARD_PID -Force -ErrorAction SilentlyContinue; $listener = Get-NetTCPConnection -LocalPort 3000 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1; if ($listener) { exit 1 }; exit 0" +if errorlevel 1 goto :port_error + +where node >nul 2>&1 +if errorlevel 1 ( + echo. + echo ERRO: Node.js nao foi encontrado neste computador. + echo Instale o Node.js e execute este arquivo novamente. + echo. + pause + exit /b 1 +) + +where npm >nul 2>&1 +if errorlevel 1 ( + echo. + echo ERRO: npm nao foi encontrado neste computador. + echo. + pause + exit /b 1 +) + +if not exist "node_modules" ( + echo. + echo Instalando as dependencias pela primeira vez... + call npm install + if errorlevel 1 goto :build_error +) + +if /I not "%BERRY_SKIP_BUILD%"=="1" ( + echo. + echo Preparando a BerryAPI e a dashboard... + call npm run build + if errorlevel 1 goto :build_error +) + +if not exist "packages\berryapi\dist\server.js" ( + echo. + echo ERRO: a BerryAPI ainda nao foi compilada. + echo Execute este arquivo novamente sem BERRY_SKIP_BUILD. + echo. + pause + exit /b 1 +) + +echo. +echo Iniciando a BerryAPI local... +set "NODE_ENV=production" + +powershell -NoProfile -ExecutionPolicy Bypass -Command "$root = $env:BERRY_DASHBOARD_ROOT; $stdout = Join-Path $root '.berry-dashboard.log'; $stderr = Join-Path $root '.berry-dashboard-error.log'; $node = (Get-Command node -ErrorAction Stop).Source; $process = Start-Process -FilePath $node -ArgumentList 'packages\berryapi\dist\server.js' -WorkingDirectory $root -WindowStyle Hidden -RedirectStandardOutput $stdout -RedirectStandardError $stderr -PassThru; Set-Content -LiteralPath $env:BERRY_DASHBOARD_PID -Value $process.Id -Encoding ascii" +if errorlevel 1 ( + echo. + echo ERRO: nao foi possivel iniciar a BerryAPI. + echo. + pause + exit /b 1 +) + +powershell -NoProfile -ExecutionPolicy Bypass -Command "$deadline = (Get-Date).AddSeconds(30); do { try { $processId = [int](Get-Content -Raw -LiteralPath $env:BERRY_DASHBOARD_PID); $process = Get-Process -Id $processId -ErrorAction Stop; $headers = @{ Authorization = 'Bearer ' + $env:API_KEY }; $response = Invoke-RestMethod -Uri 'http://127.0.0.1:3000/studio/api/capabilities' -Headers $headers -TimeoutSec 2; if ($process -and $response.success) { exit 0 } } catch {}; Start-Sleep -Milliseconds 500 } while ((Get-Date) -lt $deadline); exit 1" +if errorlevel 1 goto :start_error + +echo. +echo Dashboard pronta: http://127.0.0.1:3000/studio +echo Para encerrar, execute parar-dashboard.cmd. +echo. + +if /I not "%BERRY_NO_BROWSER%"=="1" start "" "%BERRY_DASHBOARD_URL%" +exit /b 0 + +:build_error +echo. +echo ERRO: nao foi possivel preparar o projeto. +echo Revise as mensagens acima e tente novamente. +echo. +pause +exit /b 1 + +:port_error +echo. +echo ERRO: a porta 3000 ja esta sendo usada por outro programa. +echo Encerre esse programa e execute iniciar-dashboard.cmd novamente. +echo. +pause +exit /b 1 + +:start_error +call "%~dp0parar-dashboard.cmd" >nul 2>&1 +echo. +echo ERRO: a BerryAPI nao respondeu em ate 30 segundos. +echo Consulte os arquivos: +echo %~dp0.berry-dashboard.log +echo %~dp0.berry-dashboard-error.log +echo. +pause +exit /b 1 diff --git a/mint.json b/mint.json new file mode 100644 index 0000000..88c7fcd --- /dev/null +++ b/mint.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://mintlify.com/schema.json", + "name": "BerryAPI", + "theme": "purple", + "colors": { + "primary": "#7c3aed", + "light": "#a855f7", + "dark": "#5b21b6" + }, + "navigation": [ + { + "group": "Introduction", + "pages": [ + "docs/index", + "docs/getting-started", + "docs/instances", + "docs/messages", + "docs/webhooks", + "docs/realtime", + "docs/openapi" + ] + } + ] +} diff --git a/package-lock.json b/package-lock.json index 4727499..85e11de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1338 +1,12105 @@ { - "name": "berryprotocol", - "version": "0.1.10", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "berryprotocol", - "version": "0.1.10", - "license": "Apache-2.0", - "dependencies": { - "@berrysdk/auth": "0.1.1", - "@berrysdk/core": "0.1.8", - "@berrysdk/events": "0.1.4", - "@berrysdk/media": "0.1.1", - "@berrysdk/messages": "0.1.3", - "@berrysdk/store": "0.1.2", - "pino": "^9.6.0" - }, - "devDependencies": { - "typescript": "^5.8.3" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@berrysdk/auth": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@berrysdk/auth/-/auth-0.1.1.tgz", - "integrity": "sha512-VqWbCFYO5u7iUtyUAkQtyTvKgBakDq+zM0Cm6I7a2v8f1riipdaZYSvfqwb+1XLFIaT16pmnDKJzXL3EAO1f/w==", - "license": "Apache-2.0", - "dependencies": { - "@berrysdk/events": "0.1.1", - "@berrysdk/store": "0.1.1" - } - }, - "node_modules/@berrysdk/auth/node_modules/@berrysdk/events": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@berrysdk/events/-/events-0.1.1.tgz", - "integrity": "sha512-ORU4VI/VxKaB5SUt8HaD8d6sWkjE7xtzKGtSIBQGXCgvWYupa6ooDHLmSoWPH6u+bQLYaIVSisxus/Ip8AeZ9A==", - "license": "Apache-2.0" - }, - "node_modules/@berrysdk/auth/node_modules/@berrysdk/store": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@berrysdk/store/-/store-0.1.1.tgz", - "integrity": "sha512-e5XaXWUWRC+mfyta8DCJC7UNJrhssQHiFv2VzG0n3Uh8SDbba31BAw2zUl0v8MaP20TCl+fjX1GUTgzhQH1oTw==", - "license": "Apache-2.0", - "dependencies": { - "@berrysdk/events": "0.1.1", - "better-sqlite3": "^11.10.0" - } - }, - "node_modules/@berrysdk/core": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@berrysdk/core/-/core-0.1.8.tgz", - "integrity": "sha512-KblXZ7+C+RkWJHlaYadmqYYazXW2GnmrtjdC/e6+vbfljQwM4OuKQEMJQanpw2DFYmJ1xOR8YFdbbU/3QV7E3A==", - "license": "Apache-2.0", - "dependencies": { - "@berrysdk/auth": "0.1.1", - "@berrysdk/events": "0.1.2", - "@berrysdk/media": "0.1.1", - "@berrysdk/messages": "0.1.1", - "@berrysdk/socket": "0.1.7", - "@berrysdk/store": "0.1.2", - "pino": "^9.6.0", - "qrcode-terminal": "^0.12.0" - } - }, - "node_modules/@berrysdk/core/node_modules/@berrysdk/events": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@berrysdk/events/-/events-0.1.2.tgz", - "integrity": "sha512-vn5n5vzQwR/YOAkdwX3BXES8Bb0b0nvklY/woCJi9FH2aNDLpSOToN5KSBa7OWY/ptlVhX/GMJy5WmHFeO9p0Q==", - "license": "Apache-2.0" - }, - "node_modules/@berrysdk/core/node_modules/@berrysdk/messages": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@berrysdk/messages/-/messages-0.1.1.tgz", - "integrity": "sha512-8HqdAUU92pu1SMhbsuhZwDDy6y5ifgJ3KgrMSMMe6rvGw0Um2daq1bICWvsZcdYXJWd71m+PSK/eG8/233nV4A==", - "license": "Apache-2.0", - "dependencies": { - "@berrysdk/events": "0.1.1" - } - }, - "node_modules/@berrysdk/core/node_modules/@berrysdk/messages/node_modules/@berrysdk/events": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@berrysdk/events/-/events-0.1.1.tgz", - "integrity": "sha512-ORU4VI/VxKaB5SUt8HaD8d6sWkjE7xtzKGtSIBQGXCgvWYupa6ooDHLmSoWPH6u+bQLYaIVSisxus/Ip8AeZ9A==", - "license": "Apache-2.0" - }, - "node_modules/@berrysdk/events": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@berrysdk/events/-/events-0.1.4.tgz", - "integrity": "sha512-hK3L6K6PAh7emWSn0/NuYzQnEqJafHr0DOjcdnvriDmj+kaJdJy2Kukav7pOcPRSFRPzyvvx61IiXuztEe5f9Q==", - "license": "Apache-2.0" - }, - "node_modules/@berrysdk/media": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@berrysdk/media/-/media-0.1.1.tgz", - "integrity": "sha512-8X+DqmbZxM72uhstuSzeW20eYZ+4nSkxmMl7LbE0Ezf9OKpLwQHquDN1dGmH5k6Z1YdD+Cpv7k0CzVFvWv+o9w==", - "license": "Apache-2.0" - }, - "node_modules/@berrysdk/messages": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@berrysdk/messages/-/messages-0.1.3.tgz", - "integrity": "sha512-SeBGddv6WCjYbfQlZ5SA/PbHGL71OrP7eLBPhAF3RROS776wDLo6Sx1aa3gkGi0E7mogpaL6W6uv230T2uTOFw==", - "license": "Apache-2.0", - "dependencies": { - "@berrysdk/events": "0.1.4" - } - }, - "node_modules/@berrysdk/proto": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@berrysdk/proto/-/proto-0.1.1.tgz", - "integrity": "sha512-Tx8tXfKJ74mqaLtKchaDLDtb1zl0WhvX+HiWcr9/pQC6zjNO1ZHie83T1SHXQyO0cWdVCgNkcltYpKAHZLG7Ng==", - "license": "Apache-2.0", - "dependencies": { - "@wppconnect/wa-proto": "^2.3000.1030818676", - "protobufjs": "^7.4.0" - } - }, - "node_modules/@berrysdk/socket": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@berrysdk/socket/-/socket-0.1.7.tgz", - "integrity": "sha512-T7lpxr0Ds/3Wp99odxbeD7OUfL1vXKlBjtnGgfw7tBoxFmDa9R2dXpih+nLXloPeikbOSDrFiN+N+1Hu0nFOEw==", - "license": "Apache-2.0", - "dependencies": { - "@berrysdk/events": "0.1.2", - "@berrysdk/proto": "0.1.1", - "@berrysdk/transport": "0.1.7", - "@berrysdk/wa-message": "0.1.7", - "pino": "^9.6.0", - "ws": "^8.18.1" - } - }, - "node_modules/@berrysdk/socket/node_modules/@berrysdk/events": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@berrysdk/events/-/events-0.1.2.tgz", - "integrity": "sha512-vn5n5vzQwR/YOAkdwX3BXES8Bb0b0nvklY/woCJi9FH2aNDLpSOToN5KSBa7OWY/ptlVhX/GMJy5WmHFeO9p0Q==", - "license": "Apache-2.0" - }, - "node_modules/@berrysdk/store": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@berrysdk/store/-/store-0.1.2.tgz", - "integrity": "sha512-v8aa34iV26q9GOsxshoQcduryOUsEApE1fXGXP0EaIGNgh3e7wxO5Iku963MY+HEVyTQ7654d6T0F1ku9IvzKQ==", - "license": "Apache-2.0", - "dependencies": { - "@berrysdk/events": "0.1.1", - "better-sqlite3": "^11.10.0" - } - }, - "node_modules/@berrysdk/store/node_modules/@berrysdk/events": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@berrysdk/events/-/events-0.1.1.tgz", - "integrity": "sha512-ORU4VI/VxKaB5SUt8HaD8d6sWkjE7xtzKGtSIBQGXCgvWYupa6ooDHLmSoWPH6u+bQLYaIVSisxus/Ip8AeZ9A==", - "license": "Apache-2.0" - }, - "node_modules/@berrysdk/transport": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@berrysdk/transport/-/transport-0.1.7.tgz", - "integrity": "sha512-S9zfDjGm/zCE45bwLApjCN6EaXsnVmuiQEebqvMHCNx66UBYT5zFmt7vWLG34PfJbbSzAZmLCqzueRO1BPtrrg==", - "license": "Apache-2.0", - "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", - "lru-cache": "^11.1.0", - "music-metadata": "^11.12.3", - "p-queue": "^9.0.0", - "pino": "^9.6.0", - "protobufjs": "^7.5.6", - "whatsapp-rust-bridge": "0.5.3", - "ws": "^8.18.1" - }, - "peerDependencies": { - "audio-decode": "^2.1.3", - "jimp": "^1.6.1", - "link-preview-js": "^3.0.0", - "sharp": "*" - }, - "peerDependenciesMeta": { - "audio-decode": { - "optional": true - }, - "jimp": { - "optional": true - }, - "link-preview-js": { - "optional": true - }, - "sharp": { - "optional": true - } - } - }, - "node_modules/@berrysdk/wa-message": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@berrysdk/wa-message/-/wa-message-0.1.7.tgz", - "integrity": "sha512-Z0rs87BBB7aQ2QI2MorDU61WaobyZ+fjiMxsrnf520O3Bk5UnZPinwa51oLOBzzsRp/zouGdPZQbXrtr3QkDBw==", - "license": "Apache-2.0", - "dependencies": { - "@berrysdk/events": "0.1.2", - "@berrysdk/proto": "0.1.1", - "@berrysdk/transport": "0.1.7" - } - }, - "node_modules/@berrysdk/wa-message/node_modules/@berrysdk/events": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@berrysdk/events/-/events-0.1.2.tgz", - "integrity": "sha512-vn5n5vzQwR/YOAkdwX3BXES8Bb0b0nvklY/woCJi9FH2aNDLpSOToN5KSBa7OWY/ptlVhX/GMJy5WmHFeO9p0Q==", - "license": "Apache-2.0" - }, - "node_modules/@borewit/text-codec": { - "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", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@cacheable/memory": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.8.tgz", - "integrity": "sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==", - "license": "MIT", - "dependencies": { - "@cacheable/utils": "^2.4.0", - "@keyv/bigmap": "^1.3.1", - "hookified": "^1.15.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@cacheable/node-cache": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz", - "integrity": "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==", - "license": "MIT", - "dependencies": { - "cacheable": "^2.3.1", - "hookified": "^1.14.0", - "keyv": "^5.5.5" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@cacheable/utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", - "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", - "license": "MIT", - "dependencies": { - "hashery": "^1.5.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@hapi/boom": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", - "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@keyv/bigmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", - "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", - "license": "MIT", - "dependencies": { - "hashery": "^1.4.0", - "hookified": "^1.15.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "keyv": "^5.6.0" - } - }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", - "license": "MIT" - }, - "node_modules/@pinojs/redact": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", - "license": "MIT" - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "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==", - "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==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "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/@tokenizer/inflate": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", - "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" - }, - "engines": { - "node": ">=18" - }, - "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", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.8.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", - "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", - "license": "MIT", - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" - } - }, - "node_modules/@wppconnect/wa-proto": { - "version": "2.3000.1030818676", - "resolved": "https://registry.npmjs.org/@wppconnect/wa-proto/-/wa-proto-2.3000.1030818676.tgz", - "integrity": "sha512-HiSaUvkJ2sdbZZXLV7mL1EYnnNlACqclFbD8iyG+G3n9uNFPM/j4S4pQW7dEw4k/V1TxUc5gbfucDfpDwHj9Og==", - "license": "Apache-2.0", - "dependencies": { - "long": "^5.3.2", - "protobufjs": "^7.4.0" - } - }, - "node_modules/async-mutex": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", - "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.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==", - "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" - }, - "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "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.1.13" - } - }, - "node_modules/cacheable": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz", - "integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==", - "license": "MIT", - "dependencies": { - "@cacheable/memory": "^2.0.8", - "@cacheable/utils": "^2.4.1", - "hookified": "^1.15.0", - "keyv": "^5.6.0", - "qified": "^0.10.1" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/curve25519-js": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz", - "integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=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/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/file-type": { - "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": { - "@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/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/hashery": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", - "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", - "license": "MIT", - "dependencies": { - "hookified": "^1.15.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/hookified": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", - "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", - "license": "MIT" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "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": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, - "node_modules/libsignal": { - "version": "6.0.0", - "resolved": "git+ssh://git@github.com/whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7", - "license": "GPL-3.0", - "dependencies": { - "curve25519-js": "^0.0.4", - "protobufjs": "^7.5.5" - } - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", - "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/music-metadata": { - "version": "11.12.3", - "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.12.3.tgz", - "integrity": "sha512-n6hSTZkuD59qWgHh6IP5dtDlDZQXoxk/bcA85Jywg8Z1iFrlNgl2+GTFgjZyn52W5UgQpV42V4XqrQZZAMbZTQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - }, - { - "type": "buymeacoffee", - "url": "https://buymeacoffee.com/borewit" - } - ], - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.2.2", - "@tokenizer/token": "^0.3.0", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "file-type": "^21.3.1", - "media-typer": "^1.1.0", - "strtok3": "^10.3.4", - "token-types": "^6.1.2", - "uint8array-extras": "^1.5.0", - "win-guid": "^0.2.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-queue": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", - "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^5.0.4", - "p-timeout": "^7.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", - "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pino": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", - "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", - "license": "MIT", - "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "license": "MIT", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", - "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", - "license": "MIT" - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/protobufjs": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", - "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.1", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "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/qified": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", - "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", - "license": "MIT", - "dependencies": { - "hookified": "^2.1.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/qified/node_modules/hookified": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", - "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", - "license": "MIT" - }, - "node_modules/qrcode-terminal": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", - "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", - "bin": { - "qrcode-terminal": "bin/qrcode-terminal.js" - } - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "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" - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "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" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "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": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/sonic-boom": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", - "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strtok3": { - "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" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "license": "MIT", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "node_modules/token-types": { - "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": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/whatsapp-rust-bridge": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/whatsapp-rust-bridge/-/whatsapp-rust-bridge-0.5.3.tgz", - "integrity": "sha512-Xb3GAgtWQQJ30oI4a4pjM4+YUeli9CMLTwTIewUrb+AJMFElIkiT5uo+j1Zhc+amiV0Jj+LfX76c/EEZirJbGA==", - "license": "MIT" - }, - "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/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true + "name": "berrysdk", + "version": "0.1.10", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "berrysdk", + "version": "0.1.10", + "license": "Apache-2.0", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "berryprotocol": "^0.1.10", + "dotenv": "^17.4.2", + "node-hid": "^3.3.0", + "openai": "^6.38.0", + "qrcode-terminal": "^0.12.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/express": "^5.0.1", + "@types/node": "^22.15.3", + "@types/qrcode-terminal": "^0.12.2", + "@types/ws": "^8.18.1", + "rimraf": "^6.0.1", + "tsx": "^4.19.4", + "typescript": "^5.8.3" + }, + "engines": { + "node": "20.x || >=22 <27" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "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" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@berrysdk/auth": { + "resolved": "packages/auth", + "link": true + }, + "node_modules/@berrysdk/berry-otp": { + "resolved": "packages/berry-otp", + "link": true + }, + "node_modules/@berrysdk/berryonepro": { + "resolved": "packages/berryonepro", + "link": true + }, + "node_modules/@berrysdk/cli": { + "resolved": "packages/cli", + "link": true + }, + "node_modules/@berrysdk/core": { + "resolved": "packages/core", + "link": true + }, + "node_modules/@berrysdk/events": { + "resolved": "packages/events", + "link": true + }, + "node_modules/@berrysdk/gateway": { + "resolved": "packages/gateway", + "link": true + }, + "node_modules/@berrysdk/media": { + "resolved": "packages/media", + "link": true + }, + "node_modules/@berrysdk/messages": { + "resolved": "packages/messages", + "link": true + }, + "node_modules/@berrysdk/proto": { + "resolved": "packages/protocol", + "link": true + }, + "node_modules/@berrysdk/socket": { + "resolved": "packages/socket", + "link": true + }, + "node_modules/@berrysdk/store": { + "resolved": "packages/store", + "link": true + }, + "node_modules/@berrysdk/transport": { + "resolved": "packages/transport", + "link": true + }, + "node_modules/@berrysdk/wa-message": { + "resolved": "packages/wa-message", + "link": true + }, + "node_modules/@borewit/text-codec": { + "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", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.8.tgz", + "integrity": "sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==", + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.4.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/node-cache": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz", + "integrity": "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==", + "license": "MIT", + "dependencies": { + "cacheable": "^2.3.1", + "hookified": "^1.14.0", + "keyv": "^5.5.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", + "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz", + "integrity": "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/proxy-addr/node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", + "integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/static": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-9.1.3.tgz", + "integrity": "sha512-aXrYtsiryLhRxRNaxNqsn7FUISeb7rB9q4eHUPIot5aeQBLNahnz1m6thzm7JWC1poSGXS9XrX8DvuMivp2hkQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^1.0.1", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^13.0.0" + } + }, + "node_modules/@fastify/swagger": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.7.0.tgz", + "integrity": "sha512-Vp1SC1GC2Hrkd3faFILv86BzUNyFz5N4/xdExqtCgkGASOzn/x+eMe4qXIGq7cdT6wif/P/oa6r1Ruqx19paZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "json-schema-resolver": "^3.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.2" + } + }, + "node_modules/@fastify/swagger-ui": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-5.2.6.tgz", + "integrity": "sha512-OMnms0O5s9wb6wis/K5nlrAMLsgUbr1GA8uphM41IasWe3AFdgxz6r/3bA9HTxlDNUYc2FGGKeqMp3ntxmSiNA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/static": "^9.1.2", + "fastify-plugin": "^5.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.1" + } + }, + "node_modules/@fastify/websocket": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-11.2.0.tgz", + "integrity": "sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.3", + "fastify-plugin": "^5.0.0", + "ws": "^8.16.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@hapi/boom": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", + "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "9.x.x" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, + "node_modules/@lifeomic/attempt": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@lifeomic/attempt/-/attempt-3.1.0.tgz", + "integrity": "sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==", + "license": "MIT" + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", + "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "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==", + "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==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "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/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", + "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", + "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", + "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "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", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bluebird": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.42.tgz", + "integrity": "sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==", + "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", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT" + }, + "node_modules/@types/chance": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@types/chance/-/chance-1.1.8.tgz", + "integrity": "sha512-isycXjm4a88/zl3n9lfgMcEClR9wUi+m72lkE9MvEbkgZDQrKzolWbonBiUqDNAJFIwO9GKjzmJFq79qtKVTAQ==", + "license": "MIT" + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qrcode-terminal": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/qrcode-terminal/-/qrcode-terminal-0.12.2.tgz", + "integrity": "sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/request-promise": { + "version": "4.1.51", + "resolved": "https://registry.npmjs.org/@types/request-promise/-/request-promise-4.1.51.tgz", + "integrity": "sha512-qVcP9Fuzh9oaAh8oPxiSoWMFGnWKkJDknnij66vi09Yiy62bsSDqtd+fG5kIM9wLLgZsRP3Y6acqj9O/v2ZtRw==", + "license": "MIT", + "dependencies": { + "@types/bluebird": "*", + "@types/request": "*" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.3", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@wppconnect/wa-proto": { + "version": "2.3000.1030818676", + "resolved": "https://registry.npmjs.org/@wppconnect/wa-proto/-/wa-proto-2.3000.1030818676.tgz", + "integrity": "sha512-HiSaUvkJ2sdbZZXLV7mL1EYnnNlACqclFbD8iyG+G3n9uNFPM/j4S4pQW7dEw4k/V1TxUc5gbfucDfpDwHj9Og==", + "license": "Apache-2.0", + "dependencies": { + "long": "^5.3.2", + "protobufjs": "^7.4.0" + } + }, + "node_modules/@xyflow/react": { + "version": "12.10.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz", + "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.76", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.76", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz", + "integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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==", + "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" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz", + "integrity": "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/berryapi": { + "resolved": "packages/berryapi", + "link": true + }, + "node_modules/berryonepro-probe": { + "resolved": "packages/berryonepro-probe", + "link": true + }, + "node_modules/berryotp": { + "resolved": "packages/berryotp", + "link": true + }, + "node_modules/berryprotocol": { + "resolved": "packages/berryprotocol", + "link": true + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "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" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz", + "integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==", + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.0.8", + "@cacheable/utils": "^2.4.1", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "node_modules/chance": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.13.tgz", + "integrity": "sha512-V6lQCljcLznE7tUYUM9EOAnnKXbctE6j/rdQkYOHIWbfGQbrzTsAXNW9CdU5XCo4ArXQCj/rb6HgxPlmGJcaUg==", + "license": "MIT" + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/class-transformer": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.3.1.tgz", + "integrity": "sha512-cKFwohpJbuMovS8xVLmn8N2AUbAuc8pVo4zEfsUVo8qgECOogns1WVk/FkOZoxhOPTyTYFckuoH+13FO+MQ8GA==", + "license": "MIT" + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/curve25519-js": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz", + "integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/drizzle-orm": { + "version": "0.44.7", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.7.tgz", + "integrity": "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.357", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.357.tgz", + "integrity": "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==", + "dev": true, + "license": "ISC" + }, + "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" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "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/enhanced-resolve": { + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "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" + } + }, + "node_modules/esutils": { + "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" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz", + "integrity": "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.8.5", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz", + "integrity": "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "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": { + "@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/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flat-cache/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "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==", + "license": "ISC" + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/har-validator/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/har-validator/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz", + "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/instagram-private-api": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/instagram-private-api/-/instagram-private-api-1.46.1.tgz", + "integrity": "sha512-fq0q6UfhpikKZ5Kw8HNwS6YpsNghE9I/uc8AM9Do9nsQ+3H1u0jLz+0t/FcGkGTjZz5VGvU8s2VbWj9wxchwYg==", + "license": "MIT", + "dependencies": { + "@lifeomic/attempt": "^3.0.0", + "@types/chance": "^1.0.2", + "@types/request-promise": "^4.1.43", + "bluebird": "^3.7.1", + "chance": "^1.0.18", + "class-transformer": "^0.3.1", + "debug": "^4.1.1", + "image-size": "^0.7.3", + "json-bigint": "^1.0.0", + "lodash": "^4.17.20", + "luxon": "^1.12.1", + "reflect-metadata": "^0.1.13", + "request": "^2.88.0", + "request-promise": "^4.2.4", + "rxjs": "^6.5.2", + "snakecase-keys": "^3.1.0", + "tough-cookie": "^2.5.0", + "ts-custom-error": "^2.2.2", + "ts-xor": "^1.0.6", + "url-regex-safe": "^3.0.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "re2": "^1.17.2" + }, + "peerDependenciesMeta": { + "re2": { + "optional": true + } + } + }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "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/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-resolver/-/json-schema-resolver-3.0.0.tgz", + "integrity": "sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fast-uri": "^3.0.5", + "rfdc": "^1.1.4" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/Eomm/json-schema-resolver?sponsor=1" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libsignal": { + "version": "6.0.0", + "resolved": "git+ssh://git@github.com/whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7", + "license": "GPL-3.0", + "dependencies": { + "curve25519-js": "^0.0.4", + "protobufjs": "^7.5.5" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lucide-react": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", + "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/luxon": { + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz", + "integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "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/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/music-metadata": { + "version": "11.12.3", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.12.3.tgz", + "integrity": "sha512-n6hSTZkuD59qWgHh6IP5dtDlDZQXoxk/bcA85Jywg8Z1iFrlNgl2+GTFgjZyn52W5UgQpV42V4XqrQZZAMbZTQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.2", + "@tokenizer/token": "^0.3.0", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "file-type": "^21.3.1", + "media-typer": "^1.1.0", + "strtok3": "^10.3.4", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0", + "win-guid": "^0.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "license": "MIT" + }, + "node_modules/node-hid": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-hid/-/node-hid-3.3.0.tgz", + "integrity": "sha512-j+dFgJLRAE0nufQKXk3IfS6T6YuHhCgMvz4TrG0sgtb6DSCdYpfJ1etcdmeCmPQjUgO+yo32ktVrRliNs/+fmg==", + "hasInstallScript": true, + "license": "(MIT OR X11)", + "dependencies": { + "node-addon-api": "^3.2.1", + "pkg-prebuilds": "^1.0.0" + }, + "bin": { + "hid-showdevices": "src/show-devices.js" + }, + "engines": { + "node": ">=10.16" + } + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openai": { + "version": "6.38.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.38.0.tgz", + "integrity": "sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "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==", + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.2.0.tgz", + "integrity": "sha512-dWgLE8AH0HjQ9fe74pUkKkvzzYT18Inp4zra3lKHnnwqGvcfcUBrvF2EAVX+envufDNBOzpPq/IBUONDbI7+3g==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "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/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "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": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pkg-prebuilds": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-prebuilds/-/pkg-prebuilds-1.0.0.tgz", + "integrity": "sha512-D9wlkXZCmjxj2kBHTw3fGSyjoahr33breGBoJcoezpi7ouYS59DJVOHMZ+dgqacSrZiJo4qtkXxLQTE+BqXJmQ==", + "license": "MIT", + "dependencies": { + "yargs": "^17.7.2" + }, + "bin": { + "pkg-prebuilds-copy": "bin/copy.mjs", + "pkg-prebuilds-verify": "bin/verify.mjs" + }, + "engines": { + "node": ">= 14.15.0" + } + }, + "node_modules/pkg-prebuilds/node_modules/cliui": { + "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==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/pkg-prebuilds/node_modules/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", + "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/pkg-prebuilds/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/pkg-prebuilds/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/pkg-prebuilds/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", + "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.1", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "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": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "license": "MIT" + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode-terminal": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz", + "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz", + "integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==", + "license": "MIT", + "dependencies": { + "react-router": "7.15.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "license": "MIT", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "license": "Apache-2.0" + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", + "deprecated": "request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "license": "ISC", + "dependencies": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "license": "ISC", + "dependencies": { + "lodash": "^4.17.19" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/request/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rolldown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", + "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.130.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.1", + "@rolldown/binding-darwin-arm64": "1.0.1", + "@rolldown/binding-darwin-x64": "1.0.1", + "@rolldown/binding-freebsd-x64": "1.0.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.1", + "@rolldown/binding-linux-arm64-musl": "1.0.1", + "@rolldown/binding-linux-ppc64-gnu": "1.0.1", + "@rolldown/binding-linux-s390x-gnu": "1.0.1", + "@rolldown/binding-linux-x64-gnu": "1.0.1", + "@rolldown/binding-linux-x64-musl": "1.0.1", + "@rolldown/binding-openharmony-arm64": "1.0.1", + "@rolldown/binding-wasm32-wasi": "1.0.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.1", + "@rolldown/binding-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/snakecase-keys": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-3.2.1.tgz", + "integrity": "sha512-CjU5pyRfwOtaOITYv5C8DzpZ8XA/ieRsDpr93HI2r6e3YInC6moZpSQbmUtg8cTk58tq2x3jcG2gv+p1IZGmMA==", + "license": "MIT", + "dependencies": { + "map-obj": "^4.1.0", + "to-snake-case": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "license": "ISC", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/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", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strtok3": { + "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" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/studio": { + "resolved": "packages/studio", + "link": true + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tlds": { + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", + "license": "MIT", + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/to-no-case": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", + "integrity": "sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==", + "license": "MIT" + }, + "node_modules/to-snake-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-snake-case/-/to-snake-case-1.0.0.tgz", + "integrity": "sha512-joRpzBAk1Bhi2eGEYBjukEWHOe/IvclOkiJl3DtA91jV6NwQ3MwXA4FHYeqk8BNp/D8bmi9tcNbRu/SozP0jbQ==", + "license": "MIT", + "dependencies": { + "to-space-case": "^1.0.0" + } + }, + "node_modules/to-space-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz", + "integrity": "sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==", + "license": "MIT", + "dependencies": { + "to-no-case": "^1.0.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "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": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-custom-error": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-2.2.2.tgz", + "integrity": "sha512-I0FEdfdatDjeigRqh1JFj67bcIKyRNm12UVGheBjs2pXgyELg2xeiQLVaWu1pVmNGXZVnz/fvycSU41moBIpOg==", + "deprecated": "npm package tarball contains useless codeclimate-reporter binary, please update to version 3.1.1. See https://github.com/adriengibrat/ts-custom-error/issues/32", + "license": "WTFPL", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/ts-xor": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-xor/-/ts-xor-1.3.0.tgz", + "integrity": "sha512-RLXVjliCzc1gfKQFLRpfeD0rrWmjnSTgj7+RFhoq3KRkUYa8LE/TIidYOzM5h+IdFBDSjjSgk9Lto9sdMfDFEA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-regex-safe": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-regex-safe/-/url-regex-safe-3.0.0.tgz", + "integrity": "sha512-+2U40NrcmtWFVjuxXVt9bGRw6c7/MgkGKN9xIfPrT/2RX0LTkkae6CCEDp93xqUN0UKm/rr821QnHd2dHQmN3A==", + "license": "MIT", + "dependencies": { + "ip-regex": "4.3.0", + "tlds": "^1.228.0" + }, + "engines": { + "node": ">= 10.12.0" + }, + "peerDependencies": { + "re2": "^1.17.2" + }, + "peerDependenciesMeta": { + "re2": { + "optional": true + } + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vite": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", + "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/whatsapp-rust-bridge": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/whatsapp-rust-bridge/-/whatsapp-rust-bridge-0.5.3.tgz", + "integrity": "sha512-Xb3GAgtWQQJ30oI4a4pjM4+YUeli9CMLTwTIewUrb+AJMFElIkiT5uo+j1Zhc+amiV0Jj+LfX76c/EEZirJbGA==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "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", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "packages/auth": { + "name": "@berrysdk/auth", + "version": "0.1.2", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/events": "^0.1.4", + "@berrysdk/store": "^0.1.4" + }, + "engines": { + "node": "20.x || >=22 <27" + } + }, + "packages/berry-otp": { + "name": "@berrysdk/berry-otp", + "version": "0.1.9", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/core": "^0.1.9" + } + }, + "packages/berry-otp/node_modules/@berrysdk/core/node_modules/@berrysdk/events": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@berrysdk/events/-/events-0.1.2.tgz", + "integrity": "sha512-vn5n5vzQwR/YOAkdwX3BXES8Bb0b0nvklY/woCJi9FH2aNDLpSOToN5KSBa7OWY/ptlVhX/GMJy5WmHFeO9p0Q==", + "extraneous": true, + "license": "Apache-2.0" + }, + "packages/berryapi": { + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/core": "^0.1.10", + "@fastify/cors": "^11.1.0", + "@fastify/swagger": "^9.5.1", + "@fastify/swagger-ui": "^5.2.3", + "@fastify/websocket": "^11.2.0", + "better-sqlite3": "^12.11.1", + "dotenv": "^16.6.1", + "drizzle-orm": "^0.44.2", + "fastify": "^5.6.1", + "fastify-plugin": "^5.1.0", + "pino": "^9.14.0", + "zod": "^4.1.8", + "zod-to-json-schema": "^3.24.6" + } + }, + "packages/berryapi/node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "packages/berryapi/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "packages/berryonepro": { + "name": "@berrysdk/berryonepro", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "dotenv": "^17.4.2", + "instagram-private-api": "^1.46.1", + "pino": "^9.14.0" + } + }, + "packages/berryonepro-probe": { + "version": "0.0.1", + "dependencies": { + "dotenv": "^17.4.2", + "playwright": "^1.54.2" + } + }, + "packages/berryotp": { + "version": "0.1.9", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/berry-otp": "^0.1.9" + }, + "peerDependencies": { + "berryprotocol": "^0.1.9" + } + }, + "packages/berryprotocol": { + "version": "0.1.10", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/core": "^0.1.10" + }, + "engines": { + "node": "20.x || >=22 <27" + } + }, + "packages/cli": { + "name": "@berrysdk/cli", + "version": "0.1.9", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/core": "^0.1.9", + "commander": "^14.0.0", + "pino": "^9.6.0" + } + }, + "packages/core": { + "name": "@berrysdk/core", + "version": "0.1.10", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/auth": "^0.1.2", + "@berrysdk/events": "^0.1.4", + "@berrysdk/media": "^0.1.2", + "@berrysdk/messages": "^0.1.3", + "@berrysdk/socket": "^0.1.10", + "@berrysdk/store": "^0.1.4", + "pino": "^9.6.0", + "qrcode-terminal": "^0.12.0" + }, + "engines": { + "node": "20.x || >=22 <27" + } + }, + "packages/core/node_modules/@berrysdk/auth/node_modules/@berrysdk/events": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@berrysdk/events/-/events-0.1.1.tgz", + "integrity": "sha512-ORU4VI/VxKaB5SUt8HaD8d6sWkjE7xtzKGtSIBQGXCgvWYupa6ooDHLmSoWPH6u+bQLYaIVSisxus/Ip8AeZ9A==", + "extraneous": true, + "license": "Apache-2.0" + }, + "packages/core/node_modules/@berrysdk/auth/node_modules/@berrysdk/store": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@berrysdk/store/-/store-0.1.1.tgz", + "integrity": "sha512-e5XaXWUWRC+mfyta8DCJC7UNJrhssQHiFv2VzG0n3Uh8SDbba31BAw2zUl0v8MaP20TCl+fjX1GUTgzhQH1oTw==", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/events": "0.1.1", + "better-sqlite3": "^11.10.0" + } + }, + "packages/core/node_modules/@berrysdk/messages/node_modules/@berrysdk/events": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@berrysdk/events/-/events-0.1.1.tgz", + "integrity": "sha512-ORU4VI/VxKaB5SUt8HaD8d6sWkjE7xtzKGtSIBQGXCgvWYupa6ooDHLmSoWPH6u+bQLYaIVSisxus/Ip8AeZ9A==", + "extraneous": true, + "license": "Apache-2.0" + }, + "packages/events": { + "name": "@berrysdk/events", + "version": "0.1.4", + "license": "Apache-2.0" + }, + "packages/gateway": { + "name": "@berrysdk/gateway", + "version": "0.1.9", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/core": "^0.1.9", + "express": "^5.1.0", + "pino": "^9.6.0" + } + }, + "packages/media": { + "name": "@berrysdk/media", + "version": "0.1.2", + "license": "Apache-2.0", + "engines": { + "node": "20.x || >=22 <27" + } + }, + "packages/messages": { + "name": "@berrysdk/messages", + "version": "0.1.3", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/events": "^0.1.4" + } + }, + "packages/protocol": { + "name": "@berrysdk/proto", + "version": "0.1.2", + "license": "Apache-2.0", + "dependencies": { + "@wppconnect/wa-proto": "^2.3000.1030818676", + "protobufjs": "^7.4.0" + }, + "engines": { + "node": "20.x || >=22 <27" + } + }, + "packages/socket": { + "name": "@berrysdk/socket", + "version": "0.1.10", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/events": "^0.1.4", + "@berrysdk/proto": "^0.1.2", + "@berrysdk/transport": "^0.1.9", + "@berrysdk/wa-message": "^0.1.10", + "pino": "^9.6.0", + "ws": "^8.18.1" + }, + "engines": { + "node": "20.x || >=22 <27" + } + }, + "packages/store": { + "name": "@berrysdk/store", + "version": "0.1.4", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/events": "^0.1.4", + "better-sqlite3": "^12.11.1" + }, + "engines": { + "node": "20.x || >=22 <27" + } + }, + "packages/store/node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "packages/studio": { + "version": "0.0.0", + "dependencies": { + "@radix-ui/react-label": "^2.1.7", + "@tailwindcss/vite": "^4.3.0", + "@xyflow/react": "^12.10.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.16.0", + "qrcode": "^1.5.4", + "radix-ui": "^1.4.3", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-router-dom": "^7.15.1", + "reactflow": "^11.11.4", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } + }, + "packages/studio/node_modules/@types/node": { + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "packages/studio/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "packages/studio/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "packages/transport": { + "name": "@berrysdk/transport", + "version": "0.1.9", + "license": "Apache-2.0", + "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", + "lru-cache": "^11.1.0", + "music-metadata": "^11.12.3", + "p-queue": "^9.0.0", + "pino": "^9.6.0", + "protobufjs": "^7.5.6", + "whatsapp-rust-bridge": "0.5.3", + "ws": "^8.18.1" + }, + "engines": { + "node": "20.x || >=22 <27" + }, + "peerDependencies": { + "audio-decode": "^2.1.3", + "jimp": "^1.6.1", + "link-preview-js": "^3.0.0", + "sharp": "*" + }, + "peerDependenciesMeta": { + "audio-decode": { + "optional": true + }, + "jimp": { + "optional": true + }, + "link-preview-js": { + "optional": true + }, + "sharp": { + "optional": true + } + } + }, + "packages/wa-message": { + "name": "@berrysdk/wa-message", + "version": "0.1.10", + "license": "Apache-2.0", + "dependencies": { + "@berrysdk/events": "^0.1.4", + "@berrysdk/proto": "^0.1.2", + "@berrysdk/transport": "^0.1.9" + }, + "engines": { + "node": "20.x || >=22 <27" + } } - } } - } } diff --git a/package.json b/package.json index 21a2451..2081cbe 100644 --- a/package.json +++ b/package.json @@ -1,106 +1,53 @@ { - "name": "berryprotocol", - "version": "0.1.10", - "description": "Public BerrySDK SDK for WhatsApp Web automation powered by BerryProtocol.", - "author": "BerrySDK", - "license": "Apache-2.0", - "type": "module", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist", - "README.md", - "README_PORTUGUESE.md", - "LICENSE", - "CHANGELOG.md" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.js", - "default": "./dist/index.js" - }, - "./Defaults": { - "types": "./dist/Defaults/index.d.ts", - "import": "./dist/Defaults/index.js", - "default": "./dist/Defaults/index.js" - }, - "./Socket": { - "types": "./dist/Socket/index.d.ts", - "import": "./dist/Socket/index.js", - "default": "./dist/Socket/index.js" - }, - "./Utils": { - "types": "./dist/Utils/index.d.ts", - "import": "./dist/Utils/index.js", - "default": "./dist/Utils/index.js" - }, - "./Types": { - "types": "./dist/Types/Index.d.ts", - "import": "./dist/Types/Index.js", - "default": "./dist/Types/Index.js" - }, - "./Store": { - "types": "./dist/Store/index.d.ts", - "import": "./dist/Store/index.js", - "default": "./dist/Store/index.js" - }, - "./Auth": { - "types": "./dist/Auth/index.d.ts", - "import": "./dist/Auth/index.js", - "default": "./dist/Auth/index.js" - }, - "./Media": { - "types": "./dist/Media/index.d.ts", - "import": "./dist/Media/index.js", - "default": "./dist/Media/index.js" - }, - "./Messages": { - "types": "./dist/Messages/index.d.ts", - "import": "./dist/Messages/index.js", - "default": "./dist/Messages/index.js" + "name": "berrysdk", + "version": "0.1.10", + "private": true, + "type": "module", + "engines": { + "node": "20.x || >=22 <27" + }, + "description": "Berry SDK monorepo for WhatsApp Web SDK, transport, proto wrappers, and publishing packages.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "keywords": [ + "whatsapp", + "sdk", + "bot", + "berrysdk", + "whatsapp-web" + ], + "workspaces": [ + "packages/*" + ], + "dependencies": { + "berryprotocol": "^0.1.10", + "dotenv": "^17.4.2", + "node-hid": "^3.3.0", + "openai": "^6.38.0", + "qrcode-terminal": "^0.12.0" + }, + "scripts": { + "build": "tsc -b packages/events packages/transport packages/protocol packages/messages packages/wa-message packages/media packages/store packages/auth packages/socket packages/core packages/berry-otp packages/berryprotocol packages/berryotp packages/gateway packages/cli packages/berryapi && npm run build --workspace studio", + "clean": "powershell -NoProfile -Command \"Get-ChildItem packages -Directory | ForEach-Object { Remove-Item -Recurse -Force (Join-Path $_.FullName 'dist') -ErrorAction SilentlyContinue; Remove-Item -Force (Join-Path $_.FullName '.tsbuildinfo') -ErrorAction SilentlyContinue }\"", + "prepare:publish": "npm run clean && npm run build", + "pack:check": "npm pack --workspaces --json", + "dev": "npm run dev --workspace berryapi", + "start": "npm run start --workspace berryapi", + "dev:gateway": "npm run build && node packages/gateway/dist/index.js", + "dev:cli": "npm run build && node packages/cli/dist/index.js", + "dev:berryapi": "npm run dev --workspace berryapi", + "start:berryapi": "npm run start --workspace berryapi", + "start:gateway": "node packages/gateway/dist/index.js", + "start:cli": "node packages/cli/dist/index.js" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/express": "^5.0.1", + "@types/node": "^22.15.3", + "@types/qrcode-terminal": "^0.12.2", + "@types/ws": "^8.18.1", + "rimraf": "^6.0.1", + "tsx": "^4.19.4", + "typescript": "^5.8.3" } - }, - "repository": { - "type": "git", - "url": "git+https://github.com/BerrySDK/BerryProtocol.git" - }, - "homepage": "https://github.com/BerrySDK/BerryProtocol#readme", - "bugs": { - "url": "https://github.com/BerrySDK/BerryProtocol/issues" - }, - "keywords": [ - "whatsapp", - "whatsapp-web", - "berrysdk", - "berryprotocol", - "bot", - "automation", - "api", - "sdk" - ], - "engines": { - "node": ">=20.0.0" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "build": "tsc -p tsconfig.build.json", - "clean": "if exist dist rmdir /s /q dist", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "@berrysdk/auth": "0.1.1", - "@berrysdk/core": "0.1.8", - "@berrysdk/events": "0.1.4", - "@berrysdk/media": "0.1.1", - "@berrysdk/messages": "0.1.3", - "@berrysdk/store": "0.1.2", - "pino": "^9.6.0" - }, - "devDependencies": { - "typescript": "^5.8.3" - } } diff --git a/packages/auth/package.json b/packages/auth/package.json new file mode 100644 index 0000000..a3be78b --- /dev/null +++ b/packages/auth/package.json @@ -0,0 +1,29 @@ +{ + "name": "@berrysdk/auth", + "version": "0.1.2", + "description": "Session and auth state helpers for Berry Protocol.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "engines": { + "node": "20.x || >=22 <27" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@berrysdk/events": "^0.1.4", + "@berrysdk/store": "^0.1.4" + } +} diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts new file mode 100644 index 0000000..5eaf203 --- /dev/null +++ b/packages/auth/src/index.ts @@ -0,0 +1,63 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import { AuthStateSnapshot } from "@berrysdk/events"; +import { SQLiteStore } from "@berrysdk/store"; +import { randomUUID } from "node:crypto"; + +export interface SessionStore { + load(sessionId: string): Promise; + save(sessionId: string, snapshot: AuthStateSnapshot): Promise; + remove(sessionId: string): Promise; +} + +export class SQLiteSessionStore implements SessionStore { + constructor(private readonly store: SQLiteStore) {} + + async load(sessionId: string): Promise { + return this.store.getSession(sessionId); + } + + async save(sessionId: string, snapshot: AuthStateSnapshot): Promise { + this.store.saveSession(sessionId, snapshot); + } + + async remove(sessionId: string): Promise { + this.store.deleteSession(sessionId); + } +} + +export class SessionManager { + constructor(private readonly sessionStore: SessionStore) {} + + async get(sessionId: string): Promise { + const snapshot = await this.sessionStore.load(sessionId); + if (snapshot) { + return snapshot; + } + + const created: AuthStateSnapshot = { + sessionId, + registered: false, + clientId: randomUUID(), + }; + + await this.sessionStore.save(sessionId, created); + return created; + } + + async update(sessionId: string, partial: Partial): Promise { + const current = await this.get(sessionId); + const next = { ...current, ...partial }; + await this.sessionStore.save(sessionId, next); + return next; + } + + async clear(sessionId: string): Promise { + await this.sessionStore.remove(sessionId); + } +} diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json new file mode 100644 index 0000000..a062b9e --- /dev/null +++ b/packages/auth/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../events" + }, + { + "path": "../store" + } + ] +} + diff --git a/packages/berry-otp/README.md b/packages/berry-otp/README.md new file mode 100644 index 0000000..f3b4106 --- /dev/null +++ b/packages/berry-otp/README.md @@ -0,0 +1,50 @@ +# @berrysdk/berry-otp + +Official OTP flows for Berry SDK using WhatsApp Web transport, native-flow interactive buttons, and fallback legacy mode. + +## Important notes + +- This package does **not** use the official WhatsApp Business API. +- It relies on WhatsApp Web behavior observed in real tests. +- `copy-code` was validated in real tests on WhatsApp Web `2.3000.x`. +- WhatsApp can change protocol behavior at any time. +- `stable` mode exists as a fallback. +- `editOnExpire` requires `client.editMessage(...)` support in the SDK. +- Once `verify()` succeeds, the OTP becomes `used`, so it will not expire and edit afterward. + +## Modes + +- `stable` +- `copy-code` +- `experimental-copy-code` + +Recommended default: + +- `copy-code` + +## Example + +```ts +import { BerryClient } from "@berrysdk/core"; +import { BerryOTP } from "@berrysdk/berry-otp"; + +const client = new BerryClient({ + sessionId: "otp-session", +}); + +await client.connectWithQr(); +await client.waitUntilConnected(); + +const otp = BerryOTP.createLoginFlow(client, { + issuer: "BerryProtocol", + ttlMs: 2 * 60 * 1000, + mode: "copy-code", +}); + +const sent = await otp.sendLoginCode("5511999999999@s.whatsapp.net", { + userId: "user-001", + metadata: { source: "example" }, +}); + +console.log(sent); +``` diff --git a/packages/berry-otp/package.json b/packages/berry-otp/package.json new file mode 100644 index 0000000..44ad88e --- /dev/null +++ b/packages/berry-otp/package.json @@ -0,0 +1,28 @@ +{ + "name": "@berrysdk/berry-otp", + "version": "0.1.9", + "description": "Official OTP flows for Berry SDK on top of WhatsApp native-flow and fallback buttons.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "README.md" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@berrysdk/core": "^0.1.9" + } +} diff --git a/packages/berry-otp/src/index.ts b/packages/berry-otp/src/index.ts new file mode 100644 index 0000000..a6983d2 --- /dev/null +++ b/packages/berry-otp/src/index.ts @@ -0,0 +1,646 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import crypto, { randomUUID } from "node:crypto"; +import { EventEmitter } from "node:events"; +import { MemoryOTPStore } from "./store.js"; +import { renderOTPTemplate } from "./templates.js"; +import type { + BerryOTPEventMap, + BerryOTPOptions, + BerryOTPRecord, + BerryOTPPublicRecord, + BerryOTPSendOptions, + BerryOTPSendResult, + BerryOTPStatus, + BerryOTPStore, + BerryOTPTemplate, + BerryOTPVerifyResult, +} from "./types.js"; + +export interface BerryOTPClient { + on(event: "message.received", listener: (message: BerryOTPIncomingMessage) => void): unknown; + off(event: "message.received", listener: (message: BerryOTPIncomingMessage) => void): unknown; + sendMessage(to: string, content: Record): Promise<{ id: string }>; + sendLegacyButtons( + to: string, + payload: { + text: string; + footer?: string; + buttons: Array<{ + id: string; + title: string; + kind?: "reply" | "quick_reply" | "copy_code" | "cta_url"; + code?: string; + url?: string; + }>; + }, + ): Promise<{ id: string }>; + sendText(to: string, text: string): Promise<{ id: string }>; + editMessage(to: string, messageId: string, text: string): Promise<{ id: string }>; +} + +export interface BerryOTPIncomingMessage { + buttonId?: string; + selectedButtonId?: string; + rawButtonParamsJson?: string; +} + +type FlowDefaults = { + template: BerryOTPTemplate; + purpose: string; + issuer: string; + ttlMs: number; +}; + +const DEFAULTS: Required> = { + issuer: "BerryOTP", + codeLength: 6, + ttlMs: 5 * 60 * 1000, + footer: "Este codigo dura {minutes} minutos.", + editOnExpire: true, + mode: "copy-code", + maxAttempts: 5, + rateLimitMs: 60 * 1000, + autoReplyOnDenied: true, +}; + +const EXPIRED_TEXT = [ + "Codigo expirado", + "", + "Este codigo nao e mais valido.", + "Solicite um novo codigo para continuar.", +].join("\n"); + +const DENIED_REPLY_TEXT = "Entendido. Esse codigo foi cancelado e nao podera ser usado."; + +export class BerryOTP extends EventEmitter { + private readonly options: Required> & { store: BerryOTPStore }; + private readonly timers = new Map(); + private readonly recipientIndex = new Map>(); + private readonly lastSentByRecipient = new Map(); + private readonly flowDefaults: FlowDefaults; + private readonly boundIncomingListener: (message: BerryOTPIncomingMessage) => void; + private readonly cleaner: NodeJS.Timeout; + + constructor( + private readonly client: BerryOTPClient, + options: BerryOTPOptions = {}, + flowDefaults?: Partial, + ) { + super(); + + if (!client) { + throw new Error("BerryOTP requires a BerryClient instance."); + } + + this.options = { + ...DEFAULTS, + ...options, + store: options.store ?? new MemoryOTPStore(), + }; + this.flowDefaults = { + template: flowDefaults?.template ?? "generic", + purpose: flowDefaults?.purpose ?? "continuar", + issuer: flowDefaults?.issuer ?? this.options.issuer, + ttlMs: flowDefaults?.ttlMs ?? this.options.ttlMs, + }; + this.boundIncomingListener = (message) => { + void this.handleIncomingMessage(message); + }; + + this.client.on("message.received", this.boundIncomingListener); + this.cleaner = setInterval(() => { + void this.cleanup().catch((error) => this.emit("error", error as Error)); + }, 60 * 1000); + this.cleaner.unref?.(); + } + + static createLoginFlow(client: BerryOTPClient, options: BerryOTPOptions = {}): BerryOTP { + return new BerryOTP( + client, + { + ...options, + issuer: options.issuer ?? "Berry Login", + ttlMs: options.ttlMs ?? 5 * 60 * 1000, + }, + { + template: "login", + purpose: "entrar na conta", + issuer: options.issuer ?? "Berry Login", + ttlMs: options.ttlMs ?? 5 * 60 * 1000, + }, + ); + } + + static createPasswordResetFlow(client: BerryOTPClient, options: BerryOTPOptions = {}): BerryOTP { + return new BerryOTP( + client, + { + ...options, + issuer: options.issuer ?? "Berry Password Reset", + ttlMs: options.ttlMs ?? 10 * 60 * 1000, + }, + { + template: "password_reset", + purpose: "redefinir sua senha", + issuer: options.issuer ?? "Berry Password Reset", + ttlMs: options.ttlMs ?? 10 * 60 * 1000, + }, + ); + } + + static create2FAFlow(client: BerryOTPClient, options: BerryOTPOptions = {}): BerryOTP { + return new BerryOTP( + client, + { + ...options, + issuer: options.issuer ?? "Berry 2FA", + ttlMs: options.ttlMs ?? 3 * 60 * 1000, + }, + { + template: "2fa", + purpose: "confirmar sua autenticacao em duas etapas", + issuer: options.issuer ?? "Berry 2FA", + ttlMs: options.ttlMs ?? 3 * 60 * 1000, + }, + ); + } + + on( + event: EventName, + listener: (payload: BerryOTPEventMap[EventName]) => void, + ): this { + return super.on(event, listener); + } + + once( + event: EventName, + listener: (payload: BerryOTPEventMap[EventName]) => void, + ): this { + return super.once(event, listener); + } + + async send(to: string, options: BerryOTPSendOptions = {}): Promise { + this.assertRecipient(to); + this.assertRateLimit(to); + + const id = randomUUID(); + const ttlMs = options.ttlMs ?? this.flowDefaults.ttlMs ?? this.options.ttlMs; + const code = options.code ?? this.generateCode(this.options.codeLength); + const expiresAt = Date.now() + ttlMs; + const salt = randomUUID(); + const codeHash = this.hashCode(code, salt); + const template = options.template ?? this.flowDefaults.template; + const issuer = this.flowDefaults.issuer ?? this.options.issuer; + const purpose = options.purpose ?? this.flowDefaults.purpose; + const footer = this.formatFooter(options.footer ?? this.options.footer, ttlMs); + const { text } = renderOTPTemplate({ + issuer, + code, + purpose, + ttlMs, + template, + }); + + const sentMessage = + this.options.mode === "stable" + ? await this.client.sendLegacyButtons(to, { + text, + footer, + buttons: [ + { + id: `berryotp:not_requested:${id}`, + title: "Nao pedi nenhum codigo", + }, + ], + }) + : await this.client.sendMessage(to, { + buttonsMessage: { + text, + footer, + buttons: [ + { + id: `berryotp:copy:${id}`, + title: "Copiar codigo", + kind: "copy_code", + code, + }, + { + id: `berryotp:not_requested:${id}`, + title: "Nao pedi nenhum codigo", + kind: "quick_reply", + }, + ], + }, + }); + + const record: BerryOTPRecord = { + id, + to, + codeHash, + salt, + messageId: sentMessage.id, + expiresAt, + status: "active", + attempts: 0, + metadata: options.metadata, + createdAt: Date.now(), + }; + + await this.options.store.create(record); + this.addRecipientRecord(to, id); + this.lastSentByRecipient.set(to, Date.now()); + this.scheduleExpiration(record); + + const result: BerryOTPSendResult = { + id, + to, + code, + expiresAt: new Date(expiresAt), + messageId: sentMessage.id, + status: "sent", + }; + + this.emit("sent", { + ...result, + metadata: options.metadata, + }); + + return result; + } + + async verify(to: string, code: string): Promise { + this.assertRecipient(to); + const activeRecords = await this.getRecipientRecords(to); + const latest = activeRecords[0]; + + if (!latest) { + return { valid: false, reason: "not_found" }; + } + + if (latest.status === "used") { + return { valid: false, reason: "already_used", metadata: latest.metadata }; + } + + if (latest.status === "denied") { + return { valid: false, reason: "denied", metadata: latest.metadata }; + } + + if (latest.status === "blocked" || latest.attempts >= this.options.maxAttempts) { + await this.options.store.update(latest.id, { status: "blocked" }); + return { valid: false, reason: "too_many_attempts", metadata: latest.metadata }; + } + + if (Date.now() > latest.expiresAt) { + await this.expireRecord(latest.id, latest); + return { valid: false, reason: "expired", metadata: latest.metadata }; + } + + for (const record of activeRecords.filter((candidate) => candidate.status === "active")) { + if (Date.now() > record.expiresAt) { + await this.expireRecord(record.id, record); + continue; + } + + const candidateHash = this.hashCode(code, record.salt); + if (this.safeCompare(candidateHash, record.codeHash)) { + const usedAt = Date.now(); + await this.options.store.update(record.id, { + status: "used", + usedAt, + }); + this.clearTimer(record.id); + this.emit("used", { + otpId: record.id, + to: record.to, + metadata: record.metadata, + }); + return { + valid: true, + metadata: record.metadata, + }; + } + } + + const attempts = latest.attempts + 1; + const nextStatus: BerryOTPStatus = + attempts >= this.options.maxAttempts ? "blocked" : latest.status; + + await this.options.store.update(latest.id, { + attempts, + status: nextStatus, + }); + + return { + valid: false, + reason: attempts >= this.options.maxAttempts ? "too_many_attempts" : "invalid_code", + metadata: latest.metadata, + }; + } + + async cancel(id: string, reason = "cancelled"): Promise { + const record = await this.options.store.get(id); + if (!record || record.status !== "active") { + return; + } + + await this.options.store.update(id, { + status: "denied", + deniedAt: Date.now(), + }); + this.clearTimer(id); + this.emit("denied", { + otpId: id, + to: record.to, + metadata: record.metadata, + reason, + }); + } + + async get(id: string): Promise { + const record = await this.options.store.get(id); + return record ? this.toPublicRecord(record) : null; + } + + async cleanup(): Promise { + const before = Date.now() - 10 * 60 * 1000; + await this.options.store.cleanup(before); + + for (const [to, ids] of this.recipientIndex.entries()) { + for (const id of [...ids]) { + const record = await this.options.store.get(id); + if (!record) { + ids.delete(id); + this.clearTimer(id); + } + } + + if (ids.size === 0) { + this.recipientIndex.delete(to); + } + } + } + + dispose(): void { + this.client.off("message.received", this.boundIncomingListener); + clearInterval(this.cleaner); + for (const timer of this.timers.values()) { + clearTimeout(timer); + } + this.timers.clear(); + } + + async sendLoginCode( + to: string, + options: Omit & { userId?: string } = {}, + ): Promise { + return this.send(to, { + ...options, + template: "login", + purpose: "entrar na conta", + metadata: { + userId: options.userId, + ...options.metadata, + }, + }); + } + + async verifyLoginCode(to: string, code: string): Promise { + return this.verify(to, code); + } + + async sendPasswordResetCode( + to: string, + options: Omit & { userId?: string } = {}, + ): Promise { + return this.send(to, { + ...options, + template: "password_reset", + purpose: "redefinir sua senha", + metadata: { + userId: options.userId, + ...options.metadata, + }, + }); + } + + async verifyPasswordResetCode(to: string, code: string): Promise { + return this.verify(to, code); + } + + async send2FACode( + to: string, + options: Omit & { userId?: string } = {}, + ): Promise { + return this.send(to, { + ...options, + template: "2fa", + purpose: "confirmar sua autenticacao em duas etapas", + metadata: { + userId: options.userId, + ...options.metadata, + }, + }); + } + + async verify2FACode(to: string, code: string): Promise { + return this.verify(to, code); + } + + private async handleIncomingMessage(message: BerryOTPIncomingMessage): Promise { + const buttonId = + message.buttonId ?? + message.selectedButtonId ?? + this.extractButtonIdFromParams(message.rawButtonParamsJson); + + if (!buttonId?.startsWith("berryotp:not_requested:")) { + return; + } + + const otpId = buttonId.replace("berryotp:not_requested:", ""); + const record = await this.options.store.get(otpId); + if (!record || record.status !== "active") { + return; + } + + await this.options.store.update(otpId, { + status: "denied", + deniedAt: Date.now(), + }); + this.clearTimer(otpId); + this.emit("denied", { + otpId, + to: record.to, + metadata: record.metadata, + reason: "not_requested", + }); + + if (this.options.autoReplyOnDenied) { + await this.client.sendText(record.to, DENIED_REPLY_TEXT); + } + } + + private extractButtonIdFromParams(paramsJson?: string): string | undefined { + if (!paramsJson) { + return undefined; + } + + try { + const parsed = JSON.parse(paramsJson) as Record; + const candidate = [parsed.id, parsed.button_id, parsed.selected_id].find( + (value) => typeof value === "string" && value.length > 0, + ); + return candidate as string | undefined; + } catch { + return undefined; + } + } + + private scheduleExpiration(record: BerryOTPRecord): void { + const delay = Math.max(0, record.expiresAt - Date.now()); + const timer = setTimeout(() => { + void this.expireById(record.id).catch((error) => this.emit("error", error as Error)); + }, delay); + timer.unref?.(); + this.timers.set(record.id, timer); + } + + private async expireById(id: string): Promise { + const record = await this.options.store.get(id); + if (!record) { + return; + } + + await this.expireRecord(id, record); + } + + private async expireRecord(id: string, record: BerryOTPRecord): Promise { + if (record.status !== "active" || Date.now() < record.expiresAt) { + return; + } + + await this.options.store.update(id, { + status: "expired", + expiredAt: Date.now(), + }); + this.clearTimer(id); + this.emit("expired", { + otpId: id, + to: record.to, + metadata: record.metadata, + }); + + if (this.options.editOnExpire) { + try { + await this.client.editMessage(record.to, record.messageId, EXPIRED_TEXT); + } catch (error) { + this.emit("warning", { + type: "edit_failed", + otpId: id, + error, + }); + } + } + } + + private async getRecipientRecords(to: string): Promise { + const ids = [...(this.recipientIndex.get(to) ?? new Set())]; + const records = await Promise.all(ids.map((id) => this.options.store.get(id))); + return records + .filter((record): record is BerryOTPRecord => !!record) + .sort((left, right) => right.createdAt - left.createdAt); + } + + private addRecipientRecord(to: string, id: string): void { + const current = this.recipientIndex.get(to) ?? new Set(); + current.add(id); + this.recipientIndex.set(to, current); + } + + private clearTimer(id: string): void { + const timer = this.timers.get(id); + if (timer) { + clearTimeout(timer); + this.timers.delete(id); + } + } + + private toPublicRecord(record: BerryOTPRecord): BerryOTPPublicRecord { + return { + id: record.id, + to: record.to, + messageId: record.messageId, + expiresAt: record.expiresAt, + status: record.status, + attempts: record.attempts, + metadata: record.metadata, + createdAt: record.createdAt, + usedAt: record.usedAt, + expiredAt: record.expiredAt, + deniedAt: record.deniedAt, + }; + } + + private generateCode(length: number): string { + let result = ""; + + for (let index = 0; index < length; index += 1) { + result += crypto.randomInt(0, 10).toString(); + } + + return result; + } + + private hashCode(code: string, salt: string): string { + return crypto.createHash("sha256").update(`${salt}:${code}`).digest("hex"); + } + + private safeCompare(left: string, right: string): boolean { + const leftBuffer = Buffer.from(left); + const rightBuffer = Buffer.from(right); + + if (leftBuffer.length !== rightBuffer.length) { + return false; + } + + return crypto.timingSafeEqual(leftBuffer, rightBuffer); + } + + private formatFooter(template: string, ttlMs: number): string { + const minutes = Math.max(1, Math.ceil(ttlMs / 60_000)); + return template + .replaceAll("{minutes}", String(minutes)) + .replaceAll("{seconds}", String(Math.ceil(ttlMs / 1_000))); + } + + private assertRecipient(to: string): void { + if (!to || typeof to !== "string") { + throw new Error("A valid WhatsApp destination is required."); + } + } + + private assertRateLimit(to: string): void { + const lastSentAt = this.lastSentByRecipient.get(to); + if (!lastSentAt) { + return; + } + + const elapsed = Date.now() - lastSentAt; + if (elapsed >= this.options.rateLimitMs) { + return; + } + + throw new Error( + `Please wait ${Math.ceil((this.options.rateLimitMs - elapsed) / 1000)} seconds before sending another OTP.`, + ); + } +} + +export default BerryOTP; +export { MemoryOTPStore, renderOTPTemplate }; +export type * from "./types.js"; diff --git a/packages/berry-otp/src/store.ts b/packages/berry-otp/src/store.ts new file mode 100644 index 0000000..1f5d89d --- /dev/null +++ b/packages/berry-otp/src/store.ts @@ -0,0 +1,62 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import { type BerryOTPRecord, type BerryOTPStore } from "./types.js"; + +export class MemoryOTPStore implements BerryOTPStore { + private readonly records = new Map(); + + async create(record: BerryOTPRecord): Promise { + this.records.set(record.id, { ...record }); + } + + async get(id: string): Promise { + const record = this.records.get(id); + return record ? { ...record } : null; + } + + async findActiveByCode(to: string, codeHash: string): Promise { + for (const record of this.records.values()) { + if (record.to !== to || record.status !== "active") { + continue; + } + + if (record.codeHash === codeHash) { + return { ...record }; + } + } + + return null; + } + + async update(id: string, patch: Partial): Promise { + const current = this.records.get(id); + if (!current) { + return; + } + + this.records.set(id, { + ...current, + ...patch, + }); + } + + async delete(id: string): Promise { + this.records.delete(id); + } + + async cleanup(before: number): Promise { + for (const [id, record] of this.records.entries()) { + const terminalAt = + record.expiredAt ?? record.usedAt ?? record.deniedAt ?? record.createdAt; + + if (record.status !== "active" && terminalAt < before) { + this.records.delete(id); + } + } + } +} diff --git a/packages/berry-otp/src/templates.ts b/packages/berry-otp/src/templates.ts new file mode 100644 index 0000000..5ebbc39 --- /dev/null +++ b/packages/berry-otp/src/templates.ts @@ -0,0 +1,67 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import { type RenderOTPTemplateInput, type RenderOTPTemplateResult } from "./types.js"; + +export const renderOTPTemplate = ({ + issuer, + code, + purpose, + template, +}: RenderOTPTemplateInput): RenderOTPTemplateResult => { + switch (template) { + case "login": + return { + text: [ + "Codigo de login", + "", + "Use o codigo abaixo para entrar na sua conta:", + "", + `*${code}*`, + "", + issuer, + ].join("\n"), + }; + case "password_reset": + return { + text: [ + "Redefinicao de senha", + "", + "Use o codigo abaixo para redefinir sua senha:", + "", + `*${code}*`, + "", + issuer, + ].join("\n"), + }; + case "2fa": + return { + text: [ + "Verificacao em duas etapas", + "", + "Use o codigo abaixo para continuar:", + "", + `*${code}*`, + "", + issuer, + ].join("\n"), + }; + case "generic": + default: + return { + text: [ + "Codigo de verificacao", + "", + `Use o codigo abaixo para ${purpose ?? "continuar"}:`, + "", + `*${code}*`, + "", + issuer, + ].join("\n"), + }; + } +}; diff --git a/packages/berry-otp/src/types.ts b/packages/berry-otp/src/types.ts new file mode 100644 index 0000000..2fb5e53 --- /dev/null +++ b/packages/berry-otp/src/types.ts @@ -0,0 +1,115 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +export type BerryOTPMode = "stable" | "copy-code" | "experimental-copy-code"; + +export type BerryOTPTemplate = "login" | "password_reset" | "2fa" | "generic"; + +export type BerryOTPStatus = "active" | "used" | "expired" | "denied" | "blocked"; + +export interface BerryOTPRecord { + id: string; + to: string; + codeHash: string; + salt: string; + messageId: string; + expiresAt: number; + status: BerryOTPStatus; + attempts: number; + metadata?: Record; + createdAt: number; + usedAt?: number; + expiredAt?: number; + deniedAt?: number; +} + +export interface BerryOTPPublicRecord { + id: string; + to: string; + messageId: string; + expiresAt: number; + status: BerryOTPStatus; + attempts: number; + metadata?: Record; + createdAt: number; + usedAt?: number; + expiredAt?: number; + deniedAt?: number; +} + +export interface BerryOTPStore { + create(record: BerryOTPRecord): Promise; + get(id: string): Promise; + findActiveByCode(to: string, codeHash: string): Promise; + update(id: string, patch: Partial): Promise; + delete(id: string): Promise; + cleanup(before: number): Promise; +} + +export type BerryOTPOptions = { + issuer?: string; + codeLength?: number; + ttlMs?: number; + footer?: string; + editOnExpire?: boolean; + mode?: BerryOTPMode; + maxAttempts?: number; + rateLimitMs?: number; + autoReplyOnDenied?: boolean; + store?: BerryOTPStore; +}; + +export interface BerryOTPSendOptions { + purpose?: string; + code?: string; + ttlMs?: number; + footer?: string; + metadata?: Record; + template?: BerryOTPTemplate; +} + +export interface BerryOTPSendResult { + id: string; + to: string; + code: string; + expiresAt: Date; + messageId: string; + status: "sent"; +} + +export interface BerryOTPVerifyResult { + valid: boolean; + reason?: + | "not_found" + | "expired" + | "already_used" + | "invalid_code" + | "too_many_attempts" + | "denied"; + metadata?: Record; +} + +export interface RenderOTPTemplateInput { + issuer: string; + code: string; + purpose?: string; + ttlMs: number; + template: BerryOTPTemplate; +} + +export interface RenderOTPTemplateResult { + text: string; +} + +export interface BerryOTPEventMap { + sent: BerryOTPSendResult & { metadata?: Record }; + used: { otpId: string; to: string; metadata?: Record }; + expired: { otpId: string; to: string; metadata?: Record }; + denied: { otpId: string; to: string; metadata?: Record; reason?: string }; + warning: { type: string; otpId?: string; error?: unknown; details?: string }; + error: Error; +} diff --git a/packages/berry-otp/tsconfig.json b/packages/berry-otp/tsconfig.json new file mode 100644 index 0000000..3dbeef4 --- /dev/null +++ b/packages/berry-otp/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ] +} diff --git a/packages/berryapi/package.json b/packages/berryapi/package.json new file mode 100644 index 0000000..48cf69a --- /dev/null +++ b/packages/berryapi/package.json @@ -0,0 +1,34 @@ +{ + "name": "berryapi", + "version": "0.1.0", + "private": true, + "description": "REST API platform for WhatsApp automation powered by BerryProtocol.", + "author": "BerrySDK", + "license": "Apache-2.0", + "type": "module", + "main": "dist/server.js", + "types": "dist/server.d.ts", + "files": [ + "dist" + ], + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc -b", + "start": "node dist/server.js" + }, + "dependencies": { + "@berrysdk/core": "^0.1.10", + "@fastify/cors": "^11.1.0", + "@fastify/swagger": "^9.5.1", + "@fastify/swagger-ui": "^5.2.3", + "@fastify/websocket": "^11.2.0", + "better-sqlite3": "^12.11.1", + "dotenv": "^16.6.1", + "drizzle-orm": "^0.44.2", + "fastify": "^5.6.1", + "fastify-plugin": "^5.1.0", + "pino": "^9.14.0", + "zod": "^4.1.8", + "zod-to-json-schema": "^3.24.6" + } +} diff --git a/packages/berryapi/src/app.ts b/packages/berryapi/src/app.ts new file mode 100644 index 0000000..0ca0486 --- /dev/null +++ b/packages/berryapi/src/app.ts @@ -0,0 +1,115 @@ +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import swagger from "@fastify/swagger"; +import swaggerUi from "@fastify/swagger-ui"; +import websocket from "@fastify/websocket"; +import { env } from "./config/env.js"; +import { initDatabase } from "./database/init.js"; +import { createRouteSchema } from "./docs/schema.js"; +import { InstanceManager } from "./managers/InstanceManager.js"; +import { authMiddleware } from "./middlewares/auth.js"; +import { errorHandler } from "./middlewares/errorHandler.js"; +import { RealtimeGateway } from "./realtime/RealtimeGateway.js"; +import { logger } from "./utils/logger.js"; +import { successResponse } from "./utils/response.js"; +import { registerInstanceRoutes } from "./modules/instance/instance.routes.js"; +import { registerWebhookRoutes } from "./modules/webhook/webhook.routes.js"; +import { registerSettingsRoutes } from "./modules/settings/settings.routes.js"; +import { registerMessageRoutes } from "./modules/message/routes/message.routes.js"; +import { registerChatRoutes } from "./modules/chat/chat.routes.js"; +import { registerProfileRoutes } from "./modules/profile/profile.routes.js"; +import { registerGroupRoutes } from "./modules/group/group.routes.js"; +import { registerFlowRoutes } from "./modules/flow/flow.routes.js"; +import { registerStudioRoutes } from "./modules/studio/studio.routes.js"; +import { WebhookDispatcher } from "./webhook/WebhookDispatcher.js"; + +export const buildApp = async () => { + await initDatabase(); + + const app = Fastify({ + logger: { + level: env.LOG_LEVEL, + }, + bodyLimit: 50 * 1024 * 1024, + }); + + const realtime = new RealtimeGateway(); + const webhooks = new WebhookDispatcher(); + const manager = new InstanceManager(realtime, webhooks); + await manager.bootstrap(); + + await app.register(cors, { + origin: true, + }); + await app.register(websocket); + await app.register(swagger, { + openapi: { + info: { + title: "BerryAPI", + description: "REST API for WhatsApp Web automation powered by BerryProtocol.", + version: "0.1.0", + }, + security: [{ bearerAuth: [] }], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + }, + }, + }, + }, + }); + await app.register(swaggerUi, { + routePrefix: "/docs", + uiConfig: { + docExpansion: "list", + deepLinking: false, + }, + }); + + realtime.register(app); + + app.setErrorHandler(errorHandler); + app.addHook("onRequest", authMiddleware); + + app.get("/", { + schema: createRouteSchema({ + tags: ["System"], + summary: "BerryAPI health entrypoint", + description: "Quick health response for BerryAPI.", + }), + }, async () => successResponse("BerryAPI is running.", { + name: "BerryAPI", + provider: "BerryProtocol", + docs: "/docs", + websocket: env.BERRY_WS_PATH, + })); + + app.get("/info", { + schema: createRouteSchema({ + tags: ["System"], + summary: "BerryAPI information", + }), + }, async () => successResponse("BerryAPI metadata loaded.", { + name: "BerryAPI", + version: "0.1.0", + environment: env.NODE_ENV, + websocketPath: env.BERRY_WS_PATH, + database: env.DATABASE_URL, + authFolder: env.BERRY_AUTH_FOLDER, + provider: "BerryProtocol", + })); + + await registerInstanceRoutes(app, manager); + await registerWebhookRoutes(app, manager); + await registerSettingsRoutes(app, manager); + await registerMessageRoutes(app, manager); + await registerChatRoutes(app, manager); + await registerProfileRoutes(app, manager); + await registerGroupRoutes(app, manager); + await registerFlowRoutes(app, manager); + await registerStudioRoutes(app); + + return app; +}; diff --git a/packages/berryapi/src/config/env.ts b/packages/berryapi/src/config/env.ts new file mode 100644 index 0000000..0611c2f --- /dev/null +++ b/packages/berryapi/src/config/env.ts @@ -0,0 +1,20 @@ +import { config as loadEnv } from "dotenv"; +import { z } from "zod"; + +loadEnv(); + +const envSchema = z.object({ + NODE_ENV: z.enum(["development", "test", "production"]).default("development"), + PORT: z.coerce.number().default(3000), + HOST: z.string().default("0.0.0.0"), + API_KEY: z.string().default("berryapi_dev_key"), + LOG_LEVEL: z.string().default("info"), + DATABASE_URL: z.string().default("file:./berryapi.sqlite"), + BERRY_SQLITE_PATH: z.string().default("berrysdk.db"), + BERRY_AUTH_FOLDER: z.string().default(".berry-sessions"), + BERRY_WEBHOOK_TIMEOUT_MS: z.coerce.number().default(5000), + BERRY_WS_PATH: z.string().default("/ws"), +}); + +export const env = envSchema.parse(process.env); +export type Env = typeof env; diff --git a/packages/berryapi/src/database/client.ts b/packages/berryapi/src/database/client.ts new file mode 100644 index 0000000..20ae481 --- /dev/null +++ b/packages/berryapi/src/database/client.ts @@ -0,0 +1,21 @@ +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import { dirname, isAbsolute, resolve } from "node:path"; +import { mkdirSync } from "node:fs"; +import { env } from "../config/env.js"; +import * as schema from "./schema.js"; + +const normalizeSqlitePath = (value: string): string => { + if (value.startsWith("file:")) { + const raw = value.slice(5); + return isAbsolute(raw) ? raw : resolve(process.cwd(), raw); + } + + return isAbsolute(value) ? value : resolve(process.cwd(), value); +}; + +const dbPath = normalizeSqlitePath(env.DATABASE_URL); +mkdirSync(dirname(dbPath), { recursive: true }); + +export const sqlite: any = new Database(dbPath); +export const db = drizzle(sqlite, { schema }); diff --git a/packages/berryapi/src/database/init.ts b/packages/berryapi/src/database/init.ts new file mode 100644 index 0000000..1192249 --- /dev/null +++ b/packages/berryapi/src/database/init.ts @@ -0,0 +1,61 @@ +import { sqlite } from "./client.js"; + +export const initDatabase = async (): Promise => { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS instances ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + provider TEXT NOT NULL DEFAULT 'berryprotocol', + status TEXT NOT NULL DEFAULT 'created', + connection_state TEXT NOT NULL DEFAULT 'disconnected', + auth_method TEXT NOT NULL DEFAULT 'qr', + phone_number TEXT, + qr_code TEXT, + pairing_code TEXT, + webhook_url TEXT, + webhook_events TEXT NOT NULL DEFAULT '[]', + settings_json TEXT NOT NULL DEFAULT '{}', + metadata_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + `); + + sqlite.exec(` + CREATE TABLE IF NOT EXISTS flows ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'draft', + draft_version_id TEXT, + published_version_id TEXT, + canvas_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + `); + + sqlite.exec(` + CREATE TABLE IF NOT EXISTS flow_versions ( + id TEXT PRIMARY KEY, + flow_id TEXT NOT NULL, + version_number INTEGER NOT NULL, + graph_json TEXT NOT NULL, + snapshot_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL + ); + `); + + sqlite.exec(` + CREATE TABLE IF NOT EXISTS flow_runs ( + id TEXT PRIMARY KEY, + flow_id TEXT NOT NULL, + flow_version_id TEXT, + status TEXT NOT NULL DEFAULT 'idle', + current_node_id TEXT, + variables_json TEXT NOT NULL DEFAULT '{}', + history_json TEXT NOT NULL DEFAULT '[]', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + `); +}; diff --git a/packages/berryapi/src/database/schema.ts b/packages/berryapi/src/database/schema.ts new file mode 100644 index 0000000..bfb8fdb --- /dev/null +++ b/packages/berryapi/src/database/schema.ts @@ -0,0 +1,57 @@ +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const instancesTable = sqliteTable("instances", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull().unique(), + provider: text("provider").notNull().default("berryprotocol"), + status: text("status").notNull().default("created"), + connectionState: text("connection_state").notNull().default("disconnected"), + authMethod: text("auth_method").notNull().default("qr"), + phoneNumber: text("phone_number"), + qrCode: text("qr_code"), + pairingCode: text("pairing_code"), + webhookUrl: text("webhook_url"), + webhookEvents: text("webhook_events").notNull().default("[]"), + settingsJson: text("settings_json").notNull().default("{}"), + metadataJson: text("metadata_json").notNull().default("{}"), + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull(), +}); + +export const flowsTable = sqliteTable("flows", { + id: text("id").primaryKey(), + name: text("name").notNull(), + status: text("status").notNull().default("draft"), + draftVersionId: text("draft_version_id"), + publishedVersionId: text("published_version_id"), + canvasJson: text("canvas_json").notNull().default("{}"), + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull(), +}); + +export const flowVersionsTable = sqliteTable("flow_versions", { + id: text("id").primaryKey(), + flowId: text("flow_id").notNull(), + versionNumber: integer("version_number").notNull(), + graphJson: text("graph_json").notNull(), + snapshotJson: text("snapshot_json").notNull().default("{}"), + createdAt: text("created_at").notNull(), +}); + +export const flowRunsTable = sqliteTable("flow_runs", { + id: text("id").primaryKey(), + flowId: text("flow_id").notNull(), + flowVersionId: text("flow_version_id"), + status: text("status").notNull().default("idle"), + currentNodeId: text("current_node_id"), + variablesJson: text("variables_json").notNull().default("{}"), + historyJson: text("history_json").notNull().default("[]"), + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull(), +}); + +export type InstanceRow = typeof instancesTable.$inferSelect; +export type NewInstanceRow = typeof instancesTable.$inferInsert; +export type FlowRow = typeof flowsTable.$inferSelect; +export type FlowVersionRow = typeof flowVersionsTable.$inferSelect; +export type FlowRunRow = typeof flowRunsTable.$inferSelect; diff --git a/packages/berryapi/src/docs/schema.ts b/packages/berryapi/src/docs/schema.ts new file mode 100644 index 0000000..8f7b844 --- /dev/null +++ b/packages/berryapi/src/docs/schema.ts @@ -0,0 +1,53 @@ +import type { ZodTypeAny } from "zod"; +import { toOpenApiSchema } from "../utils/zod.js"; + +export const envelopeSchema = { + type: "object", + properties: { + success: { type: "boolean" }, + message: { type: "string" }, + data: {}, + }, +}; + +export const errorEnvelopeSchema = { + type: "object", + properties: { + success: { type: "boolean", enum: [false] }, + message: { type: "string" }, + error: {}, + }, +}; + +export const createRouteSchema = ({ + tags, + summary, + description, + params, + body, + querystring, + example, +}: { + tags: string[]; + summary: string; + description?: string; + params?: ZodTypeAny; + body?: ZodTypeAny; + querystring?: ZodTypeAny; + example?: unknown; +}) => ({ + tags, + summary, + description, + ...(params ? { params: toOpenApiSchema(params, `${summary}Params`) } : {}), + ...(body ? { body: { ...toOpenApiSchema(body, `${summary}Body`), ...(example ? { examples: [example] } : {}) } } : {}), + ...(querystring ? { querystring: toOpenApiSchema(querystring, `${summary}Query`) } : {}), + response: { + 200: envelopeSchema, + 201: envelopeSchema, + 400: errorEnvelopeSchema, + 401: errorEnvelopeSchema, + 404: errorEnvelopeSchema, + 500: errorEnvelopeSchema, + }, +}); diff --git a/packages/berryapi/src/managers/InstanceManager.ts b/packages/berryapi/src/managers/InstanceManager.ts new file mode 100644 index 0000000..ae414f3 --- /dev/null +++ b/packages/berryapi/src/managers/InstanceManager.ts @@ -0,0 +1,390 @@ +import { and, eq } from "drizzle-orm"; +import { db } from "../database/client.js"; +import { instancesTable, type InstanceRow } from "../database/schema.js"; +import type { BerryApiEventName, InstanceRecord, InstanceSettings, WebhookConfig } from "../types/index.js"; +import { HttpError } from "../utils/http-error.js"; +import { BerryProtocolProvider } from "../providers/whatsapp/BerryProtocolProvider.js"; +import type { ProviderEventPayload, ProviderSendOptions, WhatsAppProvider } from "../providers/whatsapp/WhatsAppProvider.js"; +import { WebhookDispatcher } from "../webhook/WebhookDispatcher.js"; +import { RealtimeGateway } from "../realtime/RealtimeGateway.js"; +import { logger } from "../utils/logger.js"; + +type CreateInstanceInput = { + instanceName: string; + authMethod?: InstanceRecord["authMethod"]; + phoneNumber?: string; + webhook?: Partial; + settings?: Partial; +}; + +type UpdateInstanceStateInput = Partial>; + +const nowIso = () => new Date().toISOString(); + +const parseJson = (value: string | null | undefined, fallback: T): T => { + if (!value) return fallback; + try { + return JSON.parse(value) as T; + } catch { + return fallback; + } +}; + +const rowToInstance = (row: InstanceRow): InstanceRecord => ({ + instanceName: row.name, + provider: "berryprotocol", + status: row.status as InstanceRecord["status"], + connectionState: row.connectionState, + authMethod: row.authMethod as InstanceRecord["authMethod"], + phoneNumber: row.phoneNumber ?? undefined, + qrCode: row.qrCode ?? undefined, + pairingCode: row.pairingCode ?? undefined, + webhook: { + enabled: !!row.webhookUrl, + url: row.webhookUrl ?? undefined, + events: parseJson(row.webhookEvents, []), + }, + settings: parseJson(row.settingsJson, {}), + createdAt: row.createdAt, + updatedAt: row.updatedAt, +}); + +export class InstanceManager { + private readonly providers = new Map(); + private readonly eventListeners = new Set< + (event: ProviderEventPayload) => void | Promise + >(); + + constructor( + private readonly realtime: RealtimeGateway, + private readonly webhooks: WebhookDispatcher, + ) {} + + async bootstrap(): Promise { + const rows = await db.select().from(instancesTable); + for (const row of rows) { + this.ensureProvider(rowToInstance(row)); + } + } + + async createInstance(input: CreateInstanceInput): Promise { + const instanceName = input.instanceName.trim(); + if (!instanceName) { + throw new HttpError(400, "instanceName is required."); + } + + const existing = await this.findInstance(instanceName); + if (existing) { + throw new HttpError(409, "Instance already exists."); + } + + const webhookEvents = input.webhook?.events ?? []; + const settings = input.settings ?? {}; + const createdAt = nowIso(); + + await db.insert(instancesTable).values({ + name: instanceName, + provider: "berryprotocol", + status: "created", + connectionState: "disconnected", + authMethod: input.authMethod ?? "qr", + phoneNumber: input.phoneNumber, + webhookUrl: input.webhook?.url, + webhookEvents: JSON.stringify(webhookEvents), + settingsJson: JSON.stringify(settings), + metadataJson: JSON.stringify({}), + createdAt, + updatedAt: createdAt, + }); + + const instance = await this.getInstance(instanceName); + this.ensureProvider(instance); + return instance; + } + + async fetchInstances(): Promise { + const rows = await db.select().from(instancesTable); + return rows.map(rowToInstance); + } + + async getInstance(instanceName: string): Promise { + const found = await this.findInstance(instanceName); + if (!found) { + throw new HttpError(404, "Instance not found."); + } + + return found; + } + + async connectInstance(instanceName: string): Promise { + const instance = await this.getInstance(instanceName); + const provider = this.ensureProvider(instance); + await this.updateInstanceState(instanceName, { + status: "connecting", + connectionState: "connecting", + }); + void provider.connect({ + authMethod: instance.authMethod, + phoneNumber: instance.phoneNumber, + }).catch((error) => { + logger.error({ err: error, instanceName }, "failed to connect instance"); + }); + return this.getInstance(instanceName); + } + + async restartInstance(instanceName: string): Promise { + const instance = await this.getInstance(instanceName); + const provider = this.ensureProvider(instance); + await this.updateInstanceState(instanceName, { + status: "restarting", + connectionState: "reconnecting", + }); + void provider.reconnect().catch((error) => { + logger.error({ err: error, instanceName }, "failed to restart instance"); + }); + return this.getInstance(instanceName); + } + + async getConnectionState(instanceName: string): Promise> { + const instance = await this.getInstance(instanceName); + const provider = this.ensureProvider(instance); + const runtimeState = await provider.getConnectionState(); + return { + instanceName, + ...runtimeState, + }; + } + + async logoutInstance(instanceName: string): Promise { + const provider = this.providers.get(instanceName); + if (provider) { + await provider.logout(); + } + await this.updateInstanceState(instanceName, { + status: "logged_out", + connectionState: "logged_out", + qrCode: undefined, + pairingCode: undefined, + }); + return this.getInstance(instanceName); + } + + async deleteInstance(instanceName: string): Promise { + const provider = this.providers.get(instanceName); + if (provider) { + await provider.logout().catch(() => undefined); + this.providers.delete(instanceName); + } + + await db.delete(instancesTable).where(eq(instancesTable.name, instanceName)); + } + + async setPresence( + instanceName: string, + presence: "available" | "composing" | "recording" | "paused" | "unavailable", + jid?: string, + ): Promise { + const provider = this.ensureProvider(await this.getInstance(instanceName)); + await provider.setPresence(presence, jid); + } + + async sendRawMessage( + instanceName: string, + to: string, + content: Record, + options?: ProviderSendOptions, + ): Promise { + const provider = this.ensureProvider(await this.getInstance(instanceName)); + return provider.sendRaw(to, content, options); + } + + async sendText(instanceName: string, to: string, text: string): Promise { + return this.ensureProvider(await this.getInstance(instanceName)).sendText(to, text); + } + + async sendImage(instanceName: string, to: string, payload: Record): Promise { + return this.ensureProvider(await this.getInstance(instanceName)).sendImage(to, payload); + } + + async sendAudio(instanceName: string, to: string, payload: Record): Promise { + return this.ensureProvider(await this.getInstance(instanceName)).sendAudio(to, payload); + } + + async sendDocument(instanceName: string, to: string, payload: Record): Promise { + return this.ensureProvider(await this.getInstance(instanceName)).sendDocument(to, payload); + } + + async sendButtons(instanceName: string, to: string, payload: Record): Promise { + return this.ensureProvider(await this.getInstance(instanceName)).sendButtons(to, payload); + } + + async sendList(instanceName: string, to: string, payload: Record): Promise { + return this.ensureProvider(await this.getInstance(instanceName)).sendList(to, payload); + } + + async sendCarousel(instanceName: string, to: string, payload: Record): Promise { + return this.ensureProvider(await this.getInstance(instanceName)).sendCarousel(to, payload); + } + + async sendReaction(instanceName: string, to: string, emoji: string, targetMessageId: string): Promise { + return this.ensureProvider(await this.getInstance(instanceName)).sendReaction(to, emoji, targetMessageId); + } + + async sendLocation(instanceName: string, to: string, payload: Record): Promise { + return this.ensureProvider(await this.getInstance(instanceName)).sendLocation(to, payload); + } + + async sendContact(instanceName: string, to: string, payload: Record): Promise { + return this.ensureProvider(await this.getInstance(instanceName)).sendContact(to, payload); + } + + async editMessage(instanceName: string, to: string, messageId: string, text: string): Promise { + return this.ensureProvider(await this.getInstance(instanceName)).editMessage(to, messageId, text); + } + + async fetchGroups(instanceName: string): Promise { + const provider = this.ensureProvider(await this.getInstance(instanceName)); + return provider.fetchGroups(); + } + + onProviderEvent( + listener: (event: ProviderEventPayload) => void | Promise, + ): () => void { + this.eventListeners.add(listener); + return () => { + this.eventListeners.delete(listener); + }; + } + + async setWebhook(instanceName: string, input: Partial): Promise { + const instance = await this.getInstance(instanceName); + const webhook: WebhookConfig = { + enabled: input.enabled ?? true, + url: input.url ?? instance.webhook.url, + events: input.events ?? instance.webhook.events, + headers: input.headers ?? instance.webhook.headers, + }; + + await db + .update(instancesTable) + .set({ + webhookUrl: webhook.url ?? null, + webhookEvents: JSON.stringify(webhook.events), + updatedAt: nowIso(), + }) + .where(eq(instancesTable.name, instanceName)); + + return this.getInstance(instanceName); + } + + async getWebhook(instanceName: string): Promise { + const instance = await this.getInstance(instanceName); + return instance.webhook; + } + + async setSettings(instanceName: string, input: Partial): Promise { + const instance = await this.getInstance(instanceName); + const settings = { + ...instance.settings, + ...input, + }; + + await db + .update(instancesTable) + .set({ + settingsJson: JSON.stringify(settings), + updatedAt: nowIso(), + }) + .where(eq(instancesTable.name, instanceName)); + + return this.getInstance(instanceName); + } + + async getSettings(instanceName: string): Promise { + const instance = await this.getInstance(instanceName); + return instance.settings; + } + + private async findInstance(instanceName: string): Promise { + const row = await db.query.instancesTable.findFirst({ + where: eq(instancesTable.name, instanceName), + }); + return row ? rowToInstance(row) : null; + } + + private ensureProvider(instance: InstanceRecord): WhatsAppProvider { + const existing = this.providers.get(instance.instanceName); + if (existing) { + return existing; + } + + const provider = new BerryProtocolProvider(instance); + provider.onEvent((event) => { + void this.handleProviderEvent(instance.instanceName, event); + }); + this.providers.set(instance.instanceName, provider); + return provider; + } + + private async handleProviderEvent( + instanceName: string, + event: ProviderEventPayload, + ): Promise { + if (event.event === "connection.update") { + const payload = event.payload as Record; + await this.updateInstanceState(instanceName, { + status: + payload.connectedAt + ? "connected" + : payload.reason === "logged_out" + ? "logged_out" + : payload.reason === "restart_required" + ? "restarting" + : "disconnected", + connectionState: String(payload.reason ?? (payload.connectedAt ? "open" : "disconnected")), + }); + } + + if (event.event === "qrcode.updated") { + const payload = event.payload as Record; + await this.updateInstanceState(instanceName, { + status: "qr_pending", + connectionState: payload.code ? "pairing_code" : "qr", + qrCode: typeof payload.value === "string" ? payload.value : undefined, + pairingCode: typeof payload.code === "string" ? payload.code : undefined, + }); + } + + this.realtime.broadcast(event); + for (const listener of this.eventListeners) { + void Promise.resolve(listener(event)).catch((error) => { + logger.error( + { err: error, instanceName, event: event.event }, + "provider event listener failed", + ); + }); + } + + const instance = await this.getInstance(instanceName).catch(() => null); + if (instance) { + await this.webhooks.dispatch(instance.webhook, event); + } + } + + private async updateInstanceState( + instanceName: string, + update: UpdateInstanceStateInput, + ): Promise { + const current = await this.getInstance(instanceName); + await db + .update(instancesTable) + .set({ + status: update.status ?? current.status, + connectionState: update.connectionState ?? current.connectionState, + qrCode: update.qrCode === undefined ? current.qrCode ?? null : update.qrCode, + pairingCode: update.pairingCode === undefined ? current.pairingCode ?? null : update.pairingCode, + updatedAt: nowIso(), + }) + .where(eq(instancesTable.name, instanceName)); + } +} diff --git a/packages/berryapi/src/middlewares/auth.ts b/packages/berryapi/src/middlewares/auth.ts new file mode 100644 index 0000000..c74579c --- /dev/null +++ b/packages/berryapi/src/middlewares/auth.ts @@ -0,0 +1,31 @@ +import type { FastifyReply, FastifyRequest } from "fastify"; +import { env } from "../config/env.js"; +import { HttpError } from "../utils/http-error.js"; + +const publicPrefixes = ["/info", "/docs", "/documentation", "/docs/json", env.BERRY_WS_PATH]; + +export const authMiddleware = async (request: FastifyRequest, _reply: FastifyReply) => { + const requestPath = request.url.split("?", 1)[0]; + const studioPageOrAsset = + (requestPath === "/studio" || requestPath.startsWith("/studio/")) + && !requestPath.startsWith("/studio/api/"); + + if ( + requestPath === "/" + || requestPath === "/favicon.ico" + || studioPageOrAsset + || publicPrefixes.some((prefix) => requestPath === prefix || requestPath.startsWith(`${prefix}/`)) + ) { + return; + } + + const header = request.headers.authorization; + if (!header?.startsWith("Bearer ")) { + throw new HttpError(401, "Missing bearer token."); + } + + const token = header.slice("Bearer ".length).trim(); + if (token !== env.API_KEY) { + throw new HttpError(401, "Invalid API key."); + } +}; diff --git a/packages/berryapi/src/middlewares/errorHandler.ts b/packages/berryapi/src/middlewares/errorHandler.ts new file mode 100644 index 0000000..66ff97f --- /dev/null +++ b/packages/berryapi/src/middlewares/errorHandler.ts @@ -0,0 +1,16 @@ +import type { FastifyError, FastifyReply, FastifyRequest } from "fastify"; +import { HttpError } from "../utils/http-error.js"; +import { errorResponse } from "../utils/response.js"; +import { logger } from "../utils/logger.js"; + +export const errorHandler = ( + error: FastifyError | HttpError, + _request: FastifyRequest, + reply: FastifyReply, +) => { + const statusCode = error instanceof HttpError ? error.statusCode : 500; + const details = error instanceof HttpError ? error.details : { name: error.name }; + + logger.error({ err: error, statusCode }, "request failed"); + reply.status(statusCode).send(errorResponse(error.message, details)); +}; diff --git a/packages/berryapi/src/modules/chat/chat.controller.ts b/packages/berryapi/src/modules/chat/chat.controller.ts new file mode 100644 index 0000000..d0a876b --- /dev/null +++ b/packages/berryapi/src/modules/chat/chat.controller.ts @@ -0,0 +1,78 @@ +import type { FastifyReply, FastifyRequest } from "fastify"; +import type { ZodTypeAny } from "zod"; +import { successResponse } from "../../utils/response.js"; +import { validateOrThrow } from "../../utils/zod.js"; +import { + base64BodySchema, + blockStatusBodySchema, + findContactsBodySchema, + instanceNameParamsSchema, + jidBodySchema, + messageKeyBodySchema, + presenceBodySchema, + updateMessageBodySchema, +} from "./chat.validators.js"; +import type { ChatService } from "./chat.service.js"; + +export class ChatController { + constructor(private readonly service: ChatService) {} + + private getInstanceName(request: FastifyRequest): string { + return validateOrThrow(instanceNameParamsSchema, request.params).instanceName; + } + + private async run( + request: FastifyRequest, + reply: FastifyReply, + schema: ZodTypeAny, + message: string, + executor: (instanceName: string, input: any) => Promise, + ) { + const instanceName = this.getInstanceName(request); + const body = validateOrThrow(schema, request.body) as Record; + const data = await executor(instanceName, body); + reply.send(successResponse(message, data)); + } + + checkWhatsApp = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, jidBodySchema, "WhatsApp availability checked successfully.", this.service.checkWhatsApp.bind(this.service)); + + markRead = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, jidBodySchema, "Chat marked as read successfully.", this.service.markRead.bind(this.service)); + + markUnread = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, jidBodySchema, "Chat marked as unread successfully.", this.service.markUnread.bind(this.service)); + + archive = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, jidBodySchema, "Chat archived successfully.", this.service.archive.bind(this.service)); + + deleteMessageForEveryone = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, messageKeyBodySchema, "Delete for everyone dispatched successfully.", this.service.deleteMessageForEveryone.bind(this.service)); + + updateMessage = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, updateMessageBodySchema, "Message updated successfully.", this.service.updateMessage.bind(this.service)); + + sendPresence = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, presenceBodySchema, "Presence updated successfully.", this.service.sendPresence.bind(this.service)); + + updateBlockStatus = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, blockStatusBodySchema, "Block status updated successfully.", this.service.updateBlockStatus.bind(this.service)); + + fetchProfilePictureUrl = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, jidBodySchema, "Profile picture URL fetched successfully.", this.service.fetchProfilePictureUrl.bind(this.service)); + + getBase64 = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, base64BodySchema, "Base64 generated successfully.", this.service.getBase64.bind(this.service)); + + findContacts = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, findContactsBodySchema, "Contacts fetched successfully.", this.service.findContacts.bind(this.service)); + + findMessages = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, findContactsBodySchema, "Messages fetched successfully.", this.service.findMessages.bind(this.service)); + + findStatusMessage = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, findContactsBodySchema, "Status messages fetched successfully.", this.service.findStatusMessage.bind(this.service)); + + findChats = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, findContactsBodySchema, "Chats fetched successfully.", this.service.findChats.bind(this.service)); +} diff --git a/packages/berryapi/src/modules/chat/chat.routes.ts b/packages/berryapi/src/modules/chat/chat.routes.ts new file mode 100644 index 0000000..b556d53 --- /dev/null +++ b/packages/berryapi/src/modules/chat/chat.routes.ts @@ -0,0 +1,55 @@ +import type { FastifyInstance } from "fastify"; +import { createRouteSchema } from "../../docs/schema.js"; +import type { InstanceManager } from "../../managers/InstanceManager.js"; +import { ChatController } from "./chat.controller.js"; +import { ChatService } from "./chat.service.js"; +import { + base64BodySchema, + blockStatusBodySchema, + findContactsBodySchema, + instanceNameParamsSchema, + jidBodySchema, + messageKeyBodySchema, + presenceBodySchema, + updateMessageBodySchema, +} from "./chat.validators.js"; + +export const registerChatRoutes = async (app: any, manager: InstanceManager) => { + const controller = new ChatController(new ChatService(manager)); + + const post = ( + path: string, + summary: string, + body: object, + handler: any, + ) => app.post(path, { + schema: createRouteSchema({ + tags: ["Chat"], + summary, + params: instanceNameParamsSchema, + body: body as never, + }), + }, handler); + + post("/chat/checkWhatsApp/:instanceName", "Check if JID looks like a WhatsApp identifier", jidBodySchema, controller.checkWhatsApp); + post("/chat/markRead/:instanceName", "Mark chat as read", jidBodySchema, controller.markRead); + post("/chat/markUnread/:instanceName", "Mark chat as unread", jidBodySchema, controller.markUnread); + post("/chat/archive/:instanceName", "Archive chat", jidBodySchema, controller.archive); + app.delete("/chat/deleteMessageForEveryone/:instanceName", { + schema: createRouteSchema({ + tags: ["Chat"], + summary: "Delete message for everyone", + params: instanceNameParamsSchema, + body: messageKeyBodySchema, + }), + }, controller.deleteMessageForEveryone); + post("/chat/updateMessage/:instanceName", "Update chat message", updateMessageBodySchema, controller.updateMessage); + post("/chat/sendPresence/:instanceName", "Send chat presence", presenceBodySchema, controller.sendPresence); + post("/chat/updateBlockStatus/:instanceName", "Update block status", blockStatusBodySchema, controller.updateBlockStatus); + post("/chat/fetchProfilePictureUrl/:instanceName", "Fetch profile picture URL", jidBodySchema, controller.fetchProfilePictureUrl); + post("/chat/getBase64/:instanceName", "Convert file or URL to base64", base64BodySchema, controller.getBase64); + post("/chat/findContacts/:instanceName", "Find contacts", findContactsBodySchema, controller.findContacts); + post("/chat/findMessages/:instanceName", "Find messages", findContactsBodySchema, controller.findMessages); + post("/chat/findStatusMessage/:instanceName", "Find status messages", findContactsBodySchema, controller.findStatusMessage); + post("/chat/findChats/:instanceName", "Find chats", findContactsBodySchema, controller.findChats); +}; diff --git a/packages/berryapi/src/modules/chat/chat.service.ts b/packages/berryapi/src/modules/chat/chat.service.ts new file mode 100644 index 0000000..c5231e1 --- /dev/null +++ b/packages/berryapi/src/modules/chat/chat.service.ts @@ -0,0 +1,89 @@ +import { readFile } from "node:fs/promises"; +import { InstanceManager } from "../../managers/InstanceManager.js"; +import { notImplementedYet } from "../shared/todo.js"; + +const whatsappRegex = /^\d+@(?:s\.whatsapp\.net|lid|g\.us|newsletter)$/; + +export class ChatService { + constructor(private readonly manager: InstanceManager) {} + + async checkWhatsApp(_instanceName: string, input: { jid: string }) { + return { + jid: input.jid, + exists: whatsappRegex.test(input.jid), + }; + } + + async markRead(_instanceName: string, _input: { jid: string }) { + notImplementedYet("markRead is not exposed by BerryProtocol yet."); + } + + async markUnread(_instanceName: string, _input: { jid: string }) { + notImplementedYet("markUnread is not exposed by BerryProtocol yet."); + } + + async archive(_instanceName: string, _input: { jid: string }) { + notImplementedYet("archive chat is not exposed by BerryProtocol yet."); + } + + async deleteMessageForEveryone(instanceName: string, input: { remoteJid: string; messageId: string; fromMe: boolean; participant?: string }) { + return this.manager.sendRawMessage(instanceName, input.remoteJid, { + delete: { + remoteJid: input.remoteJid, + id: input.messageId, + fromMe: input.fromMe, + participant: input.participant, + }, + }); + } + + async updateMessage(instanceName: string, input: { jid: string; messageId: string; text: string }) { + return this.manager.editMessage(instanceName, input.jid, input.messageId, input.text); + } + + async sendPresence(instanceName: string, input: { presence: "available" | "composing" | "recording" | "paused" | "unavailable"; jid?: string }) { + await this.manager.setPresence(instanceName, input.presence, input.jid); + return { jid: input.jid, presence: input.presence }; + } + + async updateBlockStatus(_instanceName: string, _input: { jid: string; action: "block" | "unblock" }) { + notImplementedYet("block and unblock operations are not exposed by BerryProtocol yet."); + } + + async fetchProfilePictureUrl(_instanceName: string, _input: { jid: string }) { + notImplementedYet("fetchProfilePictureUrl is not exposed by BerryProtocol yet."); + } + + async getBase64(_instanceName: string, input: { url?: string; path?: string }) { + if (input.url) { + const response = await fetch(input.url); + const arrayBuffer = await response.arrayBuffer(); + return { + source: input.url, + base64: Buffer.from(arrayBuffer).toString("base64"), + }; + } + + const file = await readFile(String(input.path)); + return { + source: input.path, + base64: file.toString("base64"), + }; + } + + async findContacts(_instanceName: string, _input: { query?: string }) { + notImplementedYet("contact search is not exposed by BerryProtocol store yet."); + } + + async findMessages(_instanceName: string, _input: { query?: string }) { + notImplementedYet("message search is not exposed by BerryProtocol store yet."); + } + + async findStatusMessage(_instanceName: string, _input: { query?: string }) { + notImplementedYet("status search is not exposed by BerryProtocol store yet."); + } + + async findChats(_instanceName: string, _input: { query?: string }) { + notImplementedYet("chat search is not exposed by BerryProtocol store yet."); + } +} diff --git a/packages/berryapi/src/modules/chat/chat.validators.ts b/packages/berryapi/src/modules/chat/chat.validators.ts new file mode 100644 index 0000000..8b10882 --- /dev/null +++ b/packages/berryapi/src/modules/chat/chat.validators.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; +import { instanceNameParamsSchema } from "../message/validators/message.validators.js"; + +export { instanceNameParamsSchema }; + +export const jidBodySchema = z.object({ + jid: z.string().min(1), +}); + +export const messageKeyBodySchema = z.object({ + remoteJid: z.string().min(1), + messageId: z.string().min(1), + fromMe: z.boolean().default(true), + participant: z.string().optional(), +}); + +export const updateMessageBodySchema = z.object({ + jid: z.string().min(1), + messageId: z.string().min(1), + text: z.string().min(1), +}); + +export const presenceBodySchema = z.object({ + jid: z.string().optional(), + presence: z.enum(["available", "composing", "recording", "paused", "unavailable"]), +}); + +export const blockStatusBodySchema = z.object({ + jid: z.string().min(1), + action: z.enum(["block", "unblock"]), +}); + +export const findContactsBodySchema = z.object({ + query: z.string().optional(), +}); + +export const base64BodySchema = z.object({ + url: z.string().url().optional(), + path: z.string().optional(), +}).refine((value) => value.url || value.path, "url or path is required."); diff --git a/packages/berryapi/src/modules/flow/flow.catalog.ts b/packages/berryapi/src/modules/flow/flow.catalog.ts new file mode 100644 index 0000000..ffb4c80 --- /dev/null +++ b/packages/berryapi/src/modules/flow/flow.catalog.ts @@ -0,0 +1,569 @@ +export type StudioFieldType = + | "text" + | "textarea" + | "number" + | "checkbox" + | "select" + | "json"; + +export interface StudioFieldDefinition { + key: string; + label: string; + type: StudioFieldType; + required?: boolean; + placeholder?: string; + description?: string; + defaultValue?: unknown; + options?: Array<{ label: string; value: string }>; +} + +export interface StudioCapability { + id: string; + label: string; + description: string; + previewKind: string; + fields: StudioFieldDefinition[]; +} + +const mediaFields: StudioFieldDefinition[] = [ + { + key: "url", + label: "Media URL", + type: "text", + placeholder: "https://...", + }, + { + key: "path", + label: "Local path", + type: "text", + placeholder: "C:\\media\\image.png", + }, + { + key: "base64", + label: "Base64", + type: "textarea", + placeholder: "base64 content", + }, + { + key: "caption", + label: "Caption", + type: "textarea", + }, + { + key: "mimetype", + label: "Mime type", + type: "text", + placeholder: "image/png", + }, + { + key: "fileName", + label: "File name", + type: "text", + placeholder: "asset.png", + }, +]; + +const commonContextFields: StudioFieldDefinition[] = [ + { + key: "mentions", + label: "Mentions JSON", + type: "json", + placeholder: "[\"5511999999999@s.whatsapp.net\"]", + }, + { + key: "contextInfo", + label: "Context JSON", + type: "json", + placeholder: "{\"externalAdReply\": {}}", + }, + { + key: "forwardingScore", + label: "Forwarding score", + type: "number", + defaultValue: 0, + }, + { + key: "ephemeralExpiration", + label: "Ephemeral expiration", + type: "number", + }, +]; + +export const studioMessageCapabilities: StudioCapability[] = [ + { + id: "sendText", + label: "Text", + description: "Basic WhatsApp text message.", + previewKind: "text", + fields: [ + { key: "text", label: "Text", type: "textarea", required: true }, + { key: "linkPreview", label: "Link preview", type: "checkbox" }, + ...commonContextFields, + ], + }, + { + id: "sendExtendedText", + label: "Extended Text", + description: "Extended text using the same payload as text.", + previewKind: "text", + fields: [ + { key: "text", label: "Text", type: "textarea", required: true }, + ...commonContextFields, + ], + }, + { + id: "sendReply", + label: "Reply", + description: "Text reply linked to another message.", + previewKind: "reply", + fields: [ + { key: "text", label: "Reply text", type: "textarea", required: true }, + { + key: "quoted", + label: "Quoted message JSON", + type: "json", + required: true, + placeholder: "{\"remoteJid\":\"5511...@s.whatsapp.net\",\"id\":\"MSG_ID\"}", + }, + ...commonContextFields, + ], + }, + { + id: "sendForward", + label: "Forward", + description: "Forward an existing raw message payload.", + previewKind: "event", + fields: [ + { + key: "message", + label: "Forward message JSON", + type: "json", + required: true, + placeholder: "{\"conversation\":\"Forwarded content\"}", + }, + ...commonContextFields, + ], + }, + { + id: "delete", + label: "Delete", + description: "Delete a message in the conversation.", + previewKind: "event", + fields: [ + { key: "messageId", label: "Message ID", type: "text", required: true }, + { key: "fromMe", label: "From me", type: "checkbox", defaultValue: true }, + { key: "participant", label: "Participant", type: "text" }, + ], + }, + { + id: "edit", + label: "Edit", + description: "Edit an existing message.", + previewKind: "event", + fields: [ + { key: "messageId", label: "Message ID", type: "text", required: true }, + { key: "text", label: "New text", type: "textarea", required: true }, + ], + }, + { + id: "sendReaction", + label: "Reaction", + description: "React to an existing message.", + previewKind: "reaction", + fields: [ + { key: "emoji", label: "Emoji", type: "text", required: true, placeholder: "👍" }, + { key: "targetMessageId", label: "Target message ID", type: "text", required: true }, + ], + }, + { + id: "sendImage", + label: "Image", + description: "Send an image with optional caption.", + previewKind: "image", + fields: mediaFields, + }, + { + id: "sendVideo", + label: "Video", + description: "Send a video with optional caption.", + previewKind: "video", + fields: mediaFields, + }, + { + id: "sendAudio", + label: "Audio", + description: "Send an audio clip.", + previewKind: "audio", + fields: [ + ...mediaFields, + { key: "ptt", label: "Voice note (PTT)", type: "checkbox" }, + ], + }, + { + id: "sendWhatsAppAudio", + label: "WhatsApp Audio", + description: "Alias for WhatsApp-style audio send.", + previewKind: "audio", + fields: [ + ...mediaFields, + { key: "ptt", label: "Voice note (PTT)", type: "checkbox", defaultValue: true }, + ], + }, + { + id: "sendDocument", + label: "Document", + description: "Send a document.", + previewKind: "document", + fields: mediaFields, + }, + { + id: "sendSticker", + label: "Sticker", + description: "Send a sticker asset.", + previewKind: "sticker", + fields: mediaFields, + }, + { + id: "sendGif", + label: "GIF", + description: "Send a gif/video with gif playback.", + previewKind: "video", + fields: mediaFields, + }, + { + id: "sendMedia", + label: "Media Alias", + description: "Generic media alias routed as image.", + previewKind: "image", + fields: mediaFields, + }, + { + id: "sendButtons", + label: "Buttons", + description: "Quick replies, CTA URLs, copy buttons and native-flow style buttons.", + previewKind: "buttons", + fields: [ + { key: "text", label: "Text", type: "textarea", required: true }, + { key: "footer", label: "Footer", type: "text" }, + { key: "ai", label: "AI label", type: "checkbox" }, + { + key: "buttons", + label: "Buttons JSON", + type: "json", + required: true, + placeholder: "[{\"title\":\"Buy now\",\"type\":\"reply\"}]", + }, + ], + }, + { + id: "sendTemplateButtons", + label: "Template Buttons", + description: "Template buttons with reply, URL or copy code.", + previewKind: "buttons", + fields: [ + { key: "text", label: "Text", type: "textarea", required: true }, + { key: "footer", label: "Footer", type: "text" }, + { + key: "buttons", + label: "Buttons JSON", + type: "json", + required: true, + placeholder: "[{\"title\":\"Open site\",\"type\":\"cta_url\",\"url\":\"https://...\"}]", + }, + ], + }, + { + id: "sendCTAButton", + label: "CTA Button", + description: "CTA button payload using the button schema.", + previewKind: "buttons", + fields: [ + { key: "text", label: "Text", type: "textarea", required: true }, + { key: "footer", label: "Footer", type: "text" }, + { + key: "buttons", + label: "Buttons JSON", + type: "json", + required: true, + placeholder: "[{\"title\":\"Visit\",\"type\":\"cta_url\",\"url\":\"https://...\"}]", + }, + ], + }, + { + id: "sendCopyButton", + label: "Copy Button", + description: "Single copy-code button.", + previewKind: "buttons", + fields: [ + { key: "text", label: "Text", type: "textarea", required: true }, + { key: "footer", label: "Footer", type: "text" }, + { key: "copyCode", label: "Copy code", type: "text", required: true }, + { key: "buttonText", label: "Button text", type: "text", required: true }, + { key: "buttonId", label: "Button ID", type: "text" }, + ], + }, + { + id: "sendList", + label: "List", + description: "WhatsApp list message with sections and rows.", + previewKind: "list", + fields: [ + { key: "title", label: "Title", type: "text" }, + { key: "text", label: "Text", type: "textarea", required: true }, + { key: "footer", label: "Footer", type: "text" }, + { key: "buttonText", label: "Button text", type: "text", required: true }, + { + key: "sections", + label: "Sections JSON", + type: "json", + required: true, + placeholder: "[{\"title\":\"Products\",\"rows\":[{\"id\":\"sku-1\",\"title\":\"Starter\"}]}]", + }, + ], + }, + { + id: "sendCarousel", + label: "Carousel", + description: "Cards with image/video and buttons.", + previewKind: "carousel", + fields: [ + { key: "text", label: "Text", type: "textarea", required: true }, + { key: "footer", label: "Footer", type: "text" }, + { + key: "carouselCardType", + label: "Card type", + type: "select", + options: [ + { label: "Image", value: "image" }, + { label: "Video", value: "video" }, + { label: "Mixed", value: "mixed" }, + ], + }, + { key: "ai", label: "AI label", type: "checkbox" }, + { + key: "cards", + label: "Cards JSON", + type: "json", + required: true, + placeholder: "[{\"title\":\"Card 1\",\"body\":\"Body\",\"image\":{\"url\":\"https://...\"},\"buttons\":[{\"title\":\"Buy\",\"kind\":\"reply\"}]}]", + }, + ], + }, + { + id: "sendAiText", + label: "AI Text", + description: "Text with AI label enabled.", + previewKind: "text", + fields: [{ key: "text", label: "Text", type: "textarea", required: true }], + }, + { + id: "sendAiCarousel", + label: "AI Carousel", + description: "Carousel with AI label enabled.", + previewKind: "carousel", + fields: [ + { key: "text", label: "Text", type: "textarea", required: true }, + { key: "footer", label: "Footer", type: "text" }, + { + key: "cards", + label: "Cards JSON", + type: "json", + required: true, + placeholder: "[{\"title\":\"AI Card\",\"body\":\"Generated card\",\"image\":{\"url\":\"https://...\"}}]", + }, + ], + }, + { + id: "sendPoll", + label: "Poll", + description: "Poll with options and selectable count.", + previewKind: "poll", + fields: [ + { key: "title", label: "Title", type: "text", required: true }, + { + key: "options", + label: "Options JSON", + type: "json", + required: true, + placeholder: "[\"Option 1\", \"Option 2\"]", + }, + { key: "selectableCount", label: "Selectable count", type: "number", defaultValue: 1 }, + ], + }, + { + id: "sendLocation", + label: "Location", + description: "Static location pin.", + previewKind: "location", + fields: [ + { key: "latitude", label: "Latitude", type: "number", required: true }, + { key: "longitude", label: "Longitude", type: "number", required: true }, + { key: "name", label: "Name", type: "text" }, + { key: "address", label: "Address", type: "textarea" }, + ], + }, + { + id: "sendLiveLocation", + label: "Live Location", + description: "Live location message with speed/accuracy.", + previewKind: "location", + fields: [ + { key: "latitude", label: "Latitude", type: "number", required: true }, + { key: "longitude", label: "Longitude", type: "number", required: true }, + { key: "name", label: "Name", type: "text" }, + { key: "address", label: "Address", type: "textarea" }, + { key: "speedInMps", label: "Speed m/s", type: "number" }, + { key: "accuracyInMeters", label: "Accuracy meters", type: "number" }, + { key: "degreesClockwiseFromMagneticNorth", label: "Heading", type: "number" }, + ], + }, + { + id: "sendContact", + label: "Contact", + description: "Single vCard contact.", + previewKind: "contacts", + fields: [ + { key: "displayName", label: "Display name", type: "text", required: true }, + { key: "vcard", label: "vCard", type: "textarea", required: true }, + ], + }, + { + id: "sendContacts", + label: "Contacts", + description: "Bundle of vCard contacts.", + previewKind: "contacts", + fields: [ + { key: "displayName", label: "Display name", type: "text" }, + { + key: "contacts", + label: "Contacts JSON", + type: "json", + required: true, + placeholder: "[{\"displayName\":\"Berry\",\"vcard\":\"BEGIN:VCARD...\"}]", + }, + ], + }, + { + id: "sendStatus", + label: "Status", + description: "Status text or media payload.", + previewKind: "status", + fields: [ + { key: "text", label: "Text", type: "textarea" }, + { + key: "media", + label: "Media JSON", + type: "json", + placeholder: "{\"url\":\"https://...\",\"caption\":\"Status\"}", + }, + ], + }, + { + id: "sendViewOnceImage", + label: "View Once Image", + description: "Image visible once.", + previewKind: "image", + fields: mediaFields, + }, + { + id: "sendViewOnceVideo", + label: "View Once Video", + description: "Video visible once.", + previewKind: "video", + fields: mediaFields, + }, + { + id: "sendProduct", + label: "Product", + description: "Product message payload.", + previewKind: "product", + fields: [ + { key: "title", label: "Title", type: "text", required: true }, + { key: "description", label: "Description", type: "textarea" }, + { key: "price", label: "Price", type: "text" }, + { key: "retailerId", label: "Retailer ID", type: "text" }, + { + key: "productImage", + label: "Product image JSON", + type: "json", + placeholder: "{\"url\":\"https://...\"}", + }, + ], + }, + { + id: "sendCatalog", + label: "Catalog", + description: "Catalog message payload.", + previewKind: "catalog", + fields: [ + { key: "title", label: "Title", type: "text", required: true }, + { key: "businessOwnerJid", label: "Business owner JID", type: "text" }, + ], + }, + { + id: "sendCollection", + label: "Collection", + description: "Collection message payload.", + previewKind: "catalog", + fields: [ + { key: "title", label: "Title", type: "text", required: true }, + { key: "businessOwnerJid", label: "Business owner JID", type: "text" }, + { key: "collectionId", label: "Collection ID", type: "text" }, + ], + }, +]; + +export const studioNodeTypes = [ + { + id: "start", + label: "Start", + description: "Entry point of the flow.", + }, + { + id: "message", + label: "Message", + description: "Any BerryProtocol message payload.", + }, + { + id: "input", + label: "Input", + description: "Waits for a user answer and stores it in a variable.", + }, + { + id: "condition", + label: "Condition", + description: "Branch based on variables or the last user input.", + }, + { + id: "setVariable", + label: "Set Variable", + description: "Store a literal or template value in a variable.", + }, + { + id: "delay", + label: "Delay", + description: "Adds a wait marker in the flow.", + }, + { + id: "action", + label: "Action", + description: "Non-message methods like edit, delete or reaction.", + }, + { + id: "end", + label: "End", + description: "Finish the execution.", + }, +]; + +export const studioConditionOperators = [ + { label: "Equals", value: "equals" }, + { label: "Not equals", value: "notEquals" }, + { label: "Contains", value: "contains" }, + { label: "Starts with", value: "startsWith" }, + { label: "Ends with", value: "endsWith" }, + { label: "Regex", value: "regex" }, + { label: "Exists", value: "exists" }, + { label: "Greater than", value: "gt" }, + { label: "Less than", value: "lt" }, +]; diff --git a/packages/berryapi/src/modules/flow/flow.controller.ts b/packages/berryapi/src/modules/flow/flow.controller.ts new file mode 100644 index 0000000..462469a --- /dev/null +++ b/packages/berryapi/src/modules/flow/flow.controller.ts @@ -0,0 +1,137 @@ +import type { FastifyReply, FastifyRequest } from "fastify"; +import { successResponse } from "../../utils/response.js"; +import { validateOrThrow } from "../../utils/zod.js"; +import { + createFlowBodySchema, + executeContinueBodySchema, + executeStartBodySchema, + flowIdParamsSchema, + instanceNameOnlyParamsSchema, + simulateContinueBodySchema, + simulateStartBodySchema, + studioInstanceBodySchema, + updateFlowBodySchema, + updateFlowStatusBodySchema, + validateFlowBodySchema, +} from "./flow.validators.js"; +import type { FlowService } from "./flow.service.js"; + +export class FlowController { + constructor(private readonly service: FlowService) {} + + getCapabilities = async (_request: FastifyRequest, reply: FastifyReply) => { + reply.send(successResponse("Berry Studio capabilities loaded.", this.service.getCapabilities())); + }; + + listFlows = async (_request: FastifyRequest, reply: FastifyReply) => { + reply.send(successResponse("Berry Studio flows loaded.", this.service.listFlows())); + }; + + listInstances = async (_request: FastifyRequest, reply: FastifyReply) => { + reply.send(successResponse("Berry Studio instances loaded.", await this.service.listInstances())); + }; + + createInstance = async (request: FastifyRequest, reply: FastifyReply) => { + const body = validateOrThrow(studioInstanceBodySchema, request.body); + reply.code(201).send(successResponse("Studio instance created successfully.", await this.service.createInstance(body))); + }; + + connectInstance = async (request: FastifyRequest, reply: FastifyReply) => { + const { instanceName } = validateOrThrow(instanceNameOnlyParamsSchema, request.params); + reply.send(successResponse("Studio instance connection started.", await this.service.connectInstance(instanceName))); + }; + + createFlow = async (request: FastifyRequest, reply: FastifyReply) => { + const body = validateOrThrow(createFlowBodySchema, request.body); + reply.code(201).send(successResponse("Flow created successfully.", this.service.createFlow(body))); + }; + + getFlow = async (request: FastifyRequest, reply: FastifyReply) => { + const { flowId } = validateOrThrow(flowIdParamsSchema, request.params); + reply.send(successResponse("Flow loaded successfully.", this.service.getFlow(flowId))); + }; + + updateFlow = async (request: FastifyRequest, reply: FastifyReply) => { + const { flowId } = validateOrThrow(flowIdParamsSchema, request.params); + const body = validateOrThrow(updateFlowBodySchema, request.body); + reply.send(successResponse("Flow saved successfully.", this.service.updateFlow(flowId, body))); + }; + + updateFlowStatus = async (request: FastifyRequest, reply: FastifyReply) => { + const { flowId } = validateOrThrow(flowIdParamsSchema, request.params); + const body = validateOrThrow(updateFlowStatusBodySchema, request.body); + reply.send(successResponse("Flow status updated successfully.", this.service.updateFlowStatus(flowId, body.status))); + }; + + publishFlow = async (request: FastifyRequest, reply: FastifyReply) => { + const { flowId } = validateOrThrow(flowIdParamsSchema, request.params); + reply.send(successResponse("Flow published successfully.", this.service.publishFlow(flowId))); + }; + + validateFlow = async (request: FastifyRequest, reply: FastifyReply) => { + const body = validateOrThrow(validateFlowBodySchema, request.body); + reply.send(successResponse("Flow validation completed.", this.service.validateFlow(body.flow))); + }; + + simulateStart = async (request: FastifyRequest, reply: FastifyReply) => { + const body = validateOrThrow(simulateStartBodySchema, request.body); + reply.send(successResponse("Flow simulation started.", await this.service.simulateStart(body.flow, body.variables, body.contact, body.flowId))); + }; + + simulateContinue = async (request: FastifyRequest, reply: FastifyReply) => { + const body = validateOrThrow(simulateContinueBodySchema, request.body); + reply.send(successResponse("Flow simulation advanced.", await this.service.simulateContinue(body.flow, body.state, body.inputText, body.flowId))); + }; + + executeStart = async (request: FastifyRequest, reply: FastifyReply) => { + const body = validateOrThrow(executeStartBodySchema, request.body); + reply.send(successResponse("Flow execution started.", await this.service.executeStart(body.flow, body.instanceName, body.variables, body.contact, body.flowId))); + }; + + executeContinue = async (request: FastifyRequest, reply: FastifyReply) => { + const body = validateOrThrow(executeContinueBodySchema, request.body); + reply.send(successResponse("Flow execution continued.", await this.service.executeContinue(body.flow, body.state, body.inputText, body.instanceName, body.flowId))); + }; + + deleteFlow = async (request: FastifyRequest, reply: FastifyReply) => { + const { flowId } = validateOrThrow(flowIdParamsSchema, request.params); + await this.service.deleteFlow(flowId); + reply.send(successResponse("Flow deleted successfully.", { flowId })); + }; + + duplicateFlow = async (request: FastifyRequest, reply: FastifyReply) => { + const { flowId } = validateOrThrow(flowIdParamsSchema, request.params); + reply.code(201).send(successResponse("Flow duplicated successfully.", this.service.duplicateFlow(flowId))); + }; + + getAnalytics = async (request: FastifyRequest, reply: FastifyReply) => { + const query = (request.query ?? {}) as { flowId?: string; days?: string }; + const days = query.days ? Number(query.days) : undefined; + reply.send(successResponse("Studio analytics loaded.", this.service.getAnalytics(query.flowId, days))); + }; + + resetAnalytics = async (request: FastifyRequest, reply: FastifyReply) => { + const body = (request.body ?? {}) as { flowId?: string }; + reply.send(successResponse("Studio analytics reset.", this.service.resetAnalytics(body.flowId))); + }; + + listNotifications = async (_request: FastifyRequest, reply: FastifyReply) => { + reply.send(successResponse("Studio notifications loaded.", await this.service.listNotifications())); + }; + + restartInstance = async (request: FastifyRequest, reply: FastifyReply) => { + const { instanceName } = validateOrThrow(instanceNameOnlyParamsSchema, request.params); + reply.send(successResponse("Studio instance restarted.", await this.service.restartInstance(instanceName))); + }; + + logoutInstance = async (request: FastifyRequest, reply: FastifyReply) => { + const { instanceName } = validateOrThrow(instanceNameOnlyParamsSchema, request.params); + reply.send(successResponse("Studio instance logged out.", await this.service.logoutInstance(instanceName))); + }; + + deleteInstance = async (request: FastifyRequest, reply: FastifyReply) => { + const { instanceName } = validateOrThrow(instanceNameOnlyParamsSchema, request.params); + await this.service.deleteInstance(instanceName); + reply.send(successResponse("Studio instance deleted.", { instanceName })); + }; +} diff --git a/packages/berryapi/src/modules/flow/flow.routes.ts b/packages/berryapi/src/modules/flow/flow.routes.ts new file mode 100644 index 0000000..10a235b --- /dev/null +++ b/packages/berryapi/src/modules/flow/flow.routes.ts @@ -0,0 +1,173 @@ +import { createRouteSchema } from "../../docs/schema.js"; +import { FlowController } from "./flow.controller.js"; +import { FlowService } from "./flow.service.js"; +import type { InstanceManager } from "../../managers/InstanceManager.js"; + +export const registerFlowRoutes = async (app: any, manager: InstanceManager) => { + const controller = new FlowController(new FlowService(manager)); + + app.get("/studio/api/capabilities", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Get Berry Studio capabilities", + description: "Loads node types, message types and defaults used by Berry Studio.", + }), + }, controller.getCapabilities); + + app.get("/studio/api/instances", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "List Studio instances", + description: "Loads BerryAPI instances for Studio execution.", + }), + }, controller.listInstances); + + app.post("/studio/api/instances", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Create Studio instance", + description: "Creates a new BerryAPI instance from the Studio UI.", + }), + }, controller.createInstance); + + app.post("/studio/api/instances/:instanceName/connect", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Connect Studio instance", + description: "Starts a connection flow for a Studio instance.", + }), + }, controller.connectInstance); + + app.get("/studio/api/flows", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "List Berry Studio flows", + }), + }, controller.listFlows); + + app.post("/studio/api/flows", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Create Berry Studio flow", + }), + }, controller.createFlow); + + app.get("/studio/api/flows/:flowId", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Get Berry Studio flow", + }), + }, controller.getFlow); + + app.put("/studio/api/flows/:flowId", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Update Berry Studio flow", + }), + }, controller.updateFlow); + + app.post("/studio/api/flows/:flowId/status", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Update Berry Studio flow status", + }), + }, controller.updateFlowStatus); + + app.delete("/studio/api/flows/:flowId", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Delete Berry Studio flow", + }), + }, controller.deleteFlow); + + app.post("/studio/api/flows/:flowId/duplicate", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Duplicate Berry Studio flow", + }), + }, controller.duplicateFlow); + + app.post("/studio/api/flows/:flowId/publish", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Publish Berry Studio flow", + }), + }, controller.publishFlow); + + app.post("/studio/api/flows/validate", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Validate Berry Studio flow", + }), + }, controller.validateFlow); + + app.get("/studio/api/analytics", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Get Studio analytics", + }), + }, controller.getAnalytics); + + app.post("/studio/api/analytics/reset", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Reset Studio analytics", + }), + }, controller.resetAnalytics); + + app.get("/studio/api/notifications", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "List Studio notifications", + }), + }, controller.listNotifications); + + app.post("/studio/api/simulate/start", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Start Berry Studio simulation", + }), + }, controller.simulateStart); + + app.post("/studio/api/simulate/continue", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Continue Berry Studio simulation", + }), + }, controller.simulateContinue); + + app.post("/studio/api/execute/start", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Start real flow execution", + }), + }, controller.executeStart); + + app.post("/studio/api/execute/continue", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Continue real flow execution", + }), + }, controller.executeContinue); + + app.post("/studio/api/instances/:instanceName/restart", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Restart Studio instance", + }), + }, controller.restartInstance); + + app.post("/studio/api/instances/:instanceName/logout", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Logout Studio instance", + }), + }, controller.logoutInstance); + + app.delete("/studio/api/instances/:instanceName", { + schema: createRouteSchema({ + tags: ["Berry Studio"], + summary: "Delete Studio instance", + }), + }, controller.deleteInstance); +}; diff --git a/packages/berryapi/src/modules/flow/flow.service.ts b/packages/berryapi/src/modules/flow/flow.service.ts new file mode 100644 index 0000000..f92f987 --- /dev/null +++ b/packages/berryapi/src/modules/flow/flow.service.ts @@ -0,0 +1,1623 @@ +import { randomUUID } from "node:crypto"; +import { sqlite } from "../../database/client.js"; +import type { InstanceManager } from "../../managers/InstanceManager.js"; +import type { ProviderEventPayload } from "../../providers/whatsapp/WhatsAppProvider.js"; +import { MessageService } from "../message/services/message.service.js"; +import { + studioConditionOperators, + studioMessageCapabilities, + studioNodeTypes, +} from "./flow.catalog.js"; + +type FlowGraph = { + nodes: Array<{ + id: string; + type: string; + position: { x: number; y: number }; + data: Record; + }>; + edges: Array<{ + id: string; + source: string; + target: string; + sourceHandle?: string; + targetHandle?: string; + }>; + viewport?: Record; +}; + +type SimulatorState = { + status: "running" | "waiting_input" | "completed" | "failed"; + currentNodeId: string | null; + waitingNodeId?: string | null; + runId?: string | null; + variables: Record; + transcript: Array>; + logs: string[]; +}; + +type StudioNotification = { + id: string; + type: "flow" | "connection" | "alert"; + title: string; + body: string; + createdAt: number; +}; + +type FlowStatsSummary = { + conversas: number; + leads: number; + conversao: number; + blocos: number; + runs: number; + completed: number; + failed: number; +}; + +type StudioFlowListItem = { + id: string; + name: string; + status: string; + draft_version_id: string | null; + published_version_id: string | null; + created_at: string; + updated_at: string; + stats: FlowStatsSummary; +}; + +type FlowValidationResult = { + valid: boolean; + errors: string[]; + warnings: string[]; +}; + +type DispatchContext = { + instanceName: string; +}; + +type FlowTrigger = { + enabled?: boolean; + instanceName?: string; + keywords?: string[]; + matchMode?: "contains" | "exact" | "startsWith" | "any"; + caseSensitive?: boolean; +}; + +type IncomingFlowEvent = { + instanceName: string; + contactJid: string; + contactName: string; + inputText: string; +}; + +type FlowRecord = { + id: string; + name: string; + status: string; + draft_version_id: string | null; + published_version_id: string | null; + canvas_json: string; + created_at: string; + updated_at: string; +}; + +const now = () => new Date().toISOString(); + +const emptyGraph = (): FlowGraph => ({ + nodes: [ + { + id: "start-1", + type: "start", + position: { x: 120, y: 160 }, + data: { + label: "Start", + }, + }, + { + id: "end-1", + type: "end", + position: { x: 440, y: 160 }, + data: { + label: "End", + }, + }, + ], + edges: [ + { + id: "edge-start-end", + source: "start-1", + target: "end-1", + }, + ], + viewport: { + x: 0, + y: 0, + zoom: 1, + }, +}); + +const safeJsonParse = (value: string | null | undefined, fallback: T): T => { + if (!value) { + return fallback; + } + + try { + return JSON.parse(value) as T; + } catch { + return fallback; + } +}; + +const renderTemplate = (value: string, variables: Record): string => + value.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_, key) => { + const resolved = variables[key.trim()]; + return resolved == null ? "" : String(resolved); + }); + +const parseStructuredValue = (value: unknown): unknown => { + if (typeof value !== "string") { + return value; + } + + const trimmed = value.trim(); + if (!trimmed) { + return value; + } + + if ( + (trimmed.startsWith("{") && trimmed.endsWith("}")) + || (trimmed.startsWith("[") && trimmed.endsWith("]")) + ) { + try { + return JSON.parse(trimmed); + } catch { + return value; + } + } + + return value; +}; + +const getNodeKind = (node: FlowGraph["nodes"][number]) => { + if (node.type === "block") { + const rawKind = node.data.kind; + return typeof rawKind === "string" ? rawKind : "message"; + } + + return node.type; +}; + +const renderPayloadValue = ( + value: unknown, + variables: Record, +): unknown => { + if (typeof value === "string") { + return parseStructuredValue(renderTemplate(value, variables)); + } + + if (Array.isArray(value)) { + return value.map((item) => renderPayloadValue(item, variables)); + } + + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value as Record).map(([key, item]) => [ + key, + renderPayloadValue(item, variables), + ]), + ); + } + + return value; +}; + +const normalizePayload = ( + payload: Record, + variables: Record, +): Record => + renderPayloadValue(payload, variables) as Record; + +export class FlowService { + private readonly messageService?: MessageService; + + constructor(private readonly manager?: InstanceManager) { + this.messageService = manager ? new MessageService(manager) : undefined; + if (manager && typeof manager.onProviderEvent === "function") { + manager.onProviderEvent((event) => this.handleProviderEvent(event)); + } + } + + getCapabilities() { + return { + nodeTypes: studioNodeTypes, + conditionOperators: studioConditionOperators, + messageCapabilities: studioMessageCapabilities, + actionCapabilities: studioMessageCapabilities.filter((capability) => + ["sendReply", "sendForward", "delete", "edit", "sendReaction"].includes(capability.id) + ), + defaultGraph: emptyGraph(), + }; + } + + async listInstances() { + if (!this.manager) { + return []; + } + + return (await this.manager.fetchInstances()).map((instance) => ({ + instanceName: instance.instanceName, + status: instance.status, + connectionState: instance.connectionState, + authMethod: instance.authMethod, + phoneNumber: instance.phoneNumber ?? null, + qrCode: instance.qrCode ?? null, + pairingCode: instance.pairingCode ?? null, + createdAt: instance.createdAt, + updatedAt: instance.updatedAt, + })); + } + + async createInstance(input: { + instanceName: string; + authMethod?: "qr" | "pairing_code" | "link"; + phoneNumber?: string; + settings?: Record; + }) { + if (!this.manager) { + throw new Error("Instance manager is not available."); + } + + return this.manager.createInstance(input); + } + + async connectInstance(instanceName: string) { + if (!this.manager) { + throw new Error("Instance manager is not available."); + } + + return this.manager.connectInstance(instanceName); + } + + async restartInstance(instanceName: string) { + if (!this.manager) { + throw new Error("Instance manager is not available."); + } + + return this.manager.restartInstance(instanceName); + } + + async logoutInstance(instanceName: string) { + if (!this.manager) { + throw new Error("Instance manager is not available."); + } + + return this.manager.logoutInstance(instanceName); + } + + async deleteInstance(instanceName: string) { + if (!this.manager) { + throw new Error("Instance manager is not available."); + } + + await this.manager.deleteInstance(instanceName); + } + + listFlows(): StudioFlowListItem[] { + const statement = sqlite.prepare(` + SELECT id, name, status, draft_version_id, published_version_id, created_at, updated_at + FROM flows + ORDER BY updated_at DESC + `); + + return (statement.all() as Array>).map((flow) => ({ + ...flow, + stats: this.getFlowStats(String(flow.id)), + })) as StudioFlowListItem[]; + } + + createFlow(input: { name: string; graph?: FlowGraph }) { + const timestamp = now(); + const flowId = randomUUID(); + const versionId = randomUUID(); + const graph = input.graph ?? emptyGraph(); + + sqlite.prepare(` + INSERT INTO flows (id, name, status, draft_version_id, published_version_id, canvas_json, created_at, updated_at) + VALUES (?, ?, 'draft', ?, NULL, ?, ?, ?) + `).run(flowId, input.name, versionId, JSON.stringify(graph), timestamp, timestamp); + + sqlite.prepare(` + INSERT INTO flow_versions (id, flow_id, version_number, graph_json, snapshot_json, created_at) + VALUES (?, ?, 1, ?, ?, ?) + `).run(versionId, flowId, JSON.stringify(graph), JSON.stringify({ name: input.name, status: "draft" }), timestamp); + + return this.getFlow(flowId); + } + + getFlow(flowId: string) { + const flow = sqlite.prepare(` + SELECT id, name, status, draft_version_id, published_version_id, canvas_json, created_at, updated_at + FROM flows + WHERE id = ? + `).get(flowId) as FlowRecord | undefined; + + if (!flow) { + return null; + } + + const versions = sqlite.prepare(` + SELECT id, flow_id, version_number, created_at + FROM flow_versions + WHERE flow_id = ? + ORDER BY version_number DESC + `).all(flowId); + + return { + ...flow, + graph: safeJsonParse(flow.canvas_json, emptyGraph()), + versions, + stats: this.getFlowStats(flowId), + }; + } + + updateFlow(flowId: string, input: { name?: string; status?: string; graph: FlowGraph }) { + const existing = this.getFlow(flowId); + if (!existing) { + return null; + } + + const versionCount = sqlite.prepare(` + SELECT COUNT(*) as count + FROM flow_versions + WHERE flow_id = ? + `).get(flowId) as { count: number }; + + const versionId = randomUUID(); + const timestamp = now(); + const nextVersion = (versionCount?.count ?? 0) + 1; + const name = input.name ?? String(existing.name); + const status = input.status ?? String(existing.status); + + sqlite.prepare(` + UPDATE flows + SET name = ?, status = ?, draft_version_id = ?, canvas_json = ?, updated_at = ? + WHERE id = ? + `).run(name, status, versionId, JSON.stringify(input.graph), timestamp, flowId); + + sqlite.prepare(` + INSERT INTO flow_versions (id, flow_id, version_number, graph_json, snapshot_json, created_at) + VALUES (?, ?, ?, ?, ?, ?) + `).run(versionId, flowId, nextVersion, JSON.stringify(input.graph), JSON.stringify({ name, status }), timestamp); + + return this.getFlow(flowId); + } + + publishFlow(flowId: string) { + const existing = this.getFlow(flowId); + if (!existing) { + return null; + } + + const timestamp = now(); + sqlite.prepare(` + UPDATE flows + SET status = 'published', published_version_id = draft_version_id, updated_at = ? + WHERE id = ? + `).run(timestamp, flowId); + + return this.getFlow(flowId); + } + + updateFlowStatus(flowId: string, status: "draft" | "published" | "archived") { + const existing = this.getFlow(flowId); + if (!existing) { + return null; + } + + sqlite.prepare(` + UPDATE flows + SET status = ?, published_version_id = CASE WHEN ? = 'published' THEN draft_version_id ELSE published_version_id END, updated_at = ? + WHERE id = ? + `).run(status, status, now(), flowId); + + return this.getFlow(flowId); + } + + deleteFlow(flowId: string) { + sqlite.prepare(`DELETE FROM flow_runs WHERE flow_id = ?`).run(flowId); + sqlite.prepare(`DELETE FROM flow_versions WHERE flow_id = ?`).run(flowId); + sqlite.prepare(`DELETE FROM flows WHERE id = ?`).run(flowId); + } + + duplicateFlow(flowId: string) { + const existing = this.getFlow(flowId); + if (!existing) { + return null; + } + + return this.createFlow({ + name: `${String(existing.name)} (copy)`, + graph: existing.graph as FlowGraph, + }); + } + + getAnalytics(flowId?: string, days = 14) { + const normalizedDays = Number.isFinite(days) && days > 0 ? Math.min(Math.max(days, 1), 90) : 14; + const cutoff = new Date(Date.now() - normalizedDays * 24 * 60 * 60 * 1000).toISOString(); + const runs = ( + flowId + ? sqlite.prepare(` + SELECT id, flow_id, status, created_at + FROM flow_runs + WHERE flow_id = ? AND created_at >= ? + ORDER BY created_at DESC + `).all(flowId, cutoff) + : sqlite.prepare(` + SELECT id, flow_id, status, created_at + FROM flow_runs + WHERE created_at >= ? + ORDER BY created_at DESC + `).all(cutoff) + ) as Array<{ id: string; flow_id: string; status: string; created_at: string }>; + + const flowOptions = this.listFlows().map((flow) => ({ + id: String(flow.id), + name: String(flow.name), + })); + + const totals = { + visitors: runs.length, + conversations: runs.length, + started: runs.length, + leads: runs.filter((run) => run.status === "waiting_input" || run.status === "completed").length, + conversions: runs.filter((run) => run.status === "completed").length, + failures: runs.filter((run) => run.status === "failed").length, + }; + + const startRate = totals.visitors ? Math.round((totals.started / totals.visitors) * 100) : 0; + const conversionRate = totals.conversations ? Math.round((totals.conversions / totals.conversations) * 100) : 0; + const labels = Array.from({ length: normalizedDays }, (_, index) => { + const date = new Date(); + date.setHours(0, 0, 0, 0); + date.setDate(date.getDate() - (normalizedDays - index - 1)); + return date.toISOString().slice(0, 10); + }); + const activity = labels.map((label) => ({ + label, + value: runs.filter((run) => run.created_at.slice(0, 10) === label).length, + })); + + const perFlow = this.listFlows().map((flow) => ({ + id: String(flow.id), + name: String(flow.name), + status: String(flow.status), + stats: this.getFlowStats(String(flow.id)), + })); + + return { + flowOptions, + periodDays: normalizedDays, + selectedFlowId: flowId ?? "all", + totals: { + ...totals, + startRate, + conversionRate, + }, + activity, + perFlow, + }; + } + + resetAnalytics(flowId?: string) { + if (flowId) { + const info = sqlite.prepare(`DELETE FROM flow_runs WHERE flow_id = ?`).run(flowId); + return { flowId, deletedRuns: info.changes }; + } + + const info = sqlite.prepare(`DELETE FROM flow_runs`).run(); + return { flowId: null, deletedRuns: info.changes }; + } + + async listNotifications(): Promise { + const flows = this.listFlows(); + const instances = await this.listInstances(); + const recentFailedRuns = sqlite.prepare(` + SELECT id, flow_id, status, created_at + FROM flow_runs + WHERE status = 'failed' + ORDER BY updated_at DESC, created_at DESC + LIMIT 10 + `).all() as Array<{ id: string; flow_id: string; status: string; created_at: string }>; + + const notifications: StudioNotification[] = [ + ...instances.map((instance): StudioNotification => ({ + id: `instance:${instance.instanceName}`, + type: instance.connectionState === "open" || instance.status === "connected" ? "connection" : "alert", + title: `Instance ${instance.instanceName}`, + body: + instance.status === "connected" + ? "Instance connected and ready to run flows." + : `Current state: ${instance.connectionState}.`, + createdAt: Date.parse(String(instance.updatedAt ?? now())), + })), + ...flows.slice(0, 10).map((flow) => ({ + id: `flow:${String(flow.id)}`, + type: "flow" as const, + title: String(flow.name), + body: `Flow is currently ${String(flow.status)}.`, + createdAt: Date.parse(String(flow.updated_at ?? now())), + })), + ...recentFailedRuns.map((run) => ({ + id: `run:${run.id}`, + type: "alert" as const, + title: "Flow execution failed", + body: `A recent execution for flow ${run.flow_id} ended with failure.`, + createdAt: Date.parse(run.created_at), + })), + ]; + + return notifications.sort((left, right) => right.createdAt - left.createdAt).slice(0, 30); + } + + validateFlow(flow: FlowGraph): FlowValidationResult { + const errors: string[] = []; + const warnings: string[] = []; + const knownNodeTypes = new Set(studioNodeTypes.map((node) => node.id)); + const knownCapabilities = new Map(studioMessageCapabilities.map((capability) => [capability.id, capability])); + const nodeIds = new Set(); + const startNodes = flow.nodes.filter((node) => { + const kind = getNodeKind(node); + return kind === "start" || node.data.isStart === true; + }); + const endNodes = flow.nodes.filter((node) => getNodeKind(node) === "end"); + + if (!startNodes.length) { + errors.push("The flow must contain at least one start node."); + } + + if (startNodes.length > 1) { + warnings.push("More than one start node was found. Only the first start node will be used."); + } + + if (!endNodes.length) { + warnings.push("The flow does not contain an end node."); + } + + const trigger = flow.viewport?.trigger as FlowTrigger | undefined; + if (trigger?.enabled && !trigger.instanceName?.trim()) { + errors.push("An enabled WhatsApp trigger needs an instance."); + } + + if ( + trigger?.enabled + && trigger.matchMode !== "any" + && (!Array.isArray(trigger.keywords) || !trigger.keywords.some((keyword) => keyword.trim())) + ) { + warnings.push("The trigger has no keywords and will match every incoming message."); + } + + for (const node of flow.nodes) { + if (nodeIds.has(node.id)) { + errors.push(`Node id '${node.id}' is duplicated.`); + } + nodeIds.add(node.id); + + const nodeKind = getNodeKind(node); + + if (!knownNodeTypes.has(nodeKind) && !["message", "media", "choice", "input", "delay", "pixel", "tags", "condition", "note", "end"].includes(nodeKind)) { + errors.push(`Node '${node.id}' uses unknown type '${nodeKind}'.`); + continue; + } + + if ((nodeKind === "message" || nodeKind === "action") && node.type !== "block") { + const messageType = String(node.data.messageType ?? ""); + const capability = knownCapabilities.get(messageType); + if (!capability) { + errors.push(`Node '${node.id}' uses unknown message capability '${messageType}'.`); + continue; + } + + for (const field of capability.fields.filter((field) => field.required)) { + const value = (node.data.payload as Record | undefined)?.[field.key]; + if (value === undefined || value === null || value === "") { + errors.push(`Node '${node.id}' is missing required field '${field.label}'.`); + } + } + } + + if (nodeKind === "message" && node.type === "block" && !String(node.data.text ?? "").trim()) { + warnings.push(`Message block '${node.id}' is empty.`); + } + + if (nodeKind === "choice") { + const responses = Array.isArray(node.data.responses) ? node.data.responses : []; + const choiceBlock = responses.find((response) => (response as Record).kind === "choice") as Record | undefined; + const options = Array.isArray(choiceBlock?.options) ? choiceBlock.options : []; + if (!options.length) { + warnings.push(`Choice block '${node.id}' does not define any options.`); + } + } + + if (nodeKind === "input" && !node.data.variableName) { + warnings.push(`Input node '${node.id}' does not define a variable name. It will default to lastInput.`); + } + + if (nodeKind === "condition") { + const hasTrue = flow.edges.some((edge) => edge.source === node.id && edge.sourceHandle === "true"); + const hasFalse = flow.edges.some((edge) => edge.source === node.id && edge.sourceHandle === "false"); + + if (!hasTrue) { + warnings.push(`Condition node '${node.id}' does not have a true branch.`); + } + + if (!hasFalse) { + warnings.push(`Condition node '${node.id}' does not have a false branch.`); + } + } + } + + for (const edge of flow.edges) { + if (!nodeIds.has(edge.source)) { + errors.push(`Edge '${edge.id}' references missing source node '${edge.source}'.`); + } + + if (!nodeIds.has(edge.target)) { + errors.push(`Edge '${edge.id}' references missing target node '${edge.target}'.`); + } + } + + return { + valid: errors.length === 0, + errors, + warnings, + }; + } + + async simulateStart( + flow: FlowGraph, + variables?: Record, + contact?: { jid?: string; name?: string }, + flowId?: string, + ) { + const initialState: SimulatorState = { + status: "running", + currentNodeId: this.findStartNode(flow)?.id ?? null, + waitingNodeId: null, + runId: null, + variables: { + contactJid: contact?.jid ?? "5511999999999@s.whatsapp.net", + contactName: contact?.name ?? "Cliente", + flowId: flowId ?? null, + ...(variables ?? {}), + }, + transcript: [], + logs: [], + }; + + return this.advance(flow, initialState); + } + + async executeStart( + flow: FlowGraph, + instanceName: string, + variables?: Record, + contact?: { jid?: string; name?: string }, + flowId?: string, + ) { + const validation = this.validateFlow(flow); + if (!validation.valid) { + return { + status: "failed", + currentNodeId: null, + waitingNodeId: null, + runId: null, + variables: variables ?? {}, + transcript: [], + logs: ["Flow validation failed before execution.", ...validation.errors], + validation, + }; + } + + const executionVariables: Record = { + contactJid: contact?.jid ?? "5511999999999@s.whatsapp.net", + contactName: contact?.name ?? "Berry Contact", + flowId: flowId ?? null, + instanceName, + ...(variables ?? {}), + }; + const initialState: SimulatorState = { + status: "running", + currentNodeId: this.findStartNode(flow)?.id ?? null, + waitingNodeId: null, + runId: flowId ? this.createRun(flowId, "running", executionVariables) : null, + variables: executionVariables, + transcript: [], + logs: [`Executing flow with instance '${instanceName}'.`], + }; + + const state = await this.advance(flow, initialState, { + instanceName, + }); + + this.updateRun(initialState.runId, state); + + return { + ...state, + validation, + }; + } + + async simulateContinue(flow: FlowGraph, state: SimulatorState, inputText: string, flowId?: string) { + if (state.status !== "waiting_input" || !state.waitingNodeId) { + return { + ...state, + logs: [...state.logs, "No pending input node to continue."], + }; + } + + const waitingNode = this.findNode(flow, state.waitingNodeId); + if (!waitingNode) { + return { + ...state, + status: "failed", + runId: state.runId ?? null, + logs: [...state.logs, `Input node ${state.waitingNodeId} not found.`], + }; + } + + const variableName = String(state.variables.waitingVariableName ?? waitingNode.data.variableName ?? "lastInput"); + const transcript = [ + ...state.transcript, + { + role: "user", + type: "text", + text: inputText, + }, + ]; + + const nextState = await this.advance(flow, { + ...state, + status: "running", + currentNodeId: this.getNextNodeId(flow, waitingNode.id), + waitingNodeId: null, + variables: { + ...state.variables, + lastInput: inputText, + [variableName]: inputText, + flowId: flowId ?? state.variables.flowId, + }, + transcript, + logs: [...state.logs, `Captured input for ${variableName}.`], + }); + this.updateRun(state.runId, nextState); + return nextState; + } + + async executeContinue(flow: FlowGraph, state: SimulatorState, inputText: string, instanceName: string, flowId?: string) { + if (state.status !== "waiting_input" || !state.waitingNodeId) { + return { + ...state, + logs: [...state.logs, "No pending input node to continue."], + }; + } + + const waitingNode = this.findNode(flow, state.waitingNodeId); + if (!waitingNode) { + return { + ...state, + status: "failed", + runId: state.runId ?? null, + logs: [...state.logs, `Input node ${state.waitingNodeId} not found.`], + }; + } + + const variableName = String(state.variables.waitingVariableName ?? waitingNode.data.variableName ?? "lastInput"); + const transcript = [ + ...state.transcript, + { + role: "user", + type: "text", + text: inputText, + }, + ]; + + const nextState = await this.advance(flow, { + ...state, + status: "running", + currentNodeId: this.getNextNodeId(flow, waitingNode.id), + waitingNodeId: null, + variables: { + ...state.variables, + lastInput: inputText, + [variableName]: inputText, + flowId: flowId ?? state.variables.flowId, + }, + transcript, + logs: [...state.logs, `Captured input for ${variableName}.`], + }, { + instanceName, + }); + this.updateRun(state.runId, nextState); + return nextState; + } + + private async advance( + flow: FlowGraph, + state: SimulatorState, + dispatchContext?: DispatchContext, + ): Promise { + let cursor = state.currentNodeId; + const transcript = [...state.transcript]; + const logs = [...state.logs]; + const variables = { ...state.variables }; + let guard = 0; + + while (cursor && guard < 100) { + guard += 1; + const node = this.findNode(flow, cursor); + + if (!node) { + return { + status: "failed", + currentNodeId: null, + waitingNodeId: null, + variables, + transcript, + logs: [...logs, `Node ${cursor} not found.`], + }; + } + + const nodeKind = getNodeKind(node); + + if (nodeKind === "start") { + logs.push("Flow started."); + cursor = this.getNextNodeId(flow, node.id); + continue; + } + + if (nodeKind === "note") { + logs.push(`Note block ${node.id} skipped.`); + cursor = this.getNextNodeId(flow, node.id); + continue; + } + + if (node.type === "message" || node.type === "action" || ["message", "media", "choice"].includes(nodeKind)) { + const executable = node.type === "block" + ? this.buildStudioBlockMessage(node, variables) + : { + messageType: String(node.data.messageType ?? "sendText"), + payload: normalizePayload( + (node.data.payload as Record) ?? {}, + variables, + ), + previewKind: studioMessageCapabilities.find((item) => item.id === String(node.data.messageType ?? "sendText"))?.previewKind ?? "text", + waitForInput: false, + variableName: null, + }; + + const { messageType, payload, previewKind } = executable; + const payloadRecord = payload as Record; + + if (dispatchContext) { + try { + const dispatchPayload = { + to: String(payloadRecord.to ?? variables.contactJid ?? ""), + ...payloadRecord, + }; + const response = await this.dispatchMessageAction( + dispatchContext.instanceName, + messageType, + dispatchPayload, + ); + + if (response && typeof response === "object" && "messageId" in (response as Record)) { + variables.lastMessageId = (response as Record).messageId; + } + } catch (error) { + return { + status: "failed", + currentNodeId: node.id, + waitingNodeId: null, + variables, + transcript, + logs: [ + ...logs, + `Failed to execute ${messageType} on node ${node.id}: ${ + error instanceof Error ? error.message : "unknown error" + }`, + ], + }; + } + } + + transcript.push({ + role: "assistant", + type: messageType, + previewKind, + payload: payloadRecord, + }); + logs.push(`Executed ${messageType}.`); + + if (executable.waitForInput) { + logs.push(`Waiting for input on node ${node.id}.`); + return { + status: "waiting_input", + currentNodeId: node.id, + waitingNodeId: node.id, + runId: state.runId ?? null, + variables: { + ...variables, + waitingVariableName: executable.variableName ?? "lastInput", + }, + transcript, + logs, + }; + } + + cursor = this.getNextNodeId(flow, node.id); + continue; + } + + if (nodeKind === "setVariable") { + const key = String(node.data.key ?? "variable"); + const value = renderTemplate(String(node.data.value ?? ""), variables); + variables[key] = parseStructuredValue(value); + logs.push(`Variable ${key} updated.`); + cursor = this.getNextNodeId(flow, node.id); + continue; + } + + if (nodeKind === "tags") { + const rawTags = Array.isArray(node.data.tags) + ? node.data.tags + : String(node.data.text ?? "") + .split(",") + .map((value) => value.trim()) + .filter(Boolean); + variables.tags = rawTags; + logs.push(`Tags updated with ${rawTags.length} item(s).`); + cursor = this.getNextNodeId(flow, node.id); + continue; + } + + if (nodeKind === "pixel") { + const eventName = String(node.data.eventName ?? node.data.text ?? "pixel_event"); + variables.lastPixelEvent = eventName; + transcript.push({ + role: "system", + type: "pixel", + eventName, + }); + logs.push(`Pixel event '${eventName}' registered.`); + cursor = this.getNextNodeId(flow, node.id); + continue; + } + + if (nodeKind === "delay") { + const requestedDelayMs = Number(node.data.delayMs ?? node.data.seconds ?? 1000); + const delayMs = Number.isFinite(requestedDelayMs) + ? Math.min(Math.max(requestedDelayMs, 0), 60_000) + : 1000; + transcript.push({ + role: "system", + type: "delay", + delayMs, + }); + if (dispatchContext && delayMs > 0) { + logs.push(`Waiting ${delayMs}ms before the next node.`); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } else { + logs.push(`Delay marker ${delayMs}ms.`); + } + cursor = this.getNextNodeId(flow, node.id); + continue; + } + + if (nodeKind === "input") { + const prompt = renderTemplate( + String(node.data.prompt ?? node.data.text ?? "Waiting for user input"), + variables, + ); + const variableName = String(node.data.variableName ?? "lastInput"); + if (prompt) { + if (dispatchContext) { + try { + await this.dispatchMessageAction( + dispatchContext.instanceName, + "sendText", + { + to: String(variables.contactJid ?? ""), + text: prompt, + }, + ); + } catch (error) { + return { + status: "failed", + currentNodeId: node.id, + waitingNodeId: null, + runId: state.runId ?? null, + variables, + transcript, + logs: [ + ...logs, + `Failed to send input prompt on node ${node.id}: ${ + error instanceof Error ? error.message : "unknown error" + }`, + ], + }; + } + } + + transcript.push({ + role: "assistant", + type: "input_prompt", + previewKind: "text", + payload: { + text: prompt, + }, + }); + } + + logs.push(`Waiting for input on node ${node.id}.`); + return { + status: "waiting_input", + currentNodeId: node.id, + waitingNodeId: node.id, + runId: state.runId ?? null, + variables: { + ...variables, + waitingVariableName: variableName, + }, + transcript, + logs: [...logs, `Input will be stored in ${variableName}.`], + }; + } + + if (nodeKind === "condition") { + const result = this.evaluateCondition(node.data, variables); + logs.push(`Condition ${node.id} resolved to ${result ? "true" : "false"}.`); + cursor = this.getNextNodeId(flow, node.id, result ? "true" : "false") + ?? this.getNextNodeId(flow, node.id); + continue; + } + + if (nodeKind === "end") { + logs.push("Flow completed."); + return { + status: "completed", + currentNodeId: node.id, + waitingNodeId: null, + runId: state.runId ?? null, + variables, + transcript, + logs, + }; + } + + logs.push(`Unknown node type ${nodeKind}, skipping.`); + cursor = this.getNextNodeId(flow, node.id); + } + + if (guard >= 100) { + logs.push("Execution stopped after reaching the safety limit."); + } + + return { + status: cursor ? "failed" : "completed", + currentNodeId: cursor, + waitingNodeId: null, + runId: state.runId ?? null, + variables, + transcript, + logs, + }; + } + + private async handleProviderEvent(event: ProviderEventPayload): Promise { + if (event.event !== "messages.upsert") { + return; + } + + const incoming = this.extractIncomingFlowEvent(event); + if (!incoming) { + return; + } + + const resumed = await this.resumeWaitingRun(incoming); + if (!resumed) { + await this.startTriggeredFlow(incoming); + } + } + + private extractIncomingFlowEvent( + event: ProviderEventPayload, + ): IncomingFlowEvent | null { + const rawPayload = Array.isArray(event.payload) + ? event.payload[0] + : event.payload; + if (!rawPayload || typeof rawPayload !== "object") { + return null; + } + + const envelope = rawPayload as Record; + const rawMessage = + envelope.message && typeof envelope.message === "object" + ? envelope.message as Record + : envelope; + const key = + rawMessage.key && typeof rawMessage.key === "object" + ? rawMessage.key as Record + : envelope.key && typeof envelope.key === "object" + ? envelope.key as Record + : {}; + const nestedContent = + rawMessage.message && typeof rawMessage.message === "object" + ? rawMessage.message as Record + : {}; + const extendedText = + nestedContent.extendedTextMessage + && typeof nestedContent.extendedTextMessage === "object" + ? nestedContent.extendedTextMessage as Record + : {}; + const fromMe = Boolean( + rawMessage.fromMe + ?? envelope.fromMe + ?? key.fromMe, + ); + if (fromMe) { + return null; + } + + const firstString = (...values: unknown[]) => + values.find((value): value is string => + typeof value === "string" && value.trim().length > 0 + )?.trim(); + + const contactJid = firstString( + rawMessage.from, + rawMessage.chatId, + rawMessage.remoteJid, + envelope.from, + envelope.chatId, + envelope.remoteJid, + key.remoteJid, + ); + const inputText = firstString( + rawMessage.buttonId, + rawMessage.selectedId, + rawMessage.listRowId, + rawMessage.text, + rawMessage.body, + envelope.buttonId, + envelope.selectedId, + envelope.text, + envelope.body, + nestedContent.conversation, + extendedText.text, + ); + + if (!contactJid || !inputText || contactJid === "status@broadcast") { + return null; + } + + return { + instanceName: event.instanceName, + contactJid, + contactName: firstString( + rawMessage.pushName, + rawMessage.senderName, + envelope.pushName, + envelope.senderName, + ) ?? "Contato WhatsApp", + inputText, + }; + } + + private async resumeWaitingRun( + incoming: IncomingFlowEvent, + ): Promise { + const waitingRuns = sqlite.prepare(` + SELECT id, flow_id, flow_version_id, current_node_id, variables_json, history_json + FROM flow_runs + WHERE status = 'waiting_input' + ORDER BY updated_at DESC + LIMIT 200 + `).all() as Array<{ + id: string; + flow_id: string; + flow_version_id: string | null; + current_node_id: string | null; + variables_json: string; + history_json: string; + }>; + + const waitingRun = waitingRuns.find((run) => { + const variables = safeJsonParse>( + run.variables_json, + {}, + ); + return variables.instanceName === incoming.instanceName + && variables.contactJid === incoming.contactJid; + }); + + if (!waitingRun) { + return false; + } + + const variables = safeJsonParse>( + waitingRun.variables_json, + {}, + ); + const graph = this.loadRunGraph( + waitingRun.flow_id, + waitingRun.flow_version_id, + ); + if (!graph) { + return false; + } + + await this.executeContinue( + graph, + { + status: "waiting_input", + currentNodeId: waitingRun.current_node_id, + waitingNodeId: waitingRun.current_node_id, + runId: waitingRun.id, + variables, + transcript: safeJsonParse>>( + waitingRun.history_json, + [], + ), + logs: ["Flow resumed from an incoming WhatsApp message."], + }, + incoming.inputText, + incoming.instanceName, + waitingRun.flow_id, + ); + + return true; + } + + private async startTriggeredFlow( + incoming: IncomingFlowEvent, + ): Promise { + const publishedFlows = sqlite.prepare(` + SELECT id, published_version_id, canvas_json + FROM flows + WHERE status = 'published' + ORDER BY updated_at DESC + `).all() as Array<{ + id: string; + published_version_id: string | null; + canvas_json: string; + }>; + + for (const flow of publishedFlows) { + const graph = this.loadRunGraph(flow.id, flow.published_version_id) + ?? safeJsonParse(flow.canvas_json, emptyGraph()); + const trigger = graph.viewport?.trigger as FlowTrigger | undefined; + if (!this.triggerMatches(trigger, incoming)) { + continue; + } + + await this.executeStart( + graph, + incoming.instanceName, + { + lastInput: incoming.inputText, + triggerText: incoming.inputText, + instanceName: incoming.instanceName, + }, + { + jid: incoming.contactJid, + name: incoming.contactName, + }, + flow.id, + ); + return true; + } + + return false; + } + + private triggerMatches( + trigger: FlowTrigger | undefined, + incoming: IncomingFlowEvent, + ): boolean { + if ( + !trigger?.enabled + || !trigger.instanceName + || trigger.instanceName !== incoming.instanceName + ) { + return false; + } + + const keywords = Array.isArray(trigger.keywords) + ? trigger.keywords.map((keyword) => keyword.trim()).filter(Boolean) + : []; + if (trigger.matchMode === "any" || !keywords.length) { + return true; + } + + const normalize = (value: string) => + trigger.caseSensitive ? value : value.toLocaleLowerCase(); + const input = normalize(incoming.inputText); + + return keywords.some((keyword) => { + const expected = normalize(keyword); + if (trigger.matchMode === "exact") { + return input === expected; + } + if (trigger.matchMode === "startsWith") { + return input.startsWith(expected); + } + return input.includes(expected); + }); + } + + private loadRunGraph( + flowId: string, + flowVersionId: string | null, + ): FlowGraph | null { + if (flowVersionId) { + const version = sqlite.prepare(` + SELECT graph_json + FROM flow_versions + WHERE id = ? AND flow_id = ? + `).get(flowVersionId, flowId) as { graph_json: string } | undefined; + if (version) { + return safeJsonParse(version.graph_json, emptyGraph()); + } + } + + const flow = sqlite.prepare(` + SELECT canvas_json + FROM flows + WHERE id = ? + `).get(flowId) as { canvas_json: string } | undefined; + return flow + ? safeJsonParse(flow.canvas_json, emptyGraph()) + : null; + } + + private evaluateCondition(nodeData: Record, variables: Record): boolean { + const variableKey = String(nodeData.variable ?? "lastInput"); + const operator = String(nodeData.operator ?? "equals"); + const expectedRaw = renderTemplate(String(nodeData.value ?? ""), variables); + const actual = variables[variableKey]; + const actualString = actual == null ? "" : String(actual); + + switch (operator) { + case "equals": + return actualString === expectedRaw; + case "notEquals": + return actualString !== expectedRaw; + case "contains": + return actualString.includes(expectedRaw); + case "startsWith": + return actualString.startsWith(expectedRaw); + case "endsWith": + return actualString.endsWith(expectedRaw); + case "regex": + return new RegExp(expectedRaw).test(actualString); + case "exists": + return actual !== undefined && actual !== null && actualString.length > 0; + case "gt": + return Number(actual) > Number(expectedRaw); + case "lt": + return Number(actual) < Number(expectedRaw); + default: + return false; + } + } + + private findStartNode(flow: FlowGraph) { + return flow.nodes.find((node) => getNodeKind(node) === "start" || node.data.isStart === true) ?? flow.nodes[0]; + } + + private findNode(flow: FlowGraph, nodeId: string) { + return flow.nodes.find((node) => node.id === nodeId); + } + + private getNextNodeId(flow: FlowGraph, nodeId: string, handle?: string) { + if (handle) { + const handledEdge = flow.edges.find((edge) => edge.source === nodeId && edge.sourceHandle === handle); + if (handledEdge) { + return handledEdge.target; + } + } + + return flow.edges.find((edge) => edge.source === nodeId)?.target ?? null; + } + + private buildStudioBlockMessage(node: FlowGraph["nodes"][number], variables: Record) { + const nodeKind = getNodeKind(node); + const renderedText = renderTemplate(String(node.data.text ?? ""), variables); + + if (nodeKind === "choice") { + const responses = Array.isArray(node.data.responses) ? node.data.responses : []; + const choiceBlock = responses.find((response) => (response as Record).kind === "choice") as Record | undefined; + const options = Array.isArray(choiceBlock?.options) ? choiceBlock.options as Array> : []; + + return { + messageType: "sendButtons", + previewKind: "buttons", + payload: { + text: renderedText || "Choose one option", + buttons: options.map((option, index) => ({ + id: String(option.id ?? `option-${index + 1}`), + title: String(option.label ?? `Option ${index + 1}`), + type: "reply", + })), + }, + waitForInput: true, + variableName: String(node.data.variableName ?? "lastChoice"), + }; + } + + if (nodeKind === "media") { + const payload = normalizePayload((node.data.payload as Record) ?? {}, variables); + const mediaType = String(node.data.mediaType ?? payload.mediaType ?? "sendImage"); + return { + messageType: mediaType, + previewKind: mediaType.includes("Video") ? "video" : mediaType.includes("Audio") ? "audio" : "image", + payload: { + caption: renderedText || payload.caption, + ...payload, + }, + waitForInput: false, + variableName: null, + }; + } + + return { + messageType: "sendText", + previewKind: "text", + payload: { + text: renderedText, + }, + waitForInput: false, + variableName: null, + }; + } + + private getFlowStats(flowId: string): FlowStatsSummary { + const flow = sqlite.prepare(` + SELECT canvas_json + FROM flows + WHERE id = ? + `).get(flowId) as { canvas_json: string } | undefined; + + const graph = safeJsonParse(flow?.canvas_json, emptyGraph()); + const runs = sqlite.prepare(` + SELECT status + FROM flow_runs + WHERE flow_id = ? + `).all(flowId) as Array<{ status: string }>; + const conversations = runs.length; + const leads = runs.filter((run) => run.status === "waiting_input" || run.status === "completed").length; + const conversions = runs.filter((run) => run.status === "completed").length; + + return { + conversas: conversations, + leads, + conversao: conversations ? Math.round((conversions / conversations) * 100) : 0, + blocos: graph.nodes.length, + runs: conversations, + completed: conversions, + failed: runs.filter((run) => run.status === "failed").length, + }; + } + + private createRun(flowId: string, status: string, variables: Record) { + const runId = randomUUID(); + const timestamp = now(); + const flow = sqlite.prepare(` + SELECT published_version_id, draft_version_id + FROM flows + WHERE id = ? + `).get(flowId) as { + published_version_id: string | null; + draft_version_id: string | null; + } | undefined; + const flowVersionId = flow?.published_version_id ?? flow?.draft_version_id ?? null; + sqlite.prepare(` + INSERT INTO flow_runs (id, flow_id, flow_version_id, status, current_node_id, variables_json, history_json, created_at, updated_at) + VALUES (?, ?, ?, ?, NULL, ?, '[]', ?, ?) + `).run( + runId, + flowId, + flowVersionId, + status, + JSON.stringify(variables), + timestamp, + timestamp, + ); + return runId; + } + + private updateRun( + runId: string | null | undefined, + state: Pick, + ) { + if (!runId) { + return; + } + + sqlite.prepare(` + UPDATE flow_runs + SET status = ?, current_node_id = ?, variables_json = ?, history_json = ?, updated_at = ? + WHERE id = ? + `).run( + state.status, + state.currentNodeId, + JSON.stringify(state.variables), + JSON.stringify(state.transcript), + now(), + runId, + ); + } + + private async dispatchMessageAction( + instanceName: string, + messageType: string, + payload: Record, + ) { + if (!this.messageService) { + throw new Error("Message service is not available."); + } + + const messageMethodMap: Record = { + sendText: "sendText", + sendExtendedText: "sendExtendedText", + sendReply: "sendReply", + sendForward: "sendForward", + delete: "deleteMessage", + edit: "editMessage", + sendReaction: "sendReaction", + sendImage: "sendImage", + sendVideo: "sendVideo", + sendAudio: "sendAudio", + sendWhatsAppAudio: "sendAudio", + sendDocument: "sendDocument", + sendSticker: "sendSticker", + sendGif: "sendGif", + sendMedia: "sendImage", + sendButtons: "sendButtons", + sendTemplateButtons: "sendTemplateButtons", + sendCTAButton: "sendCTAButton", + sendCopyButton: "sendCopyButton", + sendList: "sendList", + sendCarousel: "sendCarousel", + sendAiText: "sendAiText", + sendAiCarousel: "sendAiCarousel", + sendPoll: "sendPoll", + sendLocation: "sendLocation", + sendLiveLocation: "sendLiveLocation", + sendContact: "sendContact", + sendContacts: "sendContacts", + sendStatus: "sendStatus", + sendViewOnceImage: "sendViewOnceImage", + sendViewOnceVideo: "sendViewOnceVideo", + sendProduct: "sendProduct", + sendCatalog: "sendCatalog", + sendCollection: "sendCollection", + }; + + const methodName = messageMethodMap[messageType]; + if (!methodName) { + throw new Error(`Unsupported message capability '${messageType}'.`); + } + + const method = (this.messageService as unknown as Record)[String(methodName)]; + if (typeof method !== "function") { + throw new Error(`Mapped service method '${String(methodName)}' is not available.`); + } + + return Reflect.apply( + method as (...args: unknown[]) => Promise, + this.messageService, + [instanceName, payload], + ); + } +} diff --git a/packages/berryapi/src/modules/flow/flow.validators.ts b/packages/berryapi/src/modules/flow/flow.validators.ts new file mode 100644 index 0000000..76f25ee --- /dev/null +++ b/packages/berryapi/src/modules/flow/flow.validators.ts @@ -0,0 +1,98 @@ +import { z } from "zod"; + +const jsonRecordSchema = z.record(z.string(), z.unknown()); + +const flowNodeSchema = z.object({ + id: z.string().min(1), + type: z.string().min(1), + position: z.object({ + x: z.number(), + y: z.number(), + }), + data: jsonRecordSchema.default({}), +}); + +const flowEdgeSchema = z.object({ + id: z.string().min(1), + source: z.string().min(1), + target: z.string().min(1), + sourceHandle: z.string().optional(), + targetHandle: z.string().optional(), +}); + +export const flowGraphSchema = z.object({ + nodes: z.array(flowNodeSchema).default([]), + edges: z.array(flowEdgeSchema).default([]), + viewport: jsonRecordSchema.optional(), +}); + +export const createFlowBodySchema = z.object({ + name: z.string().min(1), + graph: flowGraphSchema.optional(), +}); + +export const updateFlowBodySchema = z.object({ + name: z.string().min(1).optional(), + status: z.enum(["draft", "published", "archived"]).optional(), + graph: flowGraphSchema, +}); + +export const updateFlowStatusBodySchema = z.object({ + status: z.enum(["draft", "published", "archived"]), +}); + +export const flowIdParamsSchema = z.object({ + flowId: z.string().min(1), +}); + +export const simulateStartBodySchema = z.object({ + flowId: z.string().min(1).optional(), + flow: flowGraphSchema, + variables: jsonRecordSchema.optional(), + contact: z.object({ + jid: z.string().optional(), + name: z.string().optional(), + }).optional(), +}); + +export const simulateContinueBodySchema = z.object({ + flowId: z.string().min(1).optional(), + flow: flowGraphSchema, + state: z.object({ + status: z.enum(["running", "waiting_input", "completed", "failed"]), + currentNodeId: z.string().nullable(), + waitingNodeId: z.string().nullable().optional(), + runId: z.string().nullable().optional(), + variables: jsonRecordSchema.default({}), + transcript: z.array(jsonRecordSchema).default([]), + logs: z.array(z.string()).default([]), + }), + inputText: z.string(), +}); + +export const studioInstanceBodySchema = z.object({ + instanceName: z.string().min(1), + authMethod: z.enum(["qr", "pairing_code", "link"]).default("qr"), + phoneNumber: z.string().optional(), + settings: z.object({ + rejectCall: z.boolean().optional(), + readMessages: z.boolean().optional(), + syncFullHistory: z.boolean().optional(), + }).optional(), +}); + +export const instanceNameOnlyParamsSchema = z.object({ + instanceName: z.string().min(1), +}); + +export const validateFlowBodySchema = z.object({ + flow: flowGraphSchema, +}); + +export const executeStartBodySchema = simulateStartBodySchema.extend({ + instanceName: z.string().min(1), +}); + +export const executeContinueBodySchema = simulateContinueBodySchema.extend({ + instanceName: z.string().min(1), +}); diff --git a/packages/berryapi/src/modules/group/group.controller.ts b/packages/berryapi/src/modules/group/group.controller.ts new file mode 100644 index 0000000..5cb6967 --- /dev/null +++ b/packages/berryapi/src/modules/group/group.controller.ts @@ -0,0 +1,100 @@ +import type { FastifyReply, FastifyRequest } from "fastify"; +import type { ZodTypeAny } from "zod"; +import { successResponse } from "../../utils/response.js"; +import { validateOrThrow } from "../../utils/zod.js"; +import { + createGroupBodySchema, + groupJidQuerySchema, + instanceNameParamsSchema, + inviteCodeQuerySchema, + toggleEphemeralBodySchema, + updateDescriptionBodySchema, + updateMembersBodySchema, + updatePictureBodySchema, + updateSettingBodySchema, + updateSubjectBodySchema, + groupInviteBodySchema, +} from "./group.validators.js"; +import type { GroupService } from "./group.service.js"; + +export class GroupController { + constructor(private readonly service: GroupService) {} + + private getInstanceName(request: FastifyRequest): string { + return validateOrThrow(instanceNameParamsSchema, request.params).instanceName; + } + + private async runBody( + request: FastifyRequest, + reply: FastifyReply, + schema: ZodTypeAny, + message: string, + executor: (instanceName: string, input: any) => Promise, + ) { + const instanceName = this.getInstanceName(request); + const body = validateOrThrow(schema, request.body) as Record; + const data = await executor(instanceName, body); + reply.send(successResponse(message, data)); + } + + private async runQuery( + request: FastifyRequest, + reply: FastifyReply, + schema: ZodTypeAny, + message: string, + executor: (instanceName: string, input: any) => Promise, + ) { + const instanceName = this.getInstanceName(request); + const query = validateOrThrow(schema, request.query) as Record; + const data = await executor(instanceName, query); + reply.send(successResponse(message, data)); + } + + create = async (request: FastifyRequest, reply: FastifyReply) => + this.runBody(request, reply, createGroupBodySchema, "Group created successfully.", this.service.create.bind(this.service)); + + updatePicture = async (request: FastifyRequest, reply: FastifyReply) => + this.runBody(request, reply, updatePictureBodySchema, "Group picture updated successfully.", this.service.updatePicture.bind(this.service)); + + updateSubject = async (request: FastifyRequest, reply: FastifyReply) => + this.runBody(request, reply, updateSubjectBodySchema, "Group subject updated successfully.", this.service.updateSubject.bind(this.service)); + + updateDescription = async (request: FastifyRequest, reply: FastifyReply) => + this.runBody(request, reply, updateDescriptionBodySchema, "Group description updated successfully.", this.service.updateDescription.bind(this.service)); + + revokeInviteCode = async (request: FastifyRequest, reply: FastifyReply) => + this.runBody(request, reply, groupJidQuerySchema, "Group invite code revoked successfully.", this.service.revokeInviteCode.bind(this.service)); + + sendInvite = async (request: FastifyRequest, reply: FastifyReply) => + this.runBody(request, reply, groupInviteBodySchema, "Group invite sent successfully.", this.service.sendInvite.bind(this.service)); + + updateMembers = async (request: FastifyRequest, reply: FastifyReply) => + this.runBody(request, reply, updateMembersBodySchema, "Group members updated successfully.", this.service.updateMembers.bind(this.service)); + + updateSetting = async (request: FastifyRequest, reply: FastifyReply) => + this.runBody(request, reply, updateSettingBodySchema, "Group setting updated successfully.", this.service.updateSetting.bind(this.service)); + + toggleEphemeral = async (request: FastifyRequest, reply: FastifyReply) => + this.runBody(request, reply, toggleEphemeralBodySchema, "Group ephemeral setting updated successfully.", this.service.toggleEphemeral.bind(this.service)); + + inviteCode = async (request: FastifyRequest, reply: FastifyReply) => + this.runQuery(request, reply, groupJidQuerySchema, "Group invite code fetched successfully.", this.service.inviteCode.bind(this.service)); + + findByInviteCode = async (request: FastifyRequest, reply: FastifyReply) => + this.runQuery(request, reply, inviteCodeQuerySchema, "Group fetched by invite code successfully.", this.service.findByInviteCode.bind(this.service)); + + findByJid = async (request: FastifyRequest, reply: FastifyReply) => + this.runQuery(request, reply, groupJidQuerySchema, "Group fetched successfully.", this.service.findByJid.bind(this.service)); + + fetchAll = async (request: FastifyRequest, reply: FastifyReply) => { + const instanceName = this.getInstanceName(request); + const data = await this.service.fetchAll(instanceName); + reply.send(successResponse("Groups fetched successfully.", data)); + }; + + members = async (request: FastifyRequest, reply: FastifyReply) => + this.runQuery(request, reply, groupJidQuerySchema, "Group members fetched successfully.", this.service.members.bind(this.service)); + + leave = async (request: FastifyRequest, reply: FastifyReply) => + this.runBody(request, reply, groupJidQuerySchema, "Left group successfully.", this.service.leave.bind(this.service)); +} diff --git a/packages/berryapi/src/modules/group/group.routes.ts b/packages/berryapi/src/modules/group/group.routes.ts new file mode 100644 index 0000000..4ab23ac --- /dev/null +++ b/packages/berryapi/src/modules/group/group.routes.ts @@ -0,0 +1,73 @@ +import type { FastifyInstance } from "fastify"; +import { createRouteSchema } from "../../docs/schema.js"; +import type { InstanceManager } from "../../managers/InstanceManager.js"; +import { GroupController } from "./group.controller.js"; +import { GroupService } from "./group.service.js"; +import { + createGroupBodySchema, + groupInviteBodySchema, + groupJidQuerySchema, + instanceNameParamsSchema, + inviteCodeQuerySchema, + toggleEphemeralBodySchema, + updateDescriptionBodySchema, + updateMembersBodySchema, + updatePictureBodySchema, + updateSettingBodySchema, + updateSubjectBodySchema, +} from "./group.validators.js"; + +export const registerGroupRoutes = async (app: any, manager: InstanceManager) => { + const controller = new GroupController(new GroupService(manager)); + + const post = ( + path: string, + summary: string, + body: object, + handler: any, + ) => app.post(path, { + schema: createRouteSchema({ + tags: ["Group"], + summary, + params: instanceNameParamsSchema, + body: body as never, + }), + }, handler); + + const get = ( + path: string, + summary: string, + querystring: object | undefined, + handler: any, + ) => app.get(path, { + schema: createRouteSchema({ + tags: ["Group"], + summary, + params: instanceNameParamsSchema, + ...(querystring ? { querystring: querystring as never } : {}), + }), + }, handler); + + post("/group/create/:instanceName", "Create group", createGroupBodySchema, controller.create); + post("/group/updatePicture/:instanceName", "Update group picture", updatePictureBodySchema, controller.updatePicture); + post("/group/updateSubject/:instanceName", "Update group subject", updateSubjectBodySchema, controller.updateSubject); + post("/group/updateDescription/:instanceName", "Update group description", updateDescriptionBodySchema, controller.updateDescription); + get("/group/inviteCode/:instanceName", "Fetch group invite code", groupJidQuerySchema, controller.inviteCode); + post("/group/revokeInviteCode/:instanceName", "Revoke group invite code", groupJidQuerySchema, controller.revokeInviteCode); + post("/group/sendInvite/:instanceName", "Send group invite", groupInviteBodySchema, controller.sendInvite); + get("/group/findByInviteCode/:instanceName", "Find group by invite code", inviteCodeQuerySchema, controller.findByInviteCode); + get("/group/findByJid/:instanceName", "Find group by JID", groupJidQuerySchema, controller.findByJid); + get("/group/fetchAll/:instanceName", "Fetch all groups", undefined, controller.fetchAll); + get("/group/members/:instanceName", "Fetch group members", groupJidQuerySchema, controller.members); + post("/group/updateMembers/:instanceName", "Update group members", updateMembersBodySchema, controller.updateMembers); + post("/group/updateSetting/:instanceName", "Update group setting", updateSettingBodySchema, controller.updateSetting); + post("/group/toggleEphemeral/:instanceName", "Toggle group ephemeral setting", toggleEphemeralBodySchema, controller.toggleEphemeral); + app.delete("/group/leave/:instanceName", { + schema: createRouteSchema({ + tags: ["Group"], + summary: "Leave group", + params: instanceNameParamsSchema, + body: groupJidQuerySchema, + }), + }, controller.leave); +}; diff --git a/packages/berryapi/src/modules/group/group.service.ts b/packages/berryapi/src/modules/group/group.service.ts new file mode 100644 index 0000000..fac479f --- /dev/null +++ b/packages/berryapi/src/modules/group/group.service.ts @@ -0,0 +1,83 @@ +import { InstanceManager } from "../../managers/InstanceManager.js"; +import { notImplementedYet } from "../shared/todo.js"; + +type GroupRecord = { + id?: string; + subject?: string; + participants?: unknown[]; + [key: string]: unknown; +}; + +const findGroup = async (manager: InstanceManager, instanceName: string, jid: string): Promise => { + const groups = await manager.fetchGroups(instanceName) as GroupRecord[]; + return groups.find((group) => group.id === jid) ?? null; +}; + +export class GroupService { + constructor(private readonly manager: InstanceManager) {} + + async create(_instanceName: string, _input: { subject: string; participants: string[] }) { + notImplementedYet("group creation is not exposed by BerryProtocol yet."); + } + + async updatePicture(_instanceName: string, _input: { jid: string; url?: string; path?: string; base64?: string }) { + notImplementedYet("group picture update is not exposed by BerryProtocol yet."); + } + + async updateSubject(_instanceName: string, _input: { jid: string; subject: string }) { + notImplementedYet("group subject update is not exposed by BerryProtocol yet."); + } + + async updateDescription(_instanceName: string, _input: { jid: string; description?: string }) { + notImplementedYet("group description update is not exposed by BerryProtocol yet."); + } + + async inviteCode(_instanceName: string) { + notImplementedYet("group invite code lookup is not exposed by BerryProtocol yet."); + } + + async revokeInviteCode(_instanceName: string, _input: { jid: string }) { + notImplementedYet("group invite code revoke is not exposed by BerryProtocol yet."); + } + + async sendInvite(_instanceName: string, _input: { jid: string; inviteCode?: string; inviteExpiration?: number; groupName?: string; caption?: string }) { + notImplementedYet("group invite sending is not exposed by BerryProtocol yet."); + } + + async findByInviteCode(_instanceName: string, _input: { inviteCode: string }) { + notImplementedYet("find group by invite code is not exposed by BerryProtocol yet."); + } + + async findByJid(instanceName: string, input: { jid: string }) { + const group = await findGroup(this.manager, instanceName, input.jid); + return group ?? {}; + } + + async fetchAll(instanceName: string) { + return this.manager.fetchGroups(instanceName); + } + + async members(instanceName: string, input: { jid: string }) { + const group = await findGroup(this.manager, instanceName, input.jid); + return { + jid: input.jid, + participants: Array.isArray(group?.participants) ? group.participants : [], + }; + } + + async updateMembers(_instanceName: string, _input: { jid: string; participants: string[]; action: string }) { + notImplementedYet("group membership updates are not exposed by BerryProtocol yet."); + } + + async updateSetting(_instanceName: string, _input: { jid: string; setting: string }) { + notImplementedYet("group setting updates are not exposed by BerryProtocol yet."); + } + + async toggleEphemeral(_instanceName: string, _input: { jid: string; expiration: number }) { + notImplementedYet("group ephemeral updates are not exposed by BerryProtocol yet."); + } + + async leave(_instanceName: string, _input: { jid: string }) { + notImplementedYet("group leave is not exposed by BerryProtocol yet."); + } +} diff --git a/packages/berryapi/src/modules/group/group.validators.ts b/packages/berryapi/src/modules/group/group.validators.ts new file mode 100644 index 0000000..facd92a --- /dev/null +++ b/packages/berryapi/src/modules/group/group.validators.ts @@ -0,0 +1,59 @@ +import { z } from "zod"; + +export const instanceNameParamsSchema = z.object({ + instanceName: z.string().min(1), +}); + +export const groupJidQuerySchema = z.object({ + jid: z.string().min(1), +}); + +export const inviteCodeQuerySchema = z.object({ + inviteCode: z.string().min(1), +}); + +export const createGroupBodySchema = z.object({ + subject: z.string().min(1), + participants: z.array(z.string().min(1)).min(1), +}); + +export const updatePictureBodySchema = z.object({ + jid: z.string().min(1), + url: z.string().url().optional(), + path: z.string().optional(), + base64: z.string().optional(), +}).refine((value) => value.url || value.path || value.base64, "url, path or base64 is required."); + +export const updateSubjectBodySchema = z.object({ + jid: z.string().min(1), + subject: z.string().min(1), +}); + +export const updateDescriptionBodySchema = z.object({ + jid: z.string().min(1), + description: z.string().optional(), +}); + +export const groupInviteBodySchema = z.object({ + jid: z.string().min(1), + inviteCode: z.string().optional(), + inviteExpiration: z.number().int().positive().optional(), + groupName: z.string().optional(), + caption: z.string().optional(), +}); + +export const updateMembersBodySchema = z.object({ + jid: z.string().min(1), + participants: z.array(z.string().min(1)).min(1), + action: z.enum(["add", "remove", "promote", "demote", "approve", "reject"]), +}); + +export const updateSettingBodySchema = z.object({ + jid: z.string().min(1), + setting: z.enum(["announcement", "not_announcement", "locked", "unlocked"]), +}); + +export const toggleEphemeralBodySchema = z.object({ + jid: z.string().min(1), + expiration: z.number().int().nonnegative(), +}); diff --git a/packages/berryapi/src/modules/instance/instance.controller.ts b/packages/berryapi/src/modules/instance/instance.controller.ts new file mode 100644 index 0000000..1cd8939 --- /dev/null +++ b/packages/berryapi/src/modules/instance/instance.controller.ts @@ -0,0 +1,61 @@ +import type { FastifyReply, FastifyRequest } from "fastify"; +import { successResponse } from "../../utils/response.js"; +import { validateOrThrow } from "../../utils/zod.js"; +import { + createInstanceBodySchema, + instanceNameParamsSchema, + setPresenceBodySchema, +} from "./instance.validators.js"; +import type { InstanceService } from "./instance.service.js"; + +export class InstanceController { + constructor(private readonly service: InstanceService) {} + + create = async (request: FastifyRequest, reply: FastifyReply) => { + const body = validateOrThrow(createInstanceBodySchema, request.body); + const data = await this.service.create(body); + reply.status(201).send(successResponse("Instance created successfully.", data)); + }; + + fetch = async (_request: FastifyRequest, reply: FastifyReply) => { + const data = await this.service.fetch(); + reply.send(successResponse("Instances fetched successfully.", data)); + }; + + connect = async (request: FastifyRequest, reply: FastifyReply) => { + const params = validateOrThrow(instanceNameParamsSchema, request.params); + const data = await this.service.connect(params.instanceName); + reply.send(successResponse("Instance connection started.", data)); + }; + + restart = async (request: FastifyRequest, reply: FastifyReply) => { + const params = validateOrThrow(instanceNameParamsSchema, request.params); + const data = await this.service.restart(params.instanceName); + reply.send(successResponse("Instance restart started.", data)); + }; + + connectionState = async (request: FastifyRequest, reply: FastifyReply) => { + const params = validateOrThrow(instanceNameParamsSchema, request.params); + const data = await this.service.connectionState(params.instanceName); + reply.send(successResponse("Connection state fetched successfully.", data)); + }; + + logout = async (request: FastifyRequest, reply: FastifyReply) => { + const params = validateOrThrow(instanceNameParamsSchema, request.params); + const data = await this.service.logout(params.instanceName); + reply.send(successResponse("Instance logged out successfully.", data)); + }; + + delete = async (request: FastifyRequest, reply: FastifyReply) => { + const params = validateOrThrow(instanceNameParamsSchema, request.params); + const data = await this.service.delete(params.instanceName); + reply.send(successResponse("Instance deleted successfully.", data)); + }; + + setPresence = async (request: FastifyRequest, reply: FastifyReply) => { + const params = validateOrThrow(instanceNameParamsSchema, request.params); + const body = validateOrThrow(setPresenceBodySchema, request.body); + await this.service.setPresence(params.instanceName, body); + reply.send(successResponse("Presence updated successfully.", { instanceName: params.instanceName })); + }; +} diff --git a/packages/berryapi/src/modules/instance/instance.routes.ts b/packages/berryapi/src/modules/instance/instance.routes.ts new file mode 100644 index 0000000..8fe1377 --- /dev/null +++ b/packages/berryapi/src/modules/instance/instance.routes.ts @@ -0,0 +1,90 @@ +import type { FastifyInstance } from "fastify"; +import type { InstanceManager } from "../../managers/InstanceManager.js"; +import { InstanceService } from "./instance.service.js"; +import { InstanceController } from "./instance.controller.js"; +import { createRouteSchema } from "../../docs/schema.js"; +import { + createInstanceBodySchema, + instanceNameParamsSchema, + setPresenceBodySchema, +} from "./instance.validators.js"; + +export const registerInstanceRoutes = async ( + app: any, + manager: InstanceManager, +) => { + const controller = new InstanceController(new InstanceService(manager)); + + app.post( + "/instance/create", + { + schema: createRouteSchema({ + tags: ["Instances"], + summary: "Create instance", + body: createInstanceBodySchema, + example: { + instanceName: "store-01", + authMethod: "qr", + settings: { rejectCall: true }, + }, + }), + }, + controller.create, + ); + + app.get("/instance/fetch", { + schema: createRouteSchema({ + tags: ["Instances"], + summary: "Fetch instances", + }), + }, controller.fetch); + + app.get("/instance/connect/:instanceName", { + schema: createRouteSchema({ + tags: ["Instances"], + summary: "Connect instance", + params: instanceNameParamsSchema, + }), + }, controller.connect); + + app.put("/instance/restart/:instanceName", { + schema: createRouteSchema({ + tags: ["Instances"], + summary: "Restart instance", + params: instanceNameParamsSchema, + }), + }, controller.restart); + + app.get("/instance/connectionState/:instanceName", { + schema: createRouteSchema({ + tags: ["Instances"], + summary: "Get connection state", + params: instanceNameParamsSchema, + }), + }, controller.connectionState); + + app.delete("/instance/logout/:instanceName", { + schema: createRouteSchema({ + tags: ["Instances"], + summary: "Logout instance", + params: instanceNameParamsSchema, + }), + }, controller.logout); + + app.delete("/instance/delete/:instanceName", { + schema: createRouteSchema({ + tags: ["Instances"], + summary: "Delete instance", + params: instanceNameParamsSchema, + }), + }, controller.delete); + + app.post("/instance/setPresence/:instanceName", { + schema: createRouteSchema({ + tags: ["Instances"], + summary: "Set instance presence", + params: instanceNameParamsSchema, + body: setPresenceBodySchema, + }), + }, controller.setPresence); +}; diff --git a/packages/berryapi/src/modules/instance/instance.service.ts b/packages/berryapi/src/modules/instance/instance.service.ts new file mode 100644 index 0000000..93567ec --- /dev/null +++ b/packages/berryapi/src/modules/instance/instance.service.ts @@ -0,0 +1,44 @@ +import type { InstanceManager } from "../../managers/InstanceManager.js"; +import type { z } from "zod"; +import type { + createInstanceBodySchema, + instanceNameParamsSchema, + setPresenceBodySchema, +} from "./instance.validators.js"; + +export class InstanceService { + constructor(private readonly manager: InstanceManager) {} + + create(input: z.infer) { + return this.manager.createInstance(input); + } + + fetch() { + return this.manager.fetchInstances(); + } + + connect(instanceName: z.infer["instanceName"]) { + return this.manager.connectInstance(instanceName); + } + + restart(instanceName: string) { + return this.manager.restartInstance(instanceName); + } + + connectionState(instanceName: string) { + return this.manager.getConnectionState(instanceName); + } + + logout(instanceName: string) { + return this.manager.logoutInstance(instanceName); + } + + async delete(instanceName: string) { + await this.manager.deleteInstance(instanceName); + return { instanceName }; + } + + setPresence(instanceName: string, input: z.infer) { + return this.manager.setPresence(instanceName, input.presence, input.jid); + } +} diff --git a/packages/berryapi/src/modules/instance/instance.validators.ts b/packages/berryapi/src/modules/instance/instance.validators.ts new file mode 100644 index 0000000..6f1efdb --- /dev/null +++ b/packages/berryapi/src/modules/instance/instance.validators.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; + +const eventNameSchema = z.enum([ + "connection.update", + "qrcode.updated", + "messages.upsert", + "messages.update", + "messages.delete", + "chats.update", + "contacts.update", + "groups.update", + "presence.update", + "send.message", +]); + +export const instanceNameParamsSchema = z.object({ + instanceName: z.string().min(1), +}); + +export const createInstanceBodySchema = z.object({ + instanceName: z.string().min(1), + authMethod: z.enum(["qr", "pairing_code", "link"]).default("qr"), + phoneNumber: z.string().optional(), + webhook: z + .object({ + url: z.string().url().optional(), + events: z.array(eventNameSchema).optional(), + }) + .optional(), + settings: z + .object({ + rejectCall: z.boolean().optional(), + readMessages: z.boolean().optional(), + syncFullHistory: z.boolean().optional(), + }) + .optional(), +}); + +export const setPresenceBodySchema = z.object({ + presence: z.enum(["available", "composing", "recording", "paused", "unavailable"]), + jid: z.string().optional(), +}); diff --git a/packages/berryapi/src/modules/message/builders/MessageBuilder.ts b/packages/berryapi/src/modules/message/builders/MessageBuilder.ts new file mode 100644 index 0000000..da5e393 --- /dev/null +++ b/packages/berryapi/src/modules/message/builders/MessageBuilder.ts @@ -0,0 +1,373 @@ +import { readFile } from "node:fs/promises"; +import type { ProviderSendOptions } from "../../../providers/whatsapp/WhatsAppProvider.js"; +import type { CommonMessageInput, MediaLikeInput } from "../types/index.js"; + +type BuiltMessage = { + content: Record; + options?: ProviderSendOptions; +}; + +const decodeBase64 = async (base64: string): Promise => + Buffer.from(base64, "base64"); + +const mediaToTransportValue = async (input: MediaLikeInput): Promise => { + if (input.base64) { + return decodeBase64(input.base64); + } + + if (input.path) { + return readFile(input.path); + } + + if (input.url) { + return { url: input.url }; + } + + throw new Error("A media payload needs url, path or base64."); +}; + +export class MessageBuilder { + buildCommonOptions(input: Partial): ProviderSendOptions | undefined { + const contextInfo = { + ...(input.contextInfo ?? {}), + ...(typeof input.forwardingScore === "number" + ? { + forwardingScore: input.forwardingScore, + isForwarded: input.forwardingScore > 0, + } + : {}), + }; + + const options: ProviderSendOptions = { + ...(input.quoted ? { quoted: input.quoted } : {}), + ...(input.mentions ? { mentions: input.mentions } : {}), + ...(Object.keys(contextInfo).length ? { contextInfo } : {}), + ...(input.ephemeralExpiration ? { ephemeralExpiration: input.ephemeralExpiration } : {}), + ...(input.statusJidList ? { statusJidList: input.statusJidList } : {}), + }; + + return Object.keys(options).length ? options : undefined; + } + + buildText(input: CommonMessageInput & { text: string }): BuiltMessage { + return { + content: { + text: input.text, + }, + options: this.buildCommonOptions(input), + }; + } + + async buildImage(input: CommonMessageInput & MediaLikeInput): Promise { + return { + content: { + image: await mediaToTransportValue(input), + ...(input.caption ? { caption: input.caption } : {}), + ...(input.mimetype ? { mimetype: input.mimetype } : {}), + ...(input.fileName ? { fileName: input.fileName } : {}), + }, + options: this.buildCommonOptions(input), + }; + } + + async buildVideo(input: CommonMessageInput & MediaLikeInput & { gifPlayback?: boolean; viewOnce?: boolean }): Promise { + return { + content: { + video: await mediaToTransportValue(input), + ...(input.caption ? { caption: input.caption } : {}), + ...(input.mimetype ? { mimetype: input.mimetype } : {}), + ...(input.fileName ? { fileName: input.fileName } : {}), + ...(input.gifPlayback ? { gifPlayback: true } : {}), + ...(input.viewOnce ? { viewOnce: true } : {}), + }, + options: this.buildCommonOptions(input), + }; + } + + async buildAudio(input: CommonMessageInput & MediaLikeInput & { ptt?: boolean }): Promise { + return { + content: { + audio: await mediaToTransportValue(input), + ...(input.mimetype ? { mimetype: input.mimetype } : {}), + ...(input.ptt ? { ptt: true } : {}), + }, + options: this.buildCommonOptions(input), + }; + } + + async buildDocument(input: CommonMessageInput & MediaLikeInput): Promise { + return { + content: { + document: await mediaToTransportValue(input), + ...(input.caption ? { caption: input.caption } : {}), + ...(input.mimetype ? { mimetype: input.mimetype } : {}), + ...(input.fileName ? { fileName: input.fileName } : {}), + }, + options: this.buildCommonOptions(input), + }; + } + + async buildSticker(input: CommonMessageInput & MediaLikeInput): Promise { + return { + content: { + sticker: await mediaToTransportValue(input), + ...(input.mimetype ? { mimetype: input.mimetype } : {}), + }, + options: this.buildCommonOptions(input), + }; + } + + buildPoll(input: CommonMessageInput & { title: string; options: string[]; selectableCount: number }): BuiltMessage { + return { + content: { + poll: { + name: input.title, + values: input.options, + selectableCount: input.selectableCount, + }, + }, + options: this.buildCommonOptions(input), + }; + } + + buildLocation(input: CommonMessageInput & { latitude: number; longitude: number; name?: string; address?: string }): BuiltMessage { + return { + content: { + location: { + degreesLatitude: input.latitude, + degreesLongitude: input.longitude, + name: input.name, + address: input.address, + }, + }, + options: this.buildCommonOptions(input), + }; + } + + buildLiveLocation(input: CommonMessageInput & { latitude: number; longitude: number; name?: string; address?: string; speedInMps?: number; accuracyInMeters?: number; degreesClockwiseFromMagneticNorth?: number }): BuiltMessage { + return { + content: { + liveLocationMessage: { + degreesLatitude: input.latitude, + degreesLongitude: input.longitude, + name: input.name, + address: input.address, + speedInMps: input.speedInMps, + accuracyInMeters: input.accuracyInMeters, + degreesClockwiseFromMagneticNorth: input.degreesClockwiseFromMagneticNorth, + }, + }, + options: this.buildCommonOptions(input), + }; + } + + buildContact(input: CommonMessageInput & { displayName: string; vcard: string }): BuiltMessage { + return { + content: { + contacts: { + displayName: input.displayName, + contacts: [ + { + displayName: input.displayName, + vcard: input.vcard, + }, + ], + }, + }, + options: this.buildCommonOptions(input), + }; + } + + buildContacts(input: CommonMessageInput & { displayName?: string; contacts: Array<{ displayName: string; vcard: string }> }): BuiltMessage { + return { + content: { + contacts: { + displayName: input.displayName ?? input.contacts[0]?.displayName ?? "Contacts", + contacts: input.contacts, + }, + }, + options: this.buildCommonOptions(input), + }; + } + + buildButtons(input: CommonMessageInput & { text: string; footer?: string; ai?: boolean; buttons: Array<{ id?: string; title: string; type?: string; url?: string; copyCode?: string; paramsJson?: string }> }): Record { + return { + text: input.text, + footer: input.footer, + buttons: input.buttons.map((button, index) => ({ + id: button.id ?? `btn_${index + 1}`, + title: button.title, + kind: + button.type === "cta_url" + ? "cta_url" + : button.type === "cta_copy" + ? "copy_code" + : button.type === "native_flow" + ? "reply" + : "quick_reply", + ...(button.url ? { url: button.url } : {}), + ...(button.copyCode ? { code: button.copyCode } : {}), + ...(button.type === "native_flow" ? { nativeFlowName: "quick_reply", buttonParamsJson: button.paramsJson } : {}), + })), + ...(input.ai ? { ai: true } : {}), + }; + } + + buildList(input: CommonMessageInput & { title?: string; text: string; footer?: string; buttonText: string; sections: Array<{ title: string; rows: Array<{ id: string; title: string; description?: string }> }> }): Record { + return { + title: input.title, + text: input.text, + footer: input.footer, + buttonText: input.buttonText, + sections: input.sections, + }; + } + + buildCarousel(input: CommonMessageInput & { + text: string; + footer?: string; + carouselCardType?: "image" | "video" | "mixed"; + ai?: boolean; + cards: Array<{ + title?: string; + body?: string; + footer?: string; + image?: MediaLikeInput; + video?: MediaLikeInput; + buttons?: Array<{ + id?: string; + title?: string; + kind?: "reply" | "quick_reply" | "copy_code" | "cta_url"; + code?: string; + url?: string; + nativeFlowName?: string; + buttonParamsJson?: string; + }>; + }>; + }): Record { + return { + text: input.text, + footer: input.footer, + carouselCardType: input.carouselCardType, + ai: input.ai, + cards: input.cards.map((card) => ({ + title: card.title, + body: card.body, + footer: card.footer, + image: card.image + ? { + url: card.image.url, + path: card.image.path, + buffer: card.image.base64 ? Buffer.from(card.image.base64, "base64") : undefined, + caption: card.image.caption, + mimetype: card.image.mimetype, + fileName: card.image.fileName, + } + : undefined, + video: card.video + ? { + url: card.video.url, + path: card.video.path, + buffer: card.video.base64 ? Buffer.from(card.video.base64, "base64") : undefined, + caption: card.video.caption, + mimetype: card.video.mimetype, + fileName: card.video.fileName, + } + : undefined, + buttons: card.buttons, + })), + }; + } + + buildReaction(input: { emoji: string; targetMessageId: string; to: string }) { + return { + emoji: input.emoji, + targetMessageId: input.targetMessageId, + to: input.to, + }; + } + + buildEdit(input: { text: string; messageId: string; to: string }) { + return input; + } + + buildDelete(input: CommonMessageInput & { messageId: string; fromMe: boolean; participant?: string }): BuiltMessage { + return { + content: { + delete: { + remoteJid: input.to, + fromMe: input.fromMe, + id: input.messageId, + ...(input.participant ? { participant: input.participant } : {}), + }, + }, + options: this.buildCommonOptions(input), + }; + } + + buildForward(input: CommonMessageInput & { message: Record }): BuiltMessage { + return { + content: { + forward: input.message, + }, + options: this.buildCommonOptions(input), + }; + } + + buildStatus(input: CommonMessageInput & { text?: string; media?: MediaLikeInput }): Promise | BuiltMessage { + if (input.media) { + return this.buildImage({ + ...input, + ...input.media, + statusJidList: input.statusJidList, + }); + } + + return this.buildText({ + ...input, + text: input.text ?? "", + }); + } + + async buildProduct(input: CommonMessageInput & { title: string; description?: string; price?: string; retailerId?: string; productImage?: MediaLikeInput }): Promise { + return { + content: { + productMessage: { + title: input.title, + description: input.description, + priceAmount1000: input.price, + retailerId: input.retailerId, + productImage: input.productImage + ? { image: await mediaToTransportValue(input.productImage) } + : undefined, + }, + }, + options: this.buildCommonOptions(input), + }; + } + + buildCatalog(input: CommonMessageInput & { title: string; businessOwnerJid?: string }): BuiltMessage { + return { + content: { + catalogMessage: { + title: input.title, + businessOwnerJid: input.businessOwnerJid, + }, + }, + options: this.buildCommonOptions(input), + }; + } + + buildCollection(input: CommonMessageInput & { title: string; businessOwnerJid?: string; collectionId?: string }): BuiltMessage { + return { + content: { + collectionMessage: { + name: input.title, + businessOwnerJid: input.businessOwnerJid, + collectionId: input.collectionId, + }, + }, + options: this.buildCommonOptions(input), + }; + } +} diff --git a/packages/berryapi/src/modules/message/controllers/message.controller.ts b/packages/berryapi/src/modules/message/controllers/message.controller.ts new file mode 100644 index 0000000..1a6e58e --- /dev/null +++ b/packages/berryapi/src/modules/message/controllers/message.controller.ts @@ -0,0 +1,146 @@ +import type { FastifyReply, FastifyRequest } from "fastify"; +import type { ZodTypeAny } from "zod"; +import { successResponse } from "../../../utils/response.js"; +import { validateOrThrow } from "../../../utils/zod.js"; +import { + deleteMessageBodySchema, + editMessageBodySchema, + instanceNameParamsSchema, + sendAiTextBodySchema, + sendButtonsBodySchema, + sendContactBodySchema, + sendContactsBodySchema, + sendCopyButtonBodySchema, + sendCarouselBodySchema, + sendForwardBodySchema, + sendListBodySchema, + sendLocationBodySchema, + sendMediaBodySchema, + sendPollBodySchema, + sendProductBodySchema, + sendReactionBodySchema, + sendStatusBodySchema, + sendTemplateButtonsBodySchema, + sendTextBodySchema, + sendCollectionBodySchema, + sendCatalogBodySchema, + sendLiveLocationBodySchema, +} from "../validators/message.validators.js"; +import type { MessageService } from "../services/message.service.js"; + +export class MessageController { + constructor(private readonly service: MessageService) {} + + private getInstanceName(request: FastifyRequest): string { + return validateOrThrow(instanceNameParamsSchema, request.params).instanceName; + } + + private async run( + request: FastifyRequest, + reply: FastifyReply, + schema: ZodTypeAny, + message: string, + executor: (instanceName: string, input: any) => Promise, + ) { + const instanceName = this.getInstanceName(request); + const body = validateOrThrow(schema, request.body) as Record; + const data = await executor(instanceName, body); + reply.send(successResponse(message, data)); + } + + sendText = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendTextBodySchema, "Text message sent successfully.", this.service.sendText.bind(this.service)); + + sendExtendedText = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendTextBodySchema, "Extended text message sent successfully.", this.service.sendExtendedText.bind(this.service)); + + sendReply = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendTextBodySchema, "Reply message sent successfully.", this.service.sendReply.bind(this.service)); + + sendForward = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendForwardBodySchema as never, "Forward message sent successfully.", this.service.sendForward.bind(this.service)); + + deleteMessage = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, deleteMessageBodySchema as never, "Delete message sent successfully.", this.service.deleteMessage.bind(this.service)); + + editMessage = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, editMessageBodySchema as never, "Message edited successfully.", this.service.editMessage.bind(this.service)); + + sendReaction = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendReactionBodySchema as never, "Reaction sent successfully.", this.service.sendReaction.bind(this.service)); + + sendImage = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendMediaBodySchema as never, "Image message sent successfully.", this.service.sendImage.bind(this.service)); + + sendVideo = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendMediaBodySchema as never, "Video message sent successfully.", this.service.sendVideo.bind(this.service)); + + sendAudio = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendMediaBodySchema as never, "Audio message sent successfully.", this.service.sendAudio.bind(this.service)); + + sendDocument = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendMediaBodySchema as never, "Document message sent successfully.", this.service.sendDocument.bind(this.service)); + + sendSticker = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendMediaBodySchema as never, "Sticker message sent successfully.", this.service.sendSticker.bind(this.service)); + + sendGif = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendMediaBodySchema as never, "GIF message sent successfully.", this.service.sendGif.bind(this.service)); + + sendButtons = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendButtonsBodySchema as never, "Buttons message sent successfully.", this.service.sendButtons.bind(this.service)); + + sendTemplateButtons = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendTemplateButtonsBodySchema as never, "Template buttons message sent successfully.", this.service.sendTemplateButtons.bind(this.service)); + + sendCTAButton = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendButtonsBodySchema as never, "CTA button message sent successfully.", this.service.sendCTAButton.bind(this.service)); + + sendCopyButton = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendCopyButtonBodySchema as never, "Copy button message sent successfully.", this.service.sendCopyButton.bind(this.service)); + + sendList = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendListBodySchema as never, "List message sent successfully.", this.service.sendList.bind(this.service)); + + sendCarousel = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendCarouselBodySchema as never, "Carousel message sent successfully.", this.service.sendCarousel.bind(this.service)); + + sendAiText = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendAiTextBodySchema as never, "AI labeled text message sent successfully.", this.service.sendAiText.bind(this.service)); + + sendAiCarousel = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendCarouselBodySchema as never, "AI labeled carousel message sent successfully.", this.service.sendAiCarousel.bind(this.service)); + + sendPoll = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendPollBodySchema as never, "Poll message sent successfully.", this.service.sendPoll.bind(this.service)); + + sendLocation = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendLocationBodySchema as never, "Location message sent successfully.", this.service.sendLocation.bind(this.service)); + + sendLiveLocation = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendLiveLocationBodySchema as never, "Live location message sent successfully.", this.service.sendLiveLocation.bind(this.service)); + + sendContact = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendContactBodySchema as never, "Contact message sent successfully.", this.service.sendContact.bind(this.service)); + + sendContacts = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendContactsBodySchema as never, "Contacts message sent successfully.", this.service.sendContacts.bind(this.service)); + + sendStatus = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendStatusBodySchema as never, "Status message sent successfully.", this.service.sendStatus.bind(this.service)); + + sendViewOnceImage = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendMediaBodySchema as never, "View once image sent successfully.", this.service.sendViewOnceImage.bind(this.service)); + + sendViewOnceVideo = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendMediaBodySchema as never, "View once video sent successfully.", this.service.sendViewOnceVideo.bind(this.service)); + + sendProduct = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendProductBodySchema as never, "Product message sent successfully.", this.service.sendProduct.bind(this.service)); + + sendCatalog = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendCatalogBodySchema as never, "Catalog message sent successfully.", this.service.sendCatalog.bind(this.service)); + + sendCollection = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, sendCollectionBodySchema as never, "Collection message sent successfully.", this.service.sendCollection.bind(this.service)); +} diff --git a/packages/berryapi/src/modules/message/examples/payload-examples.ts b/packages/berryapi/src/modules/message/examples/payload-examples.ts new file mode 100644 index 0000000..3a55d82 --- /dev/null +++ b/packages/berryapi/src/modules/message/examples/payload-examples.ts @@ -0,0 +1,103 @@ +export const createInstanceExample = { + instanceName: "store-01", + authMethod: "qr", +}; + +export const setWebhookExample = { + enabled: true, + url: "https://example.com/berryapi/webhook", + events: ["messages.upsert", "connection.update"], +}; + +export const sendTextExample = { + to: "5511999999999@s.whatsapp.net", + text: "Ola! Sua instancia BerryAPI esta funcionando.", +}; + +export const sendMediaExample = { + to: "5511999999999@s.whatsapp.net", + url: "https://example.com/image.png", + caption: "Imagem enviada pela BerryAPI", + mimetype: "image/png", +}; + +export const sendButtonsExample = { + to: "5511999999999@s.whatsapp.net", + text: "Escolha uma opcao", + footer: "BerryAPI", + buttons: [ + { id: "support", title: "Suporte" }, + { id: "sales", title: "Vendas" }, + ], +}; + +export const sendTemplateButtonsExample = { + to: "5511999999999@s.whatsapp.net", + text: "Escolha uma acao", + footer: "BerryAPI template", + buttons: [ + { id: "support", title: "Suporte", type: "reply" }, + { id: "catalog", title: "Catalogo", type: "cta_url", url: "https://berrysdk.com" }, + { id: "otp", title: "Copiar codigo", type: "cta_copy", copyCode: "051272" }, + ], +}; + +export const sendCopyButtonExample = { + to: "5511999999999@s.whatsapp.net", + text: "Use o codigo abaixo para entrar na sua conta.", + footer: "BerryOTP", + buttonText: "Copiar codigo", + buttonId: "login_code", + copyCode: "051272", +}; + +export const sendListExample = { + to: "5511999999999@s.whatsapp.net", + title: "Menu BerryAPI", + text: "Escolha uma opcao", + buttonText: "Abrir menu", + sections: [ + { + title: "Atendimento", + rows: [ + { id: "support", title: "Suporte", description: "Falar com suporte" }, + ], + }, + ], +}; + +export const sendPollExample = { + to: "5511999999999@s.whatsapp.net", + title: "Qual sabor voce prefere?", + options: ["Calabresa", "Frango", "Portuguesa"], + selectableCount: 1, +}; + +export const sendCarouselExample = { + to: "5511999999999@s.whatsapp.net", + text: "Confira nossas pizzas!", + footer: "BerryAPI carousel", + carouselCardType: "mixed", + cards: [ + { + title: "Pizza Calabresa", + body: "Calabresa premium", + footer: "R$ 39,90", + image: { + url: "https://images.unsplash.com/photo-1513104890138-7c749659a591", + }, + buttons: [ + { + id: "pizza_calabresa", + title: "Escolher", + kind: "quick_reply", + }, + ], + }, + ], +}; + +export const sendAiTextExample = { + to: "5511999999999@s.whatsapp.net", + text: "Ola! Essa mensagem deve aparecer com AI label em chat privado.", +}; diff --git a/packages/berryapi/src/modules/message/routes/message.routes.ts b/packages/berryapi/src/modules/message/routes/message.routes.ts new file mode 100644 index 0000000..857d212 --- /dev/null +++ b/packages/berryapi/src/modules/message/routes/message.routes.ts @@ -0,0 +1,103 @@ +import type { FastifyInstance } from "fastify"; +import type { InstanceManager } from "../../../managers/InstanceManager.js"; +import { createRouteSchema } from "../../../docs/schema.js"; +import { + sendAiTextExample, + sendButtonsExample, + sendCarouselExample, + sendCopyButtonExample, + sendListExample, + sendMediaExample, + sendPollExample, + sendTemplateButtonsExample, + sendTextExample, +} from "../examples/payload-examples.js"; +import { MessageService } from "../services/message.service.js"; +import { MessageController } from "../controllers/message.controller.js"; +import { + deleteMessageBodySchema, + editMessageBodySchema, + instanceNameParamsSchema, + sendAiTextBodySchema, + sendButtonsBodySchema, + sendCatalogBodySchema, + sendCarouselBodySchema, + sendCollectionBodySchema, + sendContactBodySchema, + sendContactsBodySchema, + sendCopyButtonBodySchema, + sendForwardBodySchema, + sendListBodySchema, + sendLiveLocationBodySchema, + sendLocationBodySchema, + sendMediaBodySchema, + sendPollBodySchema, + sendProductBodySchema, + sendReactionBodySchema, + sendStatusBodySchema, + sendTemplateButtonsBodySchema, + sendTextBodySchema, +} from "../validators/message.validators.js"; + +export const registerMessageRoutes = async ( + app: any, + manager: InstanceManager, +) => { + const controller = new MessageController(new MessageService(manager)); + + const post = ( + path: string, + summary: string, + body: object, + handler: any, + example?: unknown, + ) => + app.post( + path, + { + schema: createRouteSchema({ + tags: ["Message"], + summary, + params: instanceNameParamsSchema, + body: body as never, + example, + }), + }, + handler, + ); + + post("/message/sendText/:instanceName", "Send text message", sendTextBodySchema, controller.sendText, sendTextExample); + post("/message/sendExtendedText/:instanceName", "Send extended text message", sendTextBodySchema, controller.sendExtendedText, sendTextExample); + post("/message/sendReply/:instanceName", "Send reply message", sendTextBodySchema, controller.sendReply, { ...sendTextExample, quoted: { remoteJid: "5511999999999@s.whatsapp.net", id: "MSG_ID" } }); + post("/message/sendForward/:instanceName", "Send forward message", sendForwardBodySchema, controller.sendForward); + post("/message/delete/:instanceName", "Delete message", deleteMessageBodySchema, controller.deleteMessage); + post("/message/edit/:instanceName", "Edit message", editMessageBodySchema, controller.editMessage); + post("/message/sendReaction/:instanceName", "Send reaction", sendReactionBodySchema, controller.sendReaction); + post("/message/sendImage/:instanceName", "Send image", sendMediaBodySchema, controller.sendImage, sendMediaExample); + post("/message/sendVideo/:instanceName", "Send video", sendMediaBodySchema, controller.sendVideo); + post("/message/sendAudio/:instanceName", "Send audio", sendMediaBodySchema, controller.sendAudio); + post("/message/sendWhatsAppAudio/:instanceName", "Send WhatsApp audio", sendMediaBodySchema, controller.sendAudio); + post("/message/sendDocument/:instanceName", "Send document", sendMediaBodySchema, controller.sendDocument); + post("/message/sendSticker/:instanceName", "Send sticker", sendMediaBodySchema, controller.sendSticker); + post("/message/sendGif/:instanceName", "Send GIF", sendMediaBodySchema, controller.sendGif); + post("/message/sendMedia/:instanceName", "Send media alias", sendMediaBodySchema, controller.sendImage, sendMediaExample); + post("/message/sendButtons/:instanceName", "Send buttons", sendButtonsBodySchema, controller.sendButtons, sendButtonsExample); + post("/message/sendTemplateButtons/:instanceName", "Send template buttons", sendTemplateButtonsBodySchema, controller.sendTemplateButtons, sendTemplateButtonsExample); + post("/message/sendCTAButton/:instanceName", "Send CTA button", sendButtonsBodySchema, controller.sendCTAButton, sendButtonsExample); + post("/message/sendCopyButton/:instanceName", "Send copy button", sendCopyButtonBodySchema, controller.sendCopyButton, sendCopyButtonExample); + post("/message/sendList/:instanceName", "Send list", sendListBodySchema, controller.sendList, sendListExample); + post("/message/sendCarousel/:instanceName", "Send carousel", sendCarouselBodySchema, controller.sendCarousel, sendCarouselExample); + post("/message/sendAiText/:instanceName", "Send AI labeled text", sendAiTextBodySchema, controller.sendAiText, sendAiTextExample); + post("/message/sendAiCarousel/:instanceName", "Send AI labeled carousel", sendCarouselBodySchema, controller.sendAiCarousel, { ...sendCarouselExample, ai: true }); + post("/message/sendPoll/:instanceName", "Send poll", sendPollBodySchema, controller.sendPoll, sendPollExample); + post("/message/sendLocation/:instanceName", "Send location", sendLocationBodySchema, controller.sendLocation); + post("/message/sendLiveLocation/:instanceName", "Send live location", sendLiveLocationBodySchema, controller.sendLiveLocation); + post("/message/sendContact/:instanceName", "Send contact", sendContactBodySchema, controller.sendContact); + post("/message/sendContacts/:instanceName", "Send contacts", sendContactsBodySchema, controller.sendContacts); + post("/message/sendStatus/:instanceName", "Send status", sendStatusBodySchema, controller.sendStatus); + post("/message/sendViewOnceImage/:instanceName", "Send view once image", sendMediaBodySchema, controller.sendViewOnceImage); + post("/message/sendViewOnceVideo/:instanceName", "Send view once video", sendMediaBodySchema, controller.sendViewOnceVideo); + post("/message/sendProduct/:instanceName", "Send product", sendProductBodySchema, controller.sendProduct); + post("/message/sendCatalog/:instanceName", "Send catalog", sendCatalogBodySchema, controller.sendCatalog); + post("/message/sendCollection/:instanceName", "Send collection", sendCollectionBodySchema, controller.sendCollection); +}; diff --git a/packages/berryapi/src/modules/message/services/message.service.ts b/packages/berryapi/src/modules/message/services/message.service.ts new file mode 100644 index 0000000..18e199e --- /dev/null +++ b/packages/berryapi/src/modules/message/services/message.service.ts @@ -0,0 +1,180 @@ +import type { InstanceManager } from "../../../managers/InstanceManager.js"; +import { MessageBuilder } from "../builders/MessageBuilder.js"; + +export class MessageService { + constructor( + private readonly manager: InstanceManager, + private readonly builder = new MessageBuilder(), + ) {} + + async sendText(instanceName: string, input: { to: string; text: string; [key: string]: unknown }) { + const built = this.builder.buildText(input); + return this.manager.sendRawMessage(instanceName, input.to, built.content, built.options); + } + + async sendExtendedText(instanceName: string, input: { to: string; text: string; [key: string]: unknown }) { + return this.sendText(instanceName, input); + } + + async sendReply(instanceName: string, input: { to: string; text: string; quoted: unknown; [key: string]: unknown }) { + const built = this.builder.buildText(input as never); + return this.manager.sendRawMessage(instanceName, input.to, built.content, built.options); + } + + async sendForward(instanceName: string, input: { to: string; message: Record; [key: string]: unknown }) { + const built = this.builder.buildForward(input); + return this.manager.sendRawMessage(instanceName, input.to, built.content, built.options); + } + + async deleteMessage(instanceName: string, input: { to: string; messageId: string; fromMe: boolean; participant?: string; [key: string]: unknown }) { + const built = this.builder.buildDelete(input); + return this.manager.sendRawMessage(instanceName, input.to, built.content, built.options); + } + + async editMessage(instanceName: string, input: { to: string; messageId: string; text: string }) { + return this.manager.editMessage(instanceName, input.to, input.messageId, input.text); + } + + async sendReaction(instanceName: string, input: { to: string; emoji: string; targetMessageId: string }) { + return this.manager.sendReaction(instanceName, input.to, input.emoji, input.targetMessageId); + } + + async sendImage(instanceName: string, input: Record) { + const built = await this.builder.buildImage(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendVideo(instanceName: string, input: Record) { + const built = await this.builder.buildVideo(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendAudio(instanceName: string, input: Record) { + const built = await this.builder.buildAudio(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendDocument(instanceName: string, input: Record) { + const built = await this.builder.buildDocument(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendSticker(instanceName: string, input: Record) { + const built = await this.builder.buildSticker(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendGif(instanceName: string, input: Record) { + const built = await this.builder.buildVideo({ ...input, gifPlayback: true } as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendViewOnceImage(instanceName: string, input: Record) { + const built = await this.builder.buildImage(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), { ...built.content, viewOnce: true }, built.options); + } + + async sendViewOnceVideo(instanceName: string, input: Record) { + const built = await this.builder.buildVideo({ ...input, viewOnce: true } as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendButtons(instanceName: string, input: Record) { + const payload = this.builder.buildButtons(input as never); + return this.manager.sendButtons(instanceName, String(input.to), payload); + } + + async sendTemplateButtons(instanceName: string, input: Record) { + const payload = this.builder.buildButtons(input as never); + return this.manager.sendButtons(instanceName, String(input.to), payload); + } + + async sendCTAButton(instanceName: string, input: Record) { + return this.sendButtons(instanceName, input); + } + + async sendCopyButton(instanceName: string, input: Record) { + const payload = this.builder.buildButtons({ + ...input, + buttons: [ + { + id: String(input.buttonId ?? "copy_button"), + title: String(input.buttonText ?? "Copy code"), + type: "cta_copy", + copyCode: String(input.copyCode ?? ""), + }, + ], + } as never); + return this.manager.sendButtons(instanceName, String(input.to), payload); + } + + async sendList(instanceName: string, input: Record) { + const payload = this.builder.buildList(input as never); + return this.manager.sendList(instanceName, String(input.to), payload); + } + + async sendCarousel(instanceName: string, input: Record) { + const payload = this.builder.buildCarousel(input as never); + return this.manager.sendCarousel(instanceName, String(input.to), payload); + } + + async sendAiText(instanceName: string, input: Record) { + return this.manager.sendRawMessage(instanceName, String(input.to), { + text: String(input.text), + ai: true, + }, this.builder.buildCommonOptions(input as never)); + } + + async sendAiCarousel(instanceName: string, input: Record) { + const payload = this.builder.buildCarousel({ + ...input, + ai: true, + } as never); + return this.manager.sendCarousel(instanceName, String(input.to), payload); + } + + async sendPoll(instanceName: string, input: Record) { + const built = this.builder.buildPoll(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendLocation(instanceName: string, input: Record) { + const built = this.builder.buildLocation(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendLiveLocation(instanceName: string, input: Record) { + const built = this.builder.buildLiveLocation(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendContact(instanceName: string, input: Record) { + const built = this.builder.buildContact(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendContacts(instanceName: string, input: Record) { + const built = this.builder.buildContacts(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendStatus(instanceName: string, input: Record) { + const built = await this.builder.buildStatus(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendProduct(instanceName: string, input: Record) { + const built = await this.builder.buildProduct(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendCatalog(instanceName: string, input: Record) { + const built = this.builder.buildCatalog(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } + + async sendCollection(instanceName: string, input: Record) { + const built = this.builder.buildCollection(input as never); + return this.manager.sendRawMessage(instanceName, String(input.to), built.content, built.options); + } +} diff --git a/packages/berryapi/src/modules/message/types/index.ts b/packages/berryapi/src/modules/message/types/index.ts new file mode 100644 index 0000000..1acaf57 --- /dev/null +++ b/packages/berryapi/src/modules/message/types/index.ts @@ -0,0 +1,23 @@ +export type MediaLikeInput = { + url?: string; + path?: string; + base64?: string; + mimetype?: string; + fileName?: string; + caption?: string; +}; + +export type CommonMessageInput = { + to: string; + quoted?: { + remoteJid: string; + id: string; + fromMe?: boolean; + participant?: string; + }; + mentions?: string[]; + contextInfo?: Record; + forwardingScore?: number; + ephemeralExpiration?: number; + statusJidList?: string[]; +}; diff --git a/packages/berryapi/src/modules/message/validators/message.validators.ts b/packages/berryapi/src/modules/message/validators/message.validators.ts new file mode 100644 index 0000000..8032c96 --- /dev/null +++ b/packages/berryapi/src/modules/message/validators/message.validators.ts @@ -0,0 +1,204 @@ +import { z } from "zod"; + +const mediaLikeSchema = z.object({ + url: z.string().url().optional(), + path: z.string().optional(), + base64: z.string().optional(), + mimetype: z.string().optional(), + fileName: z.string().optional(), + caption: z.string().optional(), +}); + +const commonMessageSchema = z.object({ + to: z.string().min(1), + quoted: z + .object({ + remoteJid: z.string(), + id: z.string(), + fromMe: z.boolean().optional(), + participant: z.string().optional(), + }) + .optional(), + mentions: z.array(z.string()).optional(), + contextInfo: z.record(z.string(), z.unknown()).optional(), + forwardingScore: z.number().int().nonnegative().optional(), + ephemeralExpiration: z.number().int().positive().optional(), + statusJidList: z.array(z.string()).optional(), +}); + +export const instanceNameParamsSchema = z.object({ + instanceName: z.string().min(1), +}); + +export const sendTextBodySchema = commonMessageSchema.extend({ + text: z.string().min(1), + linkPreview: z.boolean().optional(), +}); + +export const sendReactionBodySchema = commonMessageSchema.extend({ + emoji: z.string().min(1), + targetMessageId: z.string().min(1), +}); + +export const editMessageBodySchema = commonMessageSchema.extend({ + messageId: z.string().min(1), + text: z.string().min(1), +}); + +export const deleteMessageBodySchema = commonMessageSchema.extend({ + messageId: z.string().min(1), + fromMe: z.boolean().default(true), + participant: z.string().optional(), +}); + +export const sendMediaBodySchema = commonMessageSchema.extend(mediaLikeSchema.shape).refine( + (value) => !!value.url || !!value.path || !!value.base64, + "url, path or base64 is required.", +); + +export const sendButtonsBodySchema = commonMessageSchema.extend({ + text: z.string().min(1), + footer: z.string().optional(), + ai: z.boolean().optional(), + buttons: z.array( + z.object({ + id: z.string().min(1).optional(), + title: z.string().min(1), + type: z.enum(["reply", "cta_url", "cta_copy", "native_flow"]).default("reply"), + url: z.string().url().optional(), + copyCode: z.string().optional(), + paramsJson: z.string().optional(), + }), + ).min(1), +}); + +export const sendTemplateButtonsBodySchema = sendButtonsBodySchema.refine( + (value) => value.buttons.every((button) => button.type === "reply" || button.type === "cta_url" || button.type === "cta_copy"), + "Template buttons only support reply, cta_url or cta_copy.", +); + +export const sendCopyButtonBodySchema = commonMessageSchema.extend({ + text: z.string().min(1), + footer: z.string().optional(), + copyCode: z.string().min(1), + buttonText: z.string().min(1), + buttonId: z.string().min(1).optional(), +}); + +export const sendListBodySchema = commonMessageSchema.extend({ + title: z.string().optional(), + text: z.string().min(1), + footer: z.string().optional(), + buttonText: z.string().min(1), + sections: z.array( + z.object({ + title: z.string().min(1), + rows: z.array( + z.object({ + id: z.string().min(1), + title: z.string().min(1), + description: z.string().optional(), + }), + ).min(1), + }), + ).min(1), +}); + +export const sendPollBodySchema = commonMessageSchema.extend({ + title: z.string().min(1), + options: z.array(z.string().min(1)).min(2), + selectableCount: z.number().int().positive().default(1), +}); + +const carouselButtonSchema = z.object({ + id: z.string().min(1).optional(), + title: z.string().min(1).optional(), + kind: z.enum(["reply", "quick_reply", "copy_code", "cta_url"]).optional(), + code: z.string().optional(), + url: z.string().url().optional(), + nativeFlowName: z.string().optional(), + buttonParamsJson: z.string().optional(), +}); + +const carouselCardSchema = z.object({ + title: z.string().optional(), + body: z.string().optional(), + footer: z.string().optional(), + image: mediaLikeSchema.optional(), + video: mediaLikeSchema.optional(), + buttons: z.array(carouselButtonSchema).optional(), +}).refine((value) => !!value.image || !!value.video, "Each carousel card needs image or video."); + +export const sendCarouselBodySchema = commonMessageSchema.extend({ + text: z.string().min(1), + footer: z.string().optional(), + carouselCardType: z.enum(["image", "video", "mixed"]).optional(), + ai: z.boolean().optional(), + cards: z.array(carouselCardSchema).min(1).max(10), +}); + +export const sendLocationBodySchema = commonMessageSchema.extend({ + latitude: z.number(), + longitude: z.number(), + name: z.string().optional(), + address: z.string().optional(), +}); + +export const sendLiveLocationBodySchema = sendLocationBodySchema.extend({ + degreesClockwiseFromMagneticNorth: z.number().optional(), + speedInMps: z.number().optional(), + accuracyInMeters: z.number().optional(), +}); + +export const sendContactBodySchema = commonMessageSchema.extend({ + displayName: z.string().min(1), + vcard: z.string().min(1), +}); + +export const sendContactsBodySchema = commonMessageSchema.extend({ + displayName: z.string().optional(), + contacts: z.array( + z.object({ + displayName: z.string().min(1), + vcard: z.string().min(1), + }), + ).min(1), +}); + +export const sendStatusBodySchema = commonMessageSchema.extend({ + text: z.string().optional(), + media: mediaLikeSchema.optional(), +}).refine((value) => !!value.text || !!value.media, "text or media is required."); + +export const sendProductBodySchema = commonMessageSchema.extend({ + title: z.string().min(1), + description: z.string().optional(), + price: z.string().optional(), + retailerId: z.string().optional(), + businessOwnerJid: z.string().optional(), + productImage: mediaLikeSchema.optional(), +}); + +export const sendCatalogBodySchema = commonMessageSchema.extend({ + title: z.string().min(1), + businessOwnerJid: z.string().optional(), + thumbnail: mediaLikeSchema.optional(), +}); + +export const sendCollectionBodySchema = commonMessageSchema.extend({ + title: z.string().min(1), + businessOwnerJid: z.string().optional(), + collectionId: z.string().optional(), +}); + +export const sendForwardBodySchema = commonMessageSchema.extend({ + message: z.record(z.string(), z.unknown()), +}); + +export const sendPresenceBodySchema = commonMessageSchema.extend({ + presence: z.enum(["available", "composing", "recording", "paused", "unavailable"]), +}); + +export const sendAiTextBodySchema = commonMessageSchema.extend({ + text: z.string().min(1), +}); diff --git a/packages/berryapi/src/modules/profile/profile.controller.ts b/packages/berryapi/src/modules/profile/profile.controller.ts new file mode 100644 index 0000000..3da79d6 --- /dev/null +++ b/packages/berryapi/src/modules/profile/profile.controller.ts @@ -0,0 +1,58 @@ +import type { FastifyReply, FastifyRequest } from "fastify"; +import type { ZodTypeAny } from "zod"; +import { successResponse } from "../../utils/response.js"; +import { validateOrThrow } from "../../utils/zod.js"; +import { + instanceNameParamsSchema, + profileJidBodySchema, + updateNameBodySchema, + updatePictureBodySchema, + updatePrivacySettingsBodySchema, + updateStatusBodySchema, +} from "./profile.validators.js"; +import type { ProfileService } from "./profile.service.js"; + +export class ProfileController { + constructor(private readonly service: ProfileService) {} + + private getInstanceName(request: FastifyRequest): string { + return validateOrThrow(instanceNameParamsSchema, request.params).instanceName; + } + + private async run( + request: FastifyRequest, + reply: FastifyReply, + schema: ZodTypeAny | null, + message: string, + executor: (instanceName: string, input: any) => Promise, + ) { + const instanceName = this.getInstanceName(request); + const body = schema ? validateOrThrow(schema, request.body) as Record : {}; + const data = await executor(instanceName, body); + reply.send(successResponse(message, data)); + } + + fetchBusinessProfile = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, profileJidBodySchema, "Business profile fetched successfully.", this.service.fetchBusinessProfile.bind(this.service)); + + fetchProfile = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, profileJidBodySchema, "Profile fetched successfully.", this.service.fetchProfile.bind(this.service)); + + updateName = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, updateNameBodySchema, "Profile name updated successfully.", this.service.updateName.bind(this.service)); + + updateStatus = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, updateStatusBodySchema, "Profile status updated successfully.", this.service.updateStatus.bind(this.service)); + + updatePicture = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, updatePictureBodySchema, "Profile picture updated successfully.", this.service.updatePicture.bind(this.service)); + + removePicture = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, null, "Profile picture removed successfully.", this.service.removePicture.bind(this.service)); + + privacySettings = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, null, "Privacy settings fetched successfully.", this.service.privacySettings.bind(this.service)); + + updatePrivacySettings = async (request: FastifyRequest, reply: FastifyReply) => + this.run(request, reply, updatePrivacySettingsBodySchema, "Privacy settings updated successfully.", this.service.updatePrivacySettings.bind(this.service)); +} diff --git a/packages/berryapi/src/modules/profile/profile.routes.ts b/packages/berryapi/src/modules/profile/profile.routes.ts new file mode 100644 index 0000000..96faa3d --- /dev/null +++ b/packages/berryapi/src/modules/profile/profile.routes.ts @@ -0,0 +1,52 @@ +import type { FastifyInstance } from "fastify"; +import { createRouteSchema } from "../../docs/schema.js"; +import type { InstanceManager } from "../../managers/InstanceManager.js"; +import { ProfileController } from "./profile.controller.js"; +import { ProfileService } from "./profile.service.js"; +import { + instanceNameParamsSchema, + profileJidBodySchema, + updateNameBodySchema, + updatePictureBodySchema, + updatePrivacySettingsBodySchema, + updateStatusBodySchema, +} from "./profile.validators.js"; + +export const registerProfileRoutes = async (app: any, manager: InstanceManager) => { + const controller = new ProfileController(new ProfileService(manager)); + + const post = ( + path: string, + summary: string, + body: object, + handler: any, + ) => app.post(path, { + schema: createRouteSchema({ + tags: ["Profile"], + summary, + params: instanceNameParamsSchema, + body: body as never, + }), + }, handler); + + post("/profile/fetchBusinessProfile/:instanceName", "Fetch business profile", profileJidBodySchema, controller.fetchBusinessProfile); + post("/profile/fetchProfile/:instanceName", "Fetch profile", profileJidBodySchema, controller.fetchProfile); + post("/profile/updateName/:instanceName", "Update profile name", updateNameBodySchema, controller.updateName); + post("/profile/updateStatus/:instanceName", "Update profile status", updateStatusBodySchema, controller.updateStatus); + post("/profile/updatePicture/:instanceName", "Update profile picture", updatePictureBodySchema, controller.updatePicture); + app.delete("/profile/removePicture/:instanceName", { + schema: createRouteSchema({ + tags: ["Profile"], + summary: "Remove profile picture", + params: instanceNameParamsSchema, + }), + }, controller.removePicture); + app.get("/profile/privacySettings/:instanceName", { + schema: createRouteSchema({ + tags: ["Profile"], + summary: "Fetch privacy settings", + params: instanceNameParamsSchema, + }), + }, controller.privacySettings); + post("/profile/updatePrivacySettings/:instanceName", "Update privacy settings", updatePrivacySettingsBodySchema, controller.updatePrivacySettings); +}; diff --git a/packages/berryapi/src/modules/profile/profile.service.ts b/packages/berryapi/src/modules/profile/profile.service.ts new file mode 100644 index 0000000..3549721 --- /dev/null +++ b/packages/berryapi/src/modules/profile/profile.service.ts @@ -0,0 +1,56 @@ +import { InstanceManager } from "../../managers/InstanceManager.js"; +import { notImplementedYet } from "../shared/todo.js"; + +export class ProfileService { + constructor(private readonly manager: InstanceManager) {} + + async fetchBusinessProfile(instanceName: string, input: { jid?: string }) { + const state = await this.manager.getConnectionState(instanceName); + return { + instanceName, + jid: input.jid, + provider: "berryprotocol", + state, + note: "Business profile details are not exposed by BerryProtocol yet.", + }; + } + + async fetchProfile(instanceName: string, input: { jid?: string }) { + const state = await this.manager.getConnectionState(instanceName); + const instance = await this.manager.getInstance(instanceName); + return { + instanceName, + jid: input.jid, + phoneNumber: instance.phoneNumber, + connectionState: state, + }; + } + + async updateName(_instanceName: string, _input: { name: string }) { + notImplementedYet("profile name updates are not exposed by BerryProtocol yet."); + } + + async updateStatus(_instanceName: string, _input: { status: string }) { + notImplementedYet("profile status updates are not exposed by BerryProtocol yet."); + } + + async updatePicture(_instanceName: string, _input: { url?: string; path?: string; base64?: string }) { + notImplementedYet("profile picture updates are not exposed by BerryProtocol yet."); + } + + async removePicture(_instanceName: string) { + notImplementedYet("profile picture removal is not exposed by BerryProtocol yet."); + } + + async privacySettings(instanceName: string) { + const settings = await this.manager.getSettings(instanceName); + return settings.privacy ?? {}; + } + + async updatePrivacySettings(instanceName: string, input: Record) { + const instance = await this.manager.setSettings(instanceName, { + privacy: input, + }); + return instance.settings.privacy ?? {}; + } +} diff --git a/packages/berryapi/src/modules/profile/profile.validators.ts b/packages/berryapi/src/modules/profile/profile.validators.ts new file mode 100644 index 0000000..83febd3 --- /dev/null +++ b/packages/berryapi/src/modules/profile/profile.validators.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +export const instanceNameParamsSchema = z.object({ + instanceName: z.string().min(1), +}); + +export const profileJidBodySchema = z.object({ + jid: z.string().optional(), +}); + +export const updateNameBodySchema = z.object({ + name: z.string().min(1), +}); + +export const updateStatusBodySchema = z.object({ + status: z.string().min(1), +}); + +export const updatePrivacySettingsBodySchema = z.object({ + readReceipts: z.boolean().optional(), + lastSeen: z.enum(["everyone", "contacts", "nobody"]).optional(), + profilePhoto: z.enum(["everyone", "contacts", "nobody"]).optional(), + online: z.enum(["everyone", "contacts", "nobody"]).optional(), +}); + +export const updatePictureBodySchema = z.object({ + url: z.string().url().optional(), + path: z.string().optional(), + base64: z.string().optional(), +}).refine((value) => value.url || value.path || value.base64, "url, path or base64 is required."); diff --git a/packages/berryapi/src/modules/settings/settings.controller.ts b/packages/berryapi/src/modules/settings/settings.controller.ts new file mode 100644 index 0000000..220ea75 --- /dev/null +++ b/packages/berryapi/src/modules/settings/settings.controller.ts @@ -0,0 +1,22 @@ +import type { FastifyReply, FastifyRequest } from "fastify"; +import { successResponse } from "../../utils/response.js"; +import { validateOrThrow } from "../../utils/zod.js"; +import { instanceNameParamsSchema, setSettingsBodySchema } from "./settings.validators.js"; +import type { SettingsService } from "./settings.service.js"; + +export class SettingsController { + constructor(private readonly service: SettingsService) {} + + set = async (request: FastifyRequest, reply: FastifyReply) => { + const params = validateOrThrow(instanceNameParamsSchema, request.params); + const body = validateOrThrow(setSettingsBodySchema, request.body); + const data = await this.service.set(params.instanceName, body); + reply.send(successResponse("Settings updated successfully.", data)); + }; + + find = async (request: FastifyRequest, reply: FastifyReply) => { + const params = validateOrThrow(instanceNameParamsSchema, request.params); + const data = await this.service.find(params.instanceName); + reply.send(successResponse("Settings fetched successfully.", data)); + }; +} diff --git a/packages/berryapi/src/modules/settings/settings.routes.ts b/packages/berryapi/src/modules/settings/settings.routes.ts new file mode 100644 index 0000000..98f6b7d --- /dev/null +++ b/packages/berryapi/src/modules/settings/settings.routes.ts @@ -0,0 +1,30 @@ +import type { FastifyInstance } from "fastify"; +import { createRouteSchema } from "../../docs/schema.js"; +import type { InstanceManager } from "../../managers/InstanceManager.js"; +import { SettingsController } from "./settings.controller.js"; +import { SettingsService } from "./settings.service.js"; +import { instanceNameParamsSchema, setSettingsBodySchema } from "./settings.validators.js"; + +export const registerSettingsRoutes = async ( + app: any, + manager: InstanceManager, +) => { + const controller = new SettingsController(new SettingsService(manager)); + + app.post("/settings/set/:instanceName", { + schema: createRouteSchema({ + tags: ["Settings"], + summary: "Set settings", + params: instanceNameParamsSchema, + body: setSettingsBodySchema, + }), + }, controller.set); + + app.get("/settings/find/:instanceName", { + schema: createRouteSchema({ + tags: ["Settings"], + summary: "Find settings", + params: instanceNameParamsSchema, + }), + }, controller.find); +}; diff --git a/packages/berryapi/src/modules/settings/settings.service.ts b/packages/berryapi/src/modules/settings/settings.service.ts new file mode 100644 index 0000000..7104568 --- /dev/null +++ b/packages/berryapi/src/modules/settings/settings.service.ts @@ -0,0 +1,15 @@ +import type { z } from "zod"; +import type { InstanceManager } from "../../managers/InstanceManager.js"; +import type { setSettingsBodySchema } from "./settings.validators.js"; + +export class SettingsService { + constructor(private readonly manager: InstanceManager) {} + + set(instanceName: string, input: z.infer) { + return this.manager.setSettings(instanceName, input); + } + + find(instanceName: string) { + return this.manager.getSettings(instanceName); + } +} diff --git a/packages/berryapi/src/modules/settings/settings.validators.ts b/packages/berryapi/src/modules/settings/settings.validators.ts new file mode 100644 index 0000000..c05d36f --- /dev/null +++ b/packages/berryapi/src/modules/settings/settings.validators.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; +import { instanceNameParamsSchema } from "../instance/instance.validators.js"; + +export { instanceNameParamsSchema }; + +const eventNameSchema = z.enum([ + "connection.update", + "qrcode.updated", + "messages.upsert", + "messages.update", + "messages.delete", + "chats.update", + "contacts.update", + "groups.update", + "presence.update", + "send.message", +]); + +export const setSettingsBodySchema = z.object({ + rejectCall: z.boolean().optional(), + readMessages: z.boolean().optional(), + syncFullHistory: z.boolean().optional(), + alwaysOnline: z.boolean().optional(), + webhookByEvents: z.boolean().optional(), + events: z.array(eventNameSchema).optional(), +}); diff --git a/packages/berryapi/src/modules/shared/todo.ts b/packages/berryapi/src/modules/shared/todo.ts new file mode 100644 index 0000000..8b89faa --- /dev/null +++ b/packages/berryapi/src/modules/shared/todo.ts @@ -0,0 +1,8 @@ +import { HttpError } from "../../utils/http-error.js"; + +export const notImplementedYet = (message: string, details?: Record): never => { + throw new HttpError(501, message, { + todo: true, + ...details, + }); +}; diff --git a/packages/berryapi/src/modules/studio/assets/index.html b/packages/berryapi/src/modules/studio/assets/index.html new file mode 100644 index 0000000..a005329 --- /dev/null +++ b/packages/berryapi/src/modules/studio/assets/index.html @@ -0,0 +1,125 @@ + + + + + + Berry Studio + + + +
+ + +
+
+
+

Visual builder

+

No flow selected

+
+
+ + + +
+
+ +
+
+
+ Canvas + +
+
+ +
+
+ +
+
+
+

Inspector

+ Select a node +
+
Select a node to edit its behavior.
+
+ +
+
+

WhatsApp Preview

+ Rendered from the selected node +
+
+
+ B +
+ Berry Demo Chat +

online

+
+
+
+
+
+ +
+
+

Simulator

+ Test the whole flow before using a real instance +
+
Run the simulation to see the transcript here.
+
+ + +
+
+
+

Variables

+
{}
+
+
+

Logs

+
[]
+
+
+
+
+
+
+
+ + + + diff --git a/packages/berryapi/src/modules/studio/assets/studio.css b/packages/berryapi/src/modules/studio/assets/studio.css new file mode 100644 index 0000000..146b69f --- /dev/null +++ b/packages/berryapi/src/modules/studio/assets/studio.css @@ -0,0 +1,469 @@ +:root { + --bg: #f3efe6; + --panel: rgba(255, 255, 255, 0.86); + --panel-strong: #ffffff; + --border: rgba(31, 41, 55, 0.12); + --text: #111827; + --muted: #6b7280; + --accent: #0a8f6a; + --accent-strong: #056647; + --warm: #f59e0b; + --shadow: 0 18px 45px rgba(17, 24, 39, 0.12); + --radius: 22px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Segoe UI", sans-serif; + color: var(--text); + background: + radial-gradient(circle at top left, rgba(245, 158, 11, 0.18), transparent 24%), + radial-gradient(circle at top right, rgba(10, 143, 106, 0.12), transparent 28%), + linear-gradient(180deg, #f9f5ee 0%, #f0ebe1 100%); +} + +.studio-shell { + display: grid; + grid-template-columns: 320px 1fr; + min-height: 100vh; +} + +.sidebar { + padding: 24px; + border-right: 1px solid var(--border); + background: rgba(255, 248, 240, 0.85); + backdrop-filter: blur(16px); + overflow-y: auto; +} + +.main { + display: flex; + flex-direction: column; + min-width: 0; +} + +.brand h1, +.topbar h2, +.panel h2 { + margin: 0; +} + +.eyebrow { + margin: 0 0 8px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--accent-strong); +} + +.subtitle, +.hint, +.phone-topbar p { + margin: 0; + color: var(--muted); + font-size: 13px; +} + +.panel, +.canvas-card { + margin-top: 18px; + padding: 18px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--panel); + box-shadow: var(--shadow); + backdrop-filter: blur(18px); +} + +.topbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + padding: 24px 24px 0; +} + +.workspace { + display: grid; + grid-template-columns: minmax(520px, 1.2fr) minmax(420px, 0.9fr); + gap: 20px; + padding: 24px; + min-height: 0; + flex: 1; +} + +.canvas-card { + display: flex; + flex-direction: column; + min-height: calc(100vh - 120px); +} + +.canvas-toolbar, +.panel-heading, +.row-actions, +.simulator-input-row { + display: flex; + align-items: center; + gap: 10px; +} + +.panel-heading, +.canvas-toolbar { + justify-content: space-between; +} + +.sidepanes { + display: grid; + gap: 18px; + align-content: start; +} + +.button { + appearance: none; + border: 0; + border-radius: 999px; + padding: 10px 14px; + font: inherit; + font-weight: 600; + cursor: pointer; + transition: transform 120ms ease, opacity 120ms ease, background 120ms ease; +} + +.button:hover { + transform: translateY(-1px); +} + +.button-primary { + background: linear-gradient(135deg, var(--accent), var(--accent-strong)); + color: #fff; +} + +.button-secondary { + background: rgba(17, 24, 39, 0.08); + color: var(--text); +} + +.button-accent { + background: linear-gradient(135deg, #f59e0b, #d97706); + color: #fff; +} + +.button-ghost { + background: transparent; + border: 1px solid var(--border); + color: var(--muted); +} + +.field { + display: grid; + gap: 6px; + margin-top: 12px; +} + +input, +textarea, +select { + width: 100%; + border: 1px solid var(--border); + border-radius: 16px; + padding: 11px 14px; + font: inherit; + color: var(--text); + background: rgba(255, 255, 255, 0.9); +} + +textarea { + min-height: 110px; + resize: vertical; +} + +.flow-list, +.palette, +.inspector { + display: grid; + gap: 10px; + margin-top: 12px; +} + +.flow-item, +.palette-item { + display: grid; + gap: 4px; + padding: 12px 14px; + border: 1px solid var(--border); + border-radius: 18px; + background: rgba(255, 255, 255, 0.86); + cursor: pointer; +} + +.flow-item.active { + border-color: rgba(10, 143, 106, 0.35); + box-shadow: inset 0 0 0 1px rgba(10, 143, 106, 0.15); +} + +.canvas { + position: relative; + flex: 1; + min-height: 640px; + overflow: auto; + border-radius: 20px; + margin-top: 16px; + background: + linear-gradient(90deg, rgba(17, 24, 39, 0.03) 1px, transparent 1px), + linear-gradient(rgba(17, 24, 39, 0.03) 1px, transparent 1px), + linear-gradient(180deg, rgba(255, 255, 255, 0.85), rgba(247, 243, 237, 0.88)); + background-size: 32px 32px, 32px 32px, 100% 100%; +} + +.edge-layer { + position: absolute; + inset: 0; + pointer-events: none; + width: 2400px; + height: 1600px; +} + +.edge-path { + fill: none; + stroke: rgba(17, 24, 39, 0.22); + stroke-width: 2.5; +} + +.flow-node { + position: absolute; + width: 220px; + min-height: 108px; + padding: 14px; + border-radius: 22px; + border: 1px solid rgba(17, 24, 39, 0.1); + background: rgba(255, 255, 255, 0.95); + box-shadow: 0 20px 32px rgba(17, 24, 39, 0.12); + cursor: grab; +} + +.flow-node.selected { + outline: 2px solid rgba(10, 143, 106, 0.28); + box-shadow: 0 24px 36px rgba(10, 143, 106, 0.18); +} + +.node-type { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--accent-strong); +} + +.node-title { + margin: 6px 0; + font-size: 17px; +} + +.node-summary { + font-size: 13px; + color: var(--muted); +} + +.node-link-row { + display: grid; + gap: 6px; + margin-top: 12px; +} + +.phone-shell { + margin-top: 12px; + border-radius: 34px; + overflow: hidden; + background: #d8f0e5; + border: 7px solid #16212b; +} + +.phone-topbar { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 18px; + background: #f6f6f6; +} + +.avatar { + display: inline-grid; + place-items: center; + width: 38px; + height: 38px; + border-radius: 50%; + background: linear-gradient(135deg, #14b8a6, #0a8f6a); + color: #fff; + font-weight: 700; +} + +.phone-chat, +.simulator-chat { + display: grid; + gap: 10px; + padding: 18px; + min-height: 280px; + max-height: 360px; + overflow: auto; + background: + radial-gradient(circle at center, rgba(255, 255, 255, 0.18), transparent 68%), + linear-gradient(180deg, #efeae2 0%, #ebe5db 100%); +} + +.wa-message, +.sim-message { + max-width: 82%; + padding: 10px 12px; + border-radius: 16px; + font-size: 14px; + line-height: 1.45; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); +} + +.wa-message.assistant, +.sim-message.assistant { + justify-self: start; + background: #fff; +} + +.wa-message.user, +.sim-message.user { + justify-self: end; + background: #dcf8c6; +} + +.wa-meta, +.sim-meta { + margin-top: 6px; + font-size: 11px; + color: rgba(17, 24, 39, 0.52); + text-align: right; +} + +.wa-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 10px; + border-radius: 999px; + background: rgba(10, 143, 106, 0.1); + font-size: 12px; + font-weight: 700; + color: var(--accent-strong); +} + +.wa-media, +.wa-card { + margin-top: 8px; + border-radius: 14px; + background: linear-gradient(135deg, rgba(10, 143, 106, 0.18), rgba(15, 23, 42, 0.08)); + min-height: 140px; + display: grid; + place-items: center; + color: var(--accent-strong); + font-weight: 700; + text-align: center; + padding: 16px; +} + +.wa-buttons, +.wa-list, +.wa-carousel, +.wa-options { + display: grid; + gap: 8px; + margin-top: 10px; +} + +.wa-button, +.wa-option, +.wa-list-row { + padding: 10px 12px; + border-radius: 14px; + border: 1px solid rgba(10, 143, 106, 0.16); + background: rgba(255, 255, 255, 0.76); +} + +.wa-carousel { + grid-auto-flow: column; + grid-auto-columns: 190px; + overflow-x: auto; +} + +.wa-card { + display: block; + min-height: 210px; + background: #fff; + border: 1px solid rgba(17, 24, 39, 0.08); + color: var(--text); +} + +.wa-card-cover { + min-height: 92px; + border-radius: 12px; + background: linear-gradient(135deg, rgba(10, 143, 106, 0.2), rgba(245, 158, 11, 0.18)); + display: grid; + place-items: center; + font-weight: 700; +} + +.wa-card h4, +.wa-card p { + margin: 10px 0 0; +} + +.simulator-input-row { + margin-top: 12px; +} + +.simulator-meta { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-top: 12px; +} + +pre { + margin: 0; + padding: 12px; + border-radius: 16px; + background: rgba(17, 24, 39, 0.06); + overflow: auto; + font-size: 12px; + max-height: 160px; +} + +.empty-state { + color: var(--muted); +} + +.checkbox-row { + display: flex; + align-items: center; + gap: 8px; + margin-top: 12px; +} + +.checkbox-row input { + width: auto; +} + +@media (max-width: 1280px) { + .workspace { + grid-template-columns: 1fr; + } + + .studio-shell { + grid-template-columns: 1fr; + } + + .sidebar { + border-right: 0; + border-bottom: 1px solid var(--border); + } +} diff --git a/packages/berryapi/src/modules/studio/assets/studio.js b/packages/berryapi/src/modules/studio/assets/studio.js new file mode 100644 index 0000000..fa04629 --- /dev/null +++ b/packages/berryapi/src/modules/studio/assets/studio.js @@ -0,0 +1,1001 @@ +const state = { + apiKey: localStorage.getItem("berry_studio_api_key") || "berryapi_dev_key", + capabilities: null, + flows: [], + currentFlow: null, + selectedNodeId: null, + simulatorState: null, +}; + +const elements = { + apiKey: document.getElementById("api-key"), + connectButton: document.getElementById("connect-button"), + loadButton: document.getElementById("load-button"), + flowList: document.getElementById("flow-list"), + nodePalette: document.getElementById("node-palette"), + newFlowButton: document.getElementById("new-flow-button"), + flowTitle: document.getElementById("flow-title"), + canvas: document.getElementById("canvas"), + edgeLayer: document.getElementById("edge-layer"), + inspector: document.getElementById("inspector"), + selectedNodeLabel: document.getElementById("selected-node-label"), + previewChat: document.getElementById("preview-chat"), + simulatorChat: document.getElementById("simulator-chat"), + simulatorInput: document.getElementById("simulator-input"), + sendSimulatorInput: document.getElementById("send-simulator-input"), + variablesView: document.getElementById("variables-view"), + logsView: document.getElementById("logs-view"), + saveFlowButton: document.getElementById("save-flow-button"), + publishFlowButton: document.getElementById("publish-flow-button"), + simulateButton: document.getElementById("simulate-button"), + autoLayoutButton: document.getElementById("auto-layout-button"), +}; + +const makeHeaders = () => ({ + "Content-Type": "application/json", + Authorization: `Bearer ${state.apiKey}`, +}); + +const api = async (path, options = {}) => { + const response = await fetch(path, { + ...options, + headers: { + ...(options.body ? { "Content-Type": "application/json" } : {}), + Authorization: `Bearer ${state.apiKey}`, + ...(options.headers || {}), + }, + }); + + const json = await response.json(); + if (!response.ok || !json.success) { + throw new Error(json.message || "Berry Studio request failed."); + } + + return json.data; +}; + +const deepClone = (value) => JSON.parse(JSON.stringify(value)); + +const uid = (prefix) => `${prefix}-${Math.random().toString(36).slice(2, 8)}`; + +const getSelectedNode = () => + state.currentFlow?.graph.nodes.find((node) => node.id === state.selectedNodeId) || null; + +const getOutgoingEdges = (nodeId) => + state.currentFlow.graph.edges.filter((edge) => edge.source === nodeId); + +const getNodeById = (nodeId) => + state.currentFlow.graph.nodes.find((node) => node.id === nodeId) || null; + +const saveApiKey = () => { + state.apiKey = elements.apiKey.value.trim() || "berryapi_dev_key"; + localStorage.setItem("berry_studio_api_key", state.apiKey); +}; + +const renderFlowList = () => { + elements.flowList.innerHTML = ""; + + state.flows.forEach((flow) => { + const item = document.createElement("button"); + item.type = "button"; + item.className = `flow-item ${state.currentFlow?.id === flow.id ? "active" : ""}`; + item.innerHTML = ` + ${flow.name} + ${flow.status} + ${new Date(flow.updated_at).toLocaleString()} + `; + item.addEventListener("click", () => loadFlow(flow.id)); + elements.flowList.appendChild(item); + }); +}; + +const renderNodePalette = () => { + elements.nodePalette.innerHTML = ""; + const nodeTypes = state.capabilities?.nodeTypes || []; + + nodeTypes.forEach((nodeType) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "palette-item"; + button.draggable = true; + button.dataset.nodeType = nodeType.id; + button.innerHTML = ` + ${nodeType.label} + ${nodeType.description} + `; + button.addEventListener("dragstart", (event) => { + event.dataTransfer.setData("text/node-type", nodeType.id); + }); + button.addEventListener("click", () => addNode(nodeType.id)); + elements.nodePalette.appendChild(button); + }); +}; + +const renderCanvas = () => { + elements.canvas.querySelectorAll(".flow-node").forEach((node) => node.remove()); + if (!state.currentFlow) { + elements.flowTitle.textContent = "No flow selected"; + elements.edgeLayer.innerHTML = ""; + return; + } + + elements.flowTitle.textContent = state.currentFlow.name; + + state.currentFlow.graph.nodes.forEach((node) => { + const nodeElement = document.createElement("article"); + nodeElement.className = `flow-node ${state.selectedNodeId === node.id ? "selected" : ""}`; + nodeElement.style.left = `${node.position.x}px`; + nodeElement.style.top = `${node.position.y}px`; + nodeElement.dataset.nodeId = node.id; + + const summary = getNodeSummary(node); + nodeElement.innerHTML = ` +
${node.type}
+

${node.data.label || node.type}

+
${summary}
+ `; + + nodeElement.addEventListener("click", () => { + state.selectedNodeId = node.id; + renderAll(); + }); + + enableNodeDrag(nodeElement, node.id); + elements.canvas.appendChild(nodeElement); + }); + + renderEdges(); +}; + +const renderEdges = () => { + elements.edgeLayer.innerHTML = ""; + if (!state.currentFlow) { + return; + } + + state.currentFlow.graph.edges.forEach((edge) => { + const source = getNodeById(edge.source); + const target = getNodeById(edge.target); + if (!source || !target) { + return; + } + + const sourceX = source.position.x + 220; + const sourceY = source.position.y + 56; + const targetX = target.position.x; + const targetY = target.position.y + 56; + const curve = Math.max(60, Math.abs(targetX - sourceX) / 2); + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute( + "d", + `M ${sourceX} ${sourceY} C ${sourceX + curve} ${sourceY}, ${targetX - curve} ${targetY}, ${targetX} ${targetY}`, + ); + path.setAttribute("class", "edge-path"); + elements.edgeLayer.appendChild(path); + }); +}; + +const getNodeSummary = (node) => { + if (node.type === "message" || node.type === "action") { + const capability = state.capabilities.messageCapabilities.find((item) => item.id === node.data.messageType); + return capability ? capability.label : "Configure payload"; + } + + if (node.type === "input") { + return `Stores in ${node.data.variableName || "lastInput"}`; + } + + if (node.type === "condition") { + return `${node.data.variable || "lastInput"} ${node.data.operator || "equals"} ${node.data.value || ""}`; + } + + if (node.type === "setVariable") { + return `${node.data.key || "variable"} = ${node.data.value || ""}`; + } + + if (node.type === "delay") { + return `${node.data.delayMs || 1000} ms`; + } + + return node.data.label || "Ready"; +}; + +const renderInspector = () => { + const node = getSelectedNode(); + if (!node) { + elements.selectedNodeLabel.textContent = "Select a node"; + elements.inspector.className = "inspector empty-state"; + elements.inspector.textContent = "Select a node to edit its behavior."; + return; + } + + elements.selectedNodeLabel.textContent = `${node.type} • ${node.id}`; + elements.inspector.className = "inspector"; + elements.inspector.innerHTML = ""; + + const labelField = createField({ + key: "label", + label: "Node label", + type: "text", + }, node.data.label || node.type, (value) => { + node.data.label = value; + renderCanvas(); + }); + elements.inspector.appendChild(labelField); + + if (node.type === "message" || node.type === "action") { + renderMessageInspector(node); + } else if (node.type === "input") { + renderInputInspector(node); + } else if (node.type === "condition") { + renderConditionInspector(node); + } else if (node.type === "setVariable") { + renderSetVariableInspector(node); + } else if (node.type === "delay") { + renderDelayInspector(node); + } + + renderEdgeControls(node); +}; + +const renderMessageInspector = (node) => { + if (!node.data.messageType) { + node.data.messageType = node.type === "action" ? "sendReply" : "sendText"; + } + if (!node.data.payload) { + node.data.payload = {}; + } + + const options = state.capabilities.messageCapabilities.map((capability) => ({ + label: capability.label, + value: capability.id, + })); + + const typeField = createField({ + key: "messageType", + label: "Berry method", + type: "select", + options, + }, node.data.messageType, (value) => { + node.data.messageType = value; + node.data.payload = {}; + renderAll(); + }); + + elements.inspector.appendChild(typeField); + + const capability = state.capabilities.messageCapabilities.find((item) => item.id === node.data.messageType); + if (!capability) { + return; + } + + capability.fields.forEach((field) => { + const value = node.data.payload[field.key] ?? field.defaultValue ?? ""; + const fieldElement = createField(field, value, (nextValue) => { + node.data.payload[field.key] = nextValue; + renderPreview(); + renderCanvas(); + }); + elements.inspector.appendChild(fieldElement); + }); +}; + +const renderInputInspector = (node) => { + const fields = [ + { key: "prompt", label: "Prompt", type: "textarea" }, + { key: "variableName", label: "Variable name", type: "text" }, + ]; + + fields.forEach((field) => { + elements.inspector.appendChild(createField(field, node.data[field.key] || "", (value) => { + node.data[field.key] = value; + renderCanvas(); + })); + }); +}; + +const renderConditionInspector = (node) => { + const fields = [ + { key: "variable", label: "Variable", type: "text" }, + { + key: "operator", + label: "Operator", + type: "select", + options: state.capabilities.conditionOperators, + }, + { key: "value", label: "Expected value", type: "text" }, + ]; + + fields.forEach((field) => { + const value = node.data[field.key] || (field.key === "operator" ? "equals" : ""); + elements.inspector.appendChild(createField(field, value, (nextValue) => { + node.data[field.key] = nextValue; + renderCanvas(); + })); + }); +}; + +const renderSetVariableInspector = (node) => { + const fields = [ + { key: "key", label: "Variable key", type: "text" }, + { key: "value", label: "Value / template", type: "textarea" }, + ]; + + fields.forEach((field) => { + elements.inspector.appendChild(createField(field, node.data[field.key] || "", (nextValue) => { + node.data[field.key] = nextValue; + renderCanvas(); + })); + }); +}; + +const renderDelayInspector = (node) => { + elements.inspector.appendChild(createField({ + key: "delayMs", + label: "Delay (ms)", + type: "number", + }, node.data.delayMs || 1000, (value) => { + node.data.delayMs = Number(value || 0); + renderCanvas(); + })); +}; + +const renderEdgeControls = (node) => { + const edgeBox = document.createElement("div"); + edgeBox.className = "node-link-row"; + + const targets = [ + { label: "No connection", value: "" }, + ...state.currentFlow.graph.nodes + .filter((candidate) => candidate.id !== node.id) + .map((candidate) => ({ + label: `${candidate.data.label || candidate.type} (${candidate.id})`, + value: candidate.id, + })), + ]; + + if (node.type === "condition") { + ["true", "false"].forEach((handle) => { + edgeBox.appendChild(createField({ + key: handle, + label: `Branch ${handle}`, + type: "select", + options: targets, + }, getEdgeTarget(node.id, handle), (value) => { + upsertEdge(node.id, value, handle); + })); + }); + } else if (node.type !== "end") { + edgeBox.appendChild(createField({ + key: "next", + label: "Next node", + type: "select", + options: targets, + }, getEdgeTarget(node.id), (value) => { + upsertEdge(node.id, value); + })); + } + + const deleteButton = document.createElement("button"); + deleteButton.type = "button"; + deleteButton.className = "button button-secondary"; + deleteButton.textContent = "Delete node"; + deleteButton.addEventListener("click", () => deleteNode(node.id)); + edgeBox.appendChild(deleteButton); + + elements.inspector.appendChild(edgeBox); +}; + +const createField = (field, value, onChange) => { + const wrapper = document.createElement("label"); + wrapper.className = field.type === "checkbox" ? "checkbox-row" : "field"; + + const label = document.createElement("span"); + label.textContent = field.label; + wrapper.appendChild(label); + + let input; + + if (field.type === "textarea" || field.type === "json") { + input = document.createElement("textarea"); + input.value = field.type === "json" + ? (typeof value === "string" ? value : JSON.stringify(value || {}, null, 2)) + : value || ""; + } else if (field.type === "select") { + input = document.createElement("select"); + (field.options || []).forEach((option) => { + const element = document.createElement("option"); + element.value = option.value; + element.textContent = option.label; + input.appendChild(element); + }); + input.value = value || ""; + } else { + input = document.createElement("input"); + input.type = field.type === "number" ? "number" : field.type === "checkbox" ? "checkbox" : "text"; + if (field.type === "checkbox") { + input.checked = Boolean(value); + } else { + input.value = value ?? ""; + } + } + + if (field.placeholder) { + input.placeholder = field.placeholder; + } + + const handler = () => { + let nextValue; + if (field.type === "checkbox") { + nextValue = input.checked; + } else if (field.type === "number") { + nextValue = input.value === "" ? "" : Number(input.value); + } else if (field.type === "json") { + nextValue = input.value; + } else { + nextValue = input.value; + } + onChange(nextValue); + }; + + input.addEventListener("change", handler); + input.addEventListener("input", handler); + wrapper.appendChild(input); + + if (field.description) { + const hint = document.createElement("small"); + hint.className = "hint"; + hint.textContent = field.description; + wrapper.appendChild(hint); + } + + return wrapper; +}; + +const renderPreview = () => { + elements.previewChat.innerHTML = ""; + const node = getSelectedNode(); + + if (!node || !["message", "action", "input", "condition", "setVariable", "delay", "start", "end"].includes(node.type)) { + elements.previewChat.innerHTML = `
Select a node to preview how it behaves in chat.
`; + return; + } + + const sampleItems = buildPreviewItems(node); + sampleItems.forEach((item) => { + elements.previewChat.appendChild(renderChatBubble(item, "wa-message")); + }); +}; + +const buildPreviewItems = (node) => { + if (node.type === "message" || node.type === "action") { + return [{ + role: "assistant", + type: node.data.messageType || "sendText", + previewKind: state.capabilities.messageCapabilities.find((item) => item.id === node.data.messageType)?.previewKind || "text", + payload: normalizePayload(node.data.payload || {}), + }]; + } + + if (node.type === "input") { + return [ + { role: "assistant", type: "input_prompt", previewKind: "text", payload: { text: node.data.prompt || "What should we ask?" } }, + { role: "user", type: "text", previewKind: "text", payload: { text: `{{${node.data.variableName || "answer"}}}` } }, + ]; + } + + if (node.type === "condition") { + return [{ role: "assistant", type: "condition", previewKind: "event", payload: { text: `${node.data.variable || "lastInput"} ${node.data.operator || "equals"} ${node.data.value || ""}` } }]; + } + + if (node.type === "setVariable") { + return [{ role: "assistant", type: "setVariable", previewKind: "event", payload: { text: `${node.data.key || "variable"} = ${node.data.value || ""}` } }]; + } + + if (node.type === "delay") { + return [{ role: "assistant", type: "delay", previewKind: "event", payload: { text: `Delay ${node.data.delayMs || 1000} ms` } }]; + } + + return [{ role: "assistant", type: node.type, previewKind: "event", payload: { text: node.type } }]; +}; + +const normalizePayload = (payload) => { + const output = {}; + Object.entries(payload || {}).forEach(([key, value]) => { + if (typeof value !== "string") { + output[key] = value; + return; + } + + const trimmed = value.trim(); + if ((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) { + try { + output[key] = JSON.parse(trimmed); + return; + } catch {} + } + output[key] = value; + }); + return output; +}; + +const renderChatBubble = (item, className) => { + const bubble = document.createElement("div"); + bubble.className = `${className} ${item.role === "user" ? "user" : "assistant"}`; + + const payload = item.payload || {}; + const previewKind = item.previewKind || "text"; + + if (previewKind === "text" || previewKind === "status") { + bubble.innerHTML = `
${payload.text || payload.title || "Text message"}
`; + } else if (previewKind === "reply") { + bubble.innerHTML = ` + +
${payload.text || "Reply message"}
+ `; + } else if (previewKind === "image" || previewKind === "video") { + bubble.innerHTML = ` + +
${payload.caption || payload.text || "Media caption"}
+ `; + } else if (previewKind === "audio") { + bubble.innerHTML = ` + +
${payload.fileName || payload.url || "Voice note / audio file"}
+ `; + } else if (previewKind === "document") { + bubble.innerHTML = ` + +
${payload.fileName || "Document attachment"}
+
${payload.caption || ""}
+ `; + } else if (previewKind === "sticker") { + bubble.innerHTML = ``; + } else if (previewKind === "buttons") { + const buttons = Array.isArray(payload.buttons) + ? payload.buttons + : payload.copyCode + ? [{ title: payload.buttonText || "Copy", type: "cta_copy" }] + : []; + bubble.innerHTML = ` +
${payload.text || "Button message"}
+ + + `; + } else if (previewKind === "list") { + const sections = Array.isArray(payload.sections) ? payload.sections : []; + bubble.innerHTML = ` +
${payload.text || "List message"}
+ + + `; + } else if (previewKind === "carousel") { + const cards = Array.isArray(payload.cards) ? payload.cards : []; + bubble.innerHTML = ` +
${payload.text || "Carousel"}
+ + `; + } else if (previewKind === "poll") { + const options = Array.isArray(payload.options) ? payload.options : payload.values || []; + bubble.innerHTML = ` +
${payload.title || payload.name || "Poll"}
+ + `; + } else if (previewKind === "location") { + bubble.innerHTML = ` + +
${payload.name || "Pinned location"}
+
${payload.address || `${payload.latitude || ""}, ${payload.longitude || ""}`}
+ `; + } else if (previewKind === "contacts") { + const contacts = Array.isArray(payload.contacts) ? payload.contacts : payload.vcard ? [{ displayName: payload.displayName }] : []; + bubble.innerHTML = ` + + + `; + } else if (previewKind === "product") { + bubble.innerHTML = ` + + `; + } else if (previewKind === "catalog") { + bubble.innerHTML = ` + + `; + } else if (previewKind === "reaction") { + bubble.innerHTML = ``; + } else { + bubble.innerHTML = `
${payload.text || JSON.stringify(payload)}
`; + } + + const meta = document.createElement("div"); + meta.className = className === "sim-message" ? "sim-meta" : "wa-meta"; + meta.textContent = "12:34"; + bubble.appendChild(meta); + return bubble; +}; + +const renderSimulator = () => { + const transcript = state.simulatorState?.transcript || []; + if (!transcript.length) { + elements.simulatorChat.className = "simulator-chat empty-state"; + elements.simulatorChat.textContent = "Run the simulation to see the transcript here."; + } else { + elements.simulatorChat.className = "simulator-chat"; + elements.simulatorChat.innerHTML = ""; + transcript.forEach((item) => { + const bubble = renderChatBubble(item.role ? item : { role: "assistant", previewKind: "event", payload: item }, "sim-message"); + elements.simulatorChat.appendChild(bubble); + }); + } + + elements.variablesView.textContent = JSON.stringify(state.simulatorState?.variables || {}, null, 2); + elements.logsView.textContent = JSON.stringify(state.simulatorState?.logs || [], null, 2); +}; + +const renderAll = () => { + renderFlowList(); + renderNodePalette(); + renderCanvas(); + renderInspector(); + renderPreview(); + renderSimulator(); +}; + +const addNode = (type, position = null) => { + if (!state.currentFlow) { + return; + } + + const node = { + id: uid(type), + type, + position: position || { + x: 180 + state.currentFlow.graph.nodes.length * 24, + y: 120 + state.currentFlow.graph.nodes.length * 24, + }, + data: { + label: type === "message" ? "Message node" : type === "action" ? "Action node" : type.charAt(0).toUpperCase() + type.slice(1), + }, + }; + + if (type === "message") { + node.data.messageType = "sendText"; + node.data.payload = { text: "Hello from Berry Studio" }; + } + + if (type === "action") { + node.data.messageType = "sendReply"; + node.data.payload = { text: "Reply from Berry Studio", quoted: "{\"remoteJid\":\"5511999999999@s.whatsapp.net\",\"id\":\"MSG_ID\"}" }; + } + + if (type === "input") { + node.data.prompt = "How can we help you today?"; + node.data.variableName = "reply"; + } + + if (type === "condition") { + node.data.variable = "reply"; + node.data.operator = "equals"; + node.data.value = "sales"; + } + + if (type === "setVariable") { + node.data.key = "segment"; + node.data.value = "vip"; + } + + if (type === "delay") { + node.data.delayMs = 1000; + } + + state.currentFlow.graph.nodes.push(node); + state.selectedNodeId = node.id; + renderAll(); +}; + +const deleteNode = (nodeId) => { + if (!state.currentFlow) { + return; + } + + state.currentFlow.graph.nodes = state.currentFlow.graph.nodes.filter((node) => node.id !== nodeId); + state.currentFlow.graph.edges = state.currentFlow.graph.edges.filter((edge) => edge.source !== nodeId && edge.target !== nodeId); + state.selectedNodeId = state.currentFlow.graph.nodes[0]?.id || null; + renderAll(); +}; + +const getEdgeTarget = (source, handle = undefined) => + state.currentFlow.graph.edges.find((edge) => edge.source === source && edge.sourceHandle === handle)?.target || ""; + +const upsertEdge = (source, target, sourceHandle = undefined) => { + state.currentFlow.graph.edges = state.currentFlow.graph.edges.filter((edge) => !(edge.source === source && edge.sourceHandle === sourceHandle)); + if (target) { + state.currentFlow.graph.edges.push({ + id: uid("edge"), + source, + target, + sourceHandle, + }); + } + renderAll(); +}; + +const enableNodeDrag = (nodeElement, nodeId) => { + let dragging = false; + let offsetX = 0; + let offsetY = 0; + + nodeElement.addEventListener("pointerdown", (event) => { + dragging = true; + const node = getNodeById(nodeId); + offsetX = event.clientX - node.position.x; + offsetY = event.clientY - node.position.y; + nodeElement.setPointerCapture(event.pointerId); + }); + + nodeElement.addEventListener("pointermove", (event) => { + if (!dragging) { + return; + } + + const node = getNodeById(nodeId); + node.position.x = Math.max(24, event.clientX - offsetX); + node.position.y = Math.max(24, event.clientY - offsetY - 90); + renderCanvas(); + }); + + nodeElement.addEventListener("pointerup", (event) => { + dragging = false; + nodeElement.releasePointerCapture(event.pointerId); + }); +}; + +const autoLayout = () => { + if (!state.currentFlow) { + return; + } + + state.currentFlow.graph.nodes.forEach((node, index) => { + node.position.x = 120 + (index % 3) * 280; + node.position.y = 120 + Math.floor(index / 3) * 180; + }); + renderAll(); +}; + +const loadCapabilities = async () => { + state.capabilities = await api("/studio/api/capabilities"); +}; + +const loadFlows = async () => { + state.flows = await api("/studio/api/flows"); + renderFlowList(); +}; + +const loadFlow = async (flowId) => { + state.currentFlow = await api(`/studio/api/flows/${flowId}`); + state.selectedNodeId = state.currentFlow.graph.nodes[0]?.id || null; + state.simulatorState = null; + renderAll(); +}; + +const createFlow = async () => { + const name = window.prompt("Flow name", `Berry Flow ${state.flows.length + 1}`); + if (!name) { + return; + } + + const flow = await api("/studio/api/flows", { + method: "POST", + body: JSON.stringify({ + name, + graph: deepClone(state.capabilities.defaultGraph), + }), + }); + + await loadFlows(); + state.currentFlow = flow; + state.selectedNodeId = flow.graph.nodes[0]?.id || null; + renderAll(); +}; + +const saveCurrentFlow = async () => { + if (!state.currentFlow) { + return; + } + + state.currentFlow = await api(`/studio/api/flows/${state.currentFlow.id}`, { + method: "PUT", + body: JSON.stringify({ + name: state.currentFlow.name, + status: state.currentFlow.status, + graph: state.currentFlow.graph, + }), + }); + await loadFlows(); + renderAll(); +}; + +const publishCurrentFlow = async () => { + if (!state.currentFlow) { + return; + } + + state.currentFlow = await api(`/studio/api/flows/${state.currentFlow.id}/publish`, { + method: "POST", + }); + await loadFlows(); + renderAll(); +}; + +const startSimulation = async () => { + if (!state.currentFlow) { + return; + } + + state.simulatorState = await api("/studio/api/simulate/start", { + method: "POST", + body: JSON.stringify({ + flow: state.currentFlow.graph, + contact: { + jid: "5511999999999@s.whatsapp.net", + name: "Berry Demo", + }, + }), + }); + renderSimulator(); +}; + +const continueSimulation = async () => { + if (!state.currentFlow || !state.simulatorState) { + return; + } + + const inputText = elements.simulatorInput.value.trim(); + if (!inputText) { + return; + } + + state.simulatorState = await api("/studio/api/simulate/continue", { + method: "POST", + body: JSON.stringify({ + flow: state.currentFlow.graph, + state: state.simulatorState, + inputText, + }), + }); + + elements.simulatorInput.value = ""; + renderSimulator(); +}; + +const bindEvents = () => { + elements.apiKey.value = state.apiKey; + elements.connectButton.addEventListener("click", async () => { + try { + saveApiKey(); + await boot(); + } catch (error) { + alert(error.message); + } + }); + + elements.loadButton.addEventListener("click", async () => { + try { + saveApiKey(); + await loadFlows(); + } catch (error) { + alert(error.message); + } + }); + + elements.newFlowButton.addEventListener("click", async () => { + try { + await createFlow(); + } catch (error) { + alert(error.message); + } + }); + + elements.saveFlowButton.addEventListener("click", async () => { + try { + await saveCurrentFlow(); + } catch (error) { + alert(error.message); + } + }); + + elements.publishFlowButton.addEventListener("click", async () => { + try { + await publishCurrentFlow(); + } catch (error) { + alert(error.message); + } + }); + + elements.simulateButton.addEventListener("click", async () => { + try { + await startSimulation(); + } catch (error) { + alert(error.message); + } + }); + + elements.sendSimulatorInput.addEventListener("click", async () => { + try { + await continueSimulation(); + } catch (error) { + alert(error.message); + } + }); + + elements.simulatorInput.addEventListener("keydown", async (event) => { + if (event.key === "Enter") { + event.preventDefault(); + try { + await continueSimulation(); + } catch (error) { + alert(error.message); + } + } + }); + + elements.autoLayoutButton.addEventListener("click", autoLayout); + + elements.canvas.addEventListener("dragover", (event) => { + event.preventDefault(); + }); + + elements.canvas.addEventListener("drop", (event) => { + event.preventDefault(); + const nodeType = event.dataTransfer.getData("text/node-type"); + if (!nodeType) { + return; + } + + const rect = elements.canvas.getBoundingClientRect(); + addNode(nodeType, { + x: event.clientX - rect.left, + y: event.clientY - rect.top, + }); + }); +}; + +const boot = async () => { + await loadCapabilities(); + await loadFlows(); + if (!state.currentFlow && state.flows[0]?.id) { + await loadFlow(state.flows[0].id); + } else { + renderAll(); + } +}; + +bindEvents(); +boot().catch((error) => { + elements.flowList.innerHTML = `
${error.message}
`; +}); diff --git a/packages/berryapi/src/modules/studio/studio.routes.ts b/packages/berryapi/src/modules/studio/studio.routes.ts new file mode 100644 index 0000000..476fa3e --- /dev/null +++ b/packages/berryapi/src/modules/studio/studio.routes.ts @@ -0,0 +1,95 @@ +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { dirname, extname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { FastifyReply, FastifyRequest } from "fastify"; + +const currentDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(currentDir, "..", "..", "..", "..", ".."); +const studioAssetsDir = resolve(repoRoot, "packages", "berryapi", "src", "modules", "studio", "assets"); +const studioAppDistDir = resolve(repoRoot, "packages", "studio", "dist"); +const studioAppAssetsDir = join(studioAppDistDir, "assets"); + +const assetMimeTypes: Record = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", +}; + +const getAssetPath = (assetName: string) => { + const normalized = assetName.replace(/[^a-zA-Z0-9._-]/g, ""); + return join(studioAssetsDir, normalized); +}; + +const sendFile = async (reply: FastifyReply, filePath: string) => { + const extension = extname(filePath).toLowerCase(); + const file = await readFile(filePath); + reply.type(assetMimeTypes[extension] ?? "application/octet-stream").send(file); +}; + +const sendLegacyAsset = async (reply: FastifyReply, assetName: string) => { + await sendFile(reply, getAssetPath(assetName)); +}; + +export const registerStudioRoutes = async (app: any) => { + const sendStudioIndex = async (reply: FastifyReply) => { + if (existsSync(join(studioAppDistDir, "index.html"))) { + await sendFile(reply, join(studioAppDistDir, "index.html")); + return; + } + + await sendLegacyAsset(reply, "index.html"); + }; + + app.get("/studio", async (_request: FastifyRequest, reply: FastifyReply) => { + await sendStudioIndex(reply); + }); + + app.get("/studio/assets/:assetName", async (request: FastifyRequest<{ Params: { assetName: string } }>, reply: FastifyReply) => { + const normalized = request.params.assetName.replace(/[^a-zA-Z0-9._-]/g, ""); + const builtAssetPath = join(studioAppAssetsDir, normalized); + if (existsSync(builtAssetPath)) { + await sendFile(reply, builtAssetPath); + return; + } + + await sendLegacyAsset(reply, normalized); + }); + + app.get("/studio/favicon.svg", async (_request: FastifyRequest, reply: FastifyReply) => { + const builtAssetPath = join(studioAppDistDir, "favicon.svg"); + if (existsSync(builtAssetPath)) { + await sendFile(reply, builtAssetPath); + return; + } + + await sendLegacyAsset(reply, "favicon.svg"); + }); + + app.get("/favicon.ico", async (_request: FastifyRequest, reply: FastifyReply) => { + const builtAssetPath = join(studioAppDistDir, "favicon.svg"); + if (existsSync(builtAssetPath)) { + await sendFile(reply, builtAssetPath); + return; + } + + await sendLegacyAsset(reply, "favicon.svg"); + }); + + app.get("/studio/:assetName", async (request: FastifyRequest<{ Params: { assetName: string } }>, reply: FastifyReply) => { + const normalized = request.params.assetName.replace(/[^a-zA-Z0-9._-]/g, ""); + const builtAssetPath = join(studioAppDistDir, normalized); + if (existsSync(builtAssetPath)) { + await sendFile(reply, builtAssetPath); + return; + } + + await sendLegacyAsset(reply, normalized); + }); + + app.get("/studio/*", async (_request: FastifyRequest, reply: FastifyReply) => { + await sendStudioIndex(reply); + }); +}; diff --git a/packages/berryapi/src/modules/webhook/webhook.controller.ts b/packages/berryapi/src/modules/webhook/webhook.controller.ts new file mode 100644 index 0000000..ab6f94e --- /dev/null +++ b/packages/berryapi/src/modules/webhook/webhook.controller.ts @@ -0,0 +1,22 @@ +import type { FastifyReply, FastifyRequest } from "fastify"; +import { validateOrThrow } from "../../utils/zod.js"; +import { successResponse } from "../../utils/response.js"; +import { instanceNameParamsSchema, setWebhookBodySchema } from "./webhook.validators.js"; +import type { WebhookService } from "./webhook.service.js"; + +export class WebhookController { + constructor(private readonly service: WebhookService) {} + + set = async (request: FastifyRequest, reply: FastifyReply) => { + const params = validateOrThrow(instanceNameParamsSchema, request.params); + const body = validateOrThrow(setWebhookBodySchema, request.body); + const data = await this.service.set(params.instanceName, body); + reply.send(successResponse("Webhook updated successfully.", data)); + }; + + find = async (request: FastifyRequest, reply: FastifyReply) => { + const params = validateOrThrow(instanceNameParamsSchema, request.params); + const data = await this.service.find(params.instanceName); + reply.send(successResponse("Webhook fetched successfully.", data)); + }; +} diff --git a/packages/berryapi/src/modules/webhook/webhook.routes.ts b/packages/berryapi/src/modules/webhook/webhook.routes.ts new file mode 100644 index 0000000..80fabd9 --- /dev/null +++ b/packages/berryapi/src/modules/webhook/webhook.routes.ts @@ -0,0 +1,35 @@ +import type { FastifyInstance } from "fastify"; +import { createRouteSchema } from "../../docs/schema.js"; +import type { InstanceManager } from "../../managers/InstanceManager.js"; +import { WebhookController } from "./webhook.controller.js"; +import { WebhookService } from "./webhook.service.js"; +import { instanceNameParamsSchema, setWebhookBodySchema } from "./webhook.validators.js"; + +export const registerWebhookRoutes = async ( + app: any, + manager: InstanceManager, +) => { + const controller = new WebhookController(new WebhookService(manager)); + + app.post("/webhook/set/:instanceName", { + schema: createRouteSchema({ + tags: ["Webhook"], + summary: "Set webhook", + params: instanceNameParamsSchema, + body: setWebhookBodySchema, + example: { + enabled: true, + url: "https://example.com/webhook", + events: ["messages.upsert", "connection.update"], + }, + }), + }, controller.set); + + app.get("/webhook/find/:instanceName", { + schema: createRouteSchema({ + tags: ["Webhook"], + summary: "Find webhook", + params: instanceNameParamsSchema, + }), + }, controller.find); +}; diff --git a/packages/berryapi/src/modules/webhook/webhook.service.ts b/packages/berryapi/src/modules/webhook/webhook.service.ts new file mode 100644 index 0000000..5fe0406 --- /dev/null +++ b/packages/berryapi/src/modules/webhook/webhook.service.ts @@ -0,0 +1,15 @@ +import type { InstanceManager } from "../../managers/InstanceManager.js"; +import type { z } from "zod"; +import type { setWebhookBodySchema } from "./webhook.validators.js"; + +export class WebhookService { + constructor(private readonly manager: InstanceManager) {} + + set(instanceName: string, input: z.infer) { + return this.manager.setWebhook(instanceName, input); + } + + find(instanceName: string) { + return this.manager.getWebhook(instanceName); + } +} diff --git a/packages/berryapi/src/modules/webhook/webhook.validators.ts b/packages/berryapi/src/modules/webhook/webhook.validators.ts new file mode 100644 index 0000000..82a5482 --- /dev/null +++ b/packages/berryapi/src/modules/webhook/webhook.validators.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; +import { instanceNameParamsSchema } from "../instance/instance.validators.js"; + +export { instanceNameParamsSchema }; + +const eventNameSchema = z.enum([ + "connection.update", + "qrcode.updated", + "messages.upsert", + "messages.update", + "messages.delete", + "chats.update", + "contacts.update", + "groups.update", + "presence.update", + "send.message", +]); + +export const setWebhookBodySchema = z.object({ + enabled: z.boolean().default(true), + url: z.string().url(), + events: z.array(eventNameSchema).default([]), + headers: z.record(z.string(), z.string()).optional(), +}); diff --git a/packages/berryapi/src/providers/whatsapp/BerryProtocolProvider.ts b/packages/berryapi/src/providers/whatsapp/BerryProtocolProvider.ts new file mode 100644 index 0000000..620b901 --- /dev/null +++ b/packages/berryapi/src/providers/whatsapp/BerryProtocolProvider.ts @@ -0,0 +1,226 @@ +import BerryProtocol, { type BerrySendRawOptions } from "@berrysdk/core"; +import type { BerryApiEventName, InstanceRecord } from "../../types/index.js"; +import type { + ProviderConnectOptions, + ProviderEventPayload, + ProviderSendOptions, + WhatsAppProvider, +} from "./WhatsAppProvider.js"; +import { env } from "../../config/env.js"; +import { logger } from "../../utils/logger.js"; + +const toEventPayload = ( + event: BerryApiEventName, + instanceName: string, + payload: unknown, +): ProviderEventPayload => ({ + event, + instanceName, + payload, + timestamp: new Date().toISOString(), +}); + +export class BerryProtocolProvider implements WhatsAppProvider { + public readonly instanceName: string; + private readonly client: BerryProtocol; + private listeners = new Set<(event: ProviderEventPayload) => void>(); + private state: Record = { + status: "created", + connectionState: "disconnected", + }; + + constructor(instance: Pick) { + this.instanceName = instance.instanceName; + this.client = new BerryProtocol({ + sessionId: instance.instanceName, + databasePath: env.BERRY_SQLITE_PATH, + authFolder: env.BERRY_AUTH_FOLDER, + printQrInTerminal: false, + logger: logger.child({ module: "provider", instanceName: instance.instanceName }), + }); + this.bindEvents(); + } + + async connect(options: ProviderConnectOptions): Promise { + this.state = { + ...this.state, + status: "connecting", + connectionState: "connecting", + }; + + if (options.authMethod === "pairing_code") { + if (!options.phoneNumber) { + throw new Error("phoneNumber is required for pairing_code authentication."); + } + await this.client.connectWithPairingCode(options.phoneNumber); + return; + } + + if (options.authMethod === "link") { + await this.client.connectWithLink(); + return; + } + + await this.client.connectWithQr(); + } + + async reconnect(): Promise { + await this.client.reconnect(); + } + + async disconnect(): Promise { + await this.client.disconnect(); + this.state = { + ...this.state, + status: "disconnected", + connectionState: "disconnected", + }; + } + + async logout(): Promise { + await this.client.logout(); + this.state = { + ...this.state, + status: "logged_out", + connectionState: "logged_out", + }; + } + + async getConnectionState(): Promise> { + return { + ...this.state, + qrCode: this.client.getQrCode(), + }; + } + + async setPresence( + presence: "available" | "composing" | "recording" | "paused" | "unavailable", + jid?: string, + ): Promise { + await this.client.sendPresence(presence, jid); + } + + async sendText(to: string, text: string): Promise { + return this.client.sendText(to, text); + } + + async sendImage(to: string, payload: Record): Promise { + return this.client.sendImage(to, payload as never); + } + + async sendAudio(to: string, payload: Record): Promise { + return this.client.sendAudio(to, payload as never); + } + + async sendDocument(to: string, payload: Record): Promise { + return this.client.sendDocument(to, payload as never); + } + + async sendButtons(to: string, payload: Record): Promise { + return this.client.sendButtons(to, payload as never); + } + + async sendList(to: string, payload: Record): Promise { + return this.client.sendList(to, payload as never); + } + + async sendCarousel(to: string, payload: Record): Promise { + return this.client.sendCarousel(to, payload as never); + } + + async sendReaction(to: string, emoji: string, targetMessageId: string): Promise { + return this.client.sendReaction(to, emoji, targetMessageId); + } + + async sendLocation(to: string, payload: Record): Promise { + return this.client.sendLocation(to, payload as never); + } + + async sendContact(to: string, payload: Record): Promise { + return this.client.sendContact(to, payload as never); + } + + async editMessage(to: string, messageId: string, text: string): Promise { + return this.client.editMessage(to, messageId, text); + } + + async sendRaw( + to: string, + content: Record, + options?: ProviderSendOptions, + ): Promise { + return this.client.sendRaw(to, content, options as BerrySendRawOptions | undefined); + } + + async fetchGroups(): Promise { + return this.client.fetchGroups(); + } + + onEvent(listener: (event: ProviderEventPayload) => void): void { + this.listeners.add(listener); + } + + private emit(event: BerryApiEventName, payload: unknown): void { + const envelope = toEventPayload(event, this.instanceName, payload); + for (const listener of this.listeners) { + listener(envelope); + } + } + + private bindEvents(): void { + this.client.on("connection.open", (payload) => { + this.state = { + ...this.state, + status: "connected", + connectionState: "open", + }; + this.emit("connection.update", payload); + }); + + this.client.on("connection.close", (payload) => { + this.state = { + ...this.state, + status: "disconnected", + connectionState: "closed", + }; + this.emit("connection.update", payload); + }); + + this.client.on("connection.reconnecting", (payload) => { + this.state = { + ...this.state, + status: "restarting", + connectionState: "reconnecting", + }; + this.emit("connection.update", payload); + }); + + this.client.on("auth.qr", (payload) => { + this.state = { + ...this.state, + status: "qr_pending", + connectionState: "qr", + qrCode: payload.value, + }; + this.emit("qrcode.updated", payload); + }); + + this.client.on("auth.pairing_code", (payload) => { + this.state = { + ...this.state, + status: "qr_pending", + connectionState: "pairing_code", + pairingCode: payload.code, + }; + this.emit("qrcode.updated", payload); + }); + + this.client.on("message.received", (payload) => this.emit("messages.upsert", payload)); + this.client.on("message.ack", (payload) => this.emit("messages.update", payload)); + this.client.on("chats.update", (payload) => this.emit("chats.update", payload)); + this.client.on("sync.contacts", (payload) => this.emit("contacts.update", payload)); + this.client.on("sync.groups", (payload) => this.emit("groups.update", payload)); + this.client.on("presence.update", (payload) => this.emit("presence.update", payload)); + this.client.on("message.sent", (payload) => this.emit("send.message", payload)); + } +} diff --git a/packages/berryapi/src/providers/whatsapp/WhatsAppProvider.ts b/packages/berryapi/src/providers/whatsapp/WhatsAppProvider.ts new file mode 100644 index 0000000..0578f56 --- /dev/null +++ b/packages/berryapi/src/providers/whatsapp/WhatsAppProvider.ts @@ -0,0 +1,47 @@ +import type { BerryApiEventName, InstanceRecord } from "../../types/index.js"; + +export interface ProviderSendOptions { + quoted?: unknown; + mentions?: string[]; + contextInfo?: Record; + ephemeralExpiration?: number; + forwardingScore?: number; + statusJidList?: string[]; + [key: string]: unknown; +} + +export interface ProviderConnectOptions { + authMethod: InstanceRecord["authMethod"]; + phoneNumber?: string; +} + +export interface ProviderEventPayload { + event: BerryApiEventName; + instanceName: string; + payload: unknown; + timestamp: string; +} + +export interface WhatsAppProvider { + readonly instanceName: string; + connect(options: ProviderConnectOptions): Promise; + reconnect(): Promise; + disconnect(): Promise; + logout(): Promise; + getConnectionState(): Promise>; + setPresence(presence: "available" | "composing" | "recording" | "paused" | "unavailable", jid?: string): Promise; + sendText(to: string, text: string): Promise; + sendImage(to: string, payload: Record): Promise; + sendAudio(to: string, payload: Record): Promise; + sendDocument(to: string, payload: Record): Promise; + sendButtons(to: string, payload: Record): Promise; + sendList(to: string, payload: Record): Promise; + sendCarousel(to: string, payload: Record): Promise; + sendReaction(to: string, emoji: string, targetMessageId: string): Promise; + sendLocation(to: string, payload: Record): Promise; + sendContact(to: string, payload: Record): Promise; + editMessage(to: string, messageId: string, text: string): Promise; + sendRaw(to: string, content: Record, options?: ProviderSendOptions): Promise; + fetchGroups(): Promise; + onEvent(listener: (event: ProviderEventPayload) => void): void; +} diff --git a/packages/berryapi/src/realtime/RealtimeGateway.ts b/packages/berryapi/src/realtime/RealtimeGateway.ts new file mode 100644 index 0000000..2349edf --- /dev/null +++ b/packages/berryapi/src/realtime/RealtimeGateway.ts @@ -0,0 +1,84 @@ +import type { FastifyInstance } from "fastify"; +import { env } from "../config/env.js"; +import type { ProviderEventPayload } from "../providers/whatsapp/WhatsAppProvider.js"; +import { logger } from "../utils/logger.js"; + +type Subscriber = { + socket: { + send: (payload: string) => void; + close: () => void; + readyState: number; + OPEN: number; + on: (event: string, listener: (...args: any[]) => void) => void; + }; + instanceName?: string; +}; + +export class RealtimeGateway { + private readonly subscribers = new Set(); + + register(app: FastifyInstance): void { + app.get( + env.BERRY_WS_PATH, + { websocket: true }, + (socket, request) => { + const url = new URL(request.url, `http://${request.headers.host ?? "localhost"}`); + const apiKey = + url.searchParams.get("apiKey") + ?? request.headers.authorization?.replace(/^Bearer\s+/i, "").trim(); + if (apiKey !== env.API_KEY) { + socket.send( + JSON.stringify({ + success: false, + message: "Invalid API key for realtime gateway.", + error: { code: "UNAUTHORIZED" }, + }), + ); + socket.close(); + return; + } + const instanceName = url.searchParams.get("instanceName") ?? undefined; + const subscriber: Subscriber = { + socket, + instanceName, + }; + this.subscribers.add(subscriber); + + socket.on("close", () => { + this.subscribers.delete(subscriber); + }); + + socket.on("error", (error: unknown) => { + logger.warn({ err: error }, "realtime socket error"); + this.subscribers.delete(subscriber); + }); + + socket.send( + JSON.stringify({ + success: true, + message: "Connected to BerryAPI realtime gateway.", + data: { instanceName }, + }), + ); + }, + ); + } + + broadcast(event: ProviderEventPayload): void { + const payload = JSON.stringify({ + success: true, + message: "Realtime event delivered.", + data: event, + }); + + for (const subscriber of this.subscribers) { + if (subscriber.instanceName && subscriber.instanceName !== event.instanceName) { + continue; + } + + if (subscriber.socket.readyState === subscriber.socket.OPEN) { + subscriber.socket.send(payload); + } + } + } +} diff --git a/packages/berryapi/src/server.ts b/packages/berryapi/src/server.ts new file mode 100644 index 0000000..73a013f --- /dev/null +++ b/packages/berryapi/src/server.ts @@ -0,0 +1,17 @@ +import { buildApp } from "./app.js"; +import { env } from "./config/env.js"; +import { logger } from "./utils/logger.js"; + +const start = async () => { + const app = await buildApp(); + await app.listen({ + host: env.HOST, + port: env.PORT, + }); + logger.info({ host: env.HOST, port: env.PORT }, "BerryAPI server started"); +}; + +start().catch((error) => { + logger.error({ err: error }, "failed to start BerryAPI"); + process.exitCode = 1; +}); diff --git a/packages/berryapi/src/types/index.ts b/packages/berryapi/src/types/index.ts new file mode 100644 index 0000000..b7f8e15 --- /dev/null +++ b/packages/berryapi/src/types/index.ts @@ -0,0 +1,53 @@ +export type BerryApiEventName = + | "connection.update" + | "qrcode.updated" + | "messages.upsert" + | "messages.update" + | "messages.delete" + | "chats.update" + | "contacts.update" + | "groups.update" + | "presence.update" + | "send.message"; + +export type InstanceStatus = + | "created" + | "connecting" + | "connected" + | "disconnected" + | "qr_pending" + | "restarting" + | "logged_out"; + +export interface InstanceSettings { + rejectCall?: boolean; + msgRetryCounterCache?: boolean; + alwaysOnline?: boolean; + readMessages?: boolean; + syncFullHistory?: boolean; + webhookByEvents?: boolean; + events?: BerryApiEventName[]; + privacy?: Record; +} + +export interface WebhookConfig { + enabled: boolean; + url?: string; + events: BerryApiEventName[]; + headers?: Record; +} + +export interface InstanceRecord { + instanceName: string; + provider: "berryprotocol"; + status: InstanceStatus; + connectionState: string; + authMethod: "qr" | "pairing_code" | "link"; + phoneNumber?: string; + qrCode?: string; + pairingCode?: string; + webhook: WebhookConfig; + settings: InstanceSettings; + createdAt: string; + updatedAt: string; +} diff --git a/packages/berryapi/src/utils/http-error.ts b/packages/berryapi/src/utils/http-error.ts new file mode 100644 index 0000000..014ea39 --- /dev/null +++ b/packages/berryapi/src/utils/http-error.ts @@ -0,0 +1,10 @@ +export class HttpError extends Error { + constructor( + public readonly statusCode: number, + message: string, + public readonly details?: unknown, + ) { + super(message); + this.name = "HttpError"; + } +} diff --git a/packages/berryapi/src/utils/logger.ts b/packages/berryapi/src/utils/logger.ts new file mode 100644 index 0000000..211d320 --- /dev/null +++ b/packages/berryapi/src/utils/logger.ts @@ -0,0 +1,7 @@ +import pino from "pino"; +import { env } from "../config/env.js"; + +export const logger = pino({ + name: "berryapi", + level: env.LOG_LEVEL, +}); diff --git a/packages/berryapi/src/utils/response.ts b/packages/berryapi/src/utils/response.ts new file mode 100644 index 0000000..e29b42e --- /dev/null +++ b/packages/berryapi/src/utils/response.ts @@ -0,0 +1,23 @@ +export interface ApiSuccess { + success: true; + message: string; + data: T; +} + +export interface ApiFailure { + success: false; + message: string; + error: unknown; +} + +export const successResponse = (message: string, data: T): ApiSuccess => ({ + success: true, + message, + data, +}); + +export const errorResponse = (message: string, error: unknown): ApiFailure => ({ + success: false, + message, + error, +}); diff --git a/packages/berryapi/src/utils/zod.ts b/packages/berryapi/src/utils/zod.ts new file mode 100644 index 0000000..6ea3087 --- /dev/null +++ b/packages/berryapi/src/utils/zod.ts @@ -0,0 +1,20 @@ +import type { ZodTypeAny } from "zod"; +import { z } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { HttpError } from "./http-error.js"; + +export const validateOrThrow = (schema: T, payload: unknown): z.infer => { + const result = schema.safeParse(payload); + if (!result.success) { + throw new HttpError(400, "Validation failed", result.error.flatten()); + } + + return result.data; +}; + +export const toOpenApiSchema = (schema: ZodTypeAny, name: string) => + zodToJsonSchema(schema as any, { + name, + target: "openApi3", + $refStrategy: "none", + }); diff --git a/packages/berryapi/src/webhook/WebhookDispatcher.ts b/packages/berryapi/src/webhook/WebhookDispatcher.ts new file mode 100644 index 0000000..1ab7494 --- /dev/null +++ b/packages/berryapi/src/webhook/WebhookDispatcher.ts @@ -0,0 +1,35 @@ +import { env } from "../config/env.js"; +import { logger } from "../utils/logger.js"; +import type { ProviderEventPayload } from "../providers/whatsapp/WhatsAppProvider.js"; +import type { WebhookConfig } from "../types/index.js"; + +export class WebhookDispatcher { + async dispatch(config: WebhookConfig, event: ProviderEventPayload): Promise { + if (!config.enabled || !config.url) { + return; + } + + if (config.events.length && !config.events.includes(event.event)) { + return; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), env.BERRY_WEBHOOK_TIMEOUT_MS); + + try { + await fetch(config.url, { + method: "POST", + headers: { + "content-type": "application/json", + ...(config.headers ?? {}), + }, + body: JSON.stringify(event), + signal: controller.signal, + }); + } catch (error) { + logger.warn({ err: error, url: config.url, event: event.event }, "webhook dispatch failed"); + } finally { + clearTimeout(timeout); + } + } +} diff --git a/packages/berryapi/tsconfig.json b/packages/berryapi/tsconfig.json new file mode 100644 index 0000000..97425ed --- /dev/null +++ b/packages/berryapi/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core" + } + ] +} diff --git a/packages/berryotp/README.md b/packages/berryotp/README.md new file mode 100644 index 0000000..a15c285 --- /dev/null +++ b/packages/berryotp/README.md @@ -0,0 +1,16 @@ +# berryotp + +Facade package for the official BerryOTP library. + +## Install + +```bash +npm install berryotp +``` + +## Usage + +```ts +import BerryProtocol from "berryprotocol"; +import { BerryOTP } from "berryotp"; +``` diff --git a/packages/berryotp/package.json b/packages/berryotp/package.json new file mode 100644 index 0000000..a27dc87 --- /dev/null +++ b/packages/berryotp/package.json @@ -0,0 +1,31 @@ +{ + "name": "berryotp", + "version": "0.1.9", + "description": "Facade package for BerryOTP so consumers can import it without the @berrysdk scope.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "README.md" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@berrysdk/berry-otp": "^0.1.9" + }, + "peerDependencies": { + "berryprotocol": "^0.1.9" + } +} diff --git a/packages/berryotp/src/index.ts b/packages/berryotp/src/index.ts new file mode 100644 index 0000000..1d103a7 --- /dev/null +++ b/packages/berryotp/src/index.ts @@ -0,0 +1,9 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +export { BerryOTP, default } from "@berrysdk/berry-otp"; +export * from "@berrysdk/berry-otp"; diff --git a/packages/berryotp/tsconfig.json b/packages/berryotp/tsconfig.json new file mode 100644 index 0000000..01f106c --- /dev/null +++ b/packages/berryotp/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@berrysdk/berry-otp": [ + "../berry-otp/dist/index.d.ts" + ] + }, + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ] +} diff --git a/packages/berryprotocol/README.md b/packages/berryprotocol/README.md new file mode 100644 index 0000000..700d4b3 --- /dev/null +++ b/packages/berryprotocol/README.md @@ -0,0 +1,25 @@ +# berryprotocol + +Facade package for the Berry SDK. + +## Install + +```bash +npm install berryprotocol +``` + +## Usage + +```ts +import BerryProtocol from "berryprotocol"; +``` + +For OTP flows, install the official companion package: + +```bash +npm install berryotp +``` + +```ts +import { BerryOTP } from "berryotp"; +``` diff --git a/packages/berryprotocol/package.json b/packages/berryprotocol/package.json new file mode 100644 index 0000000..0286341 --- /dev/null +++ b/packages/berryprotocol/package.json @@ -0,0 +1,31 @@ +{ + "name": "berryprotocol", + "version": "0.1.10", + "description": "Facade package for Berry SDK that exposes BerryProtocol as the main SDK entrypoint.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "engines": { + "node": "20.x || >=22 <27" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "README.md" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@berrysdk/core": "^0.1.10" + } +} diff --git a/packages/berryprotocol/src/index.ts b/packages/berryprotocol/src/index.ts new file mode 100644 index 0000000..2e4887d --- /dev/null +++ b/packages/berryprotocol/src/index.ts @@ -0,0 +1,14 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +export { + BerryClient, + BerryProtocol, + default, +} from "@berrysdk/core"; + +export * from "@berrysdk/core"; diff --git a/packages/berryprotocol/tsconfig.json b/packages/berryprotocol/tsconfig.json new file mode 100644 index 0000000..a8410eb --- /dev/null +++ b/packages/berryprotocol/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@berrysdk/core": [ + "../core/dist/index.d.ts" + ] + }, + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ] +} diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..746a01a --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,27 @@ +{ + "name": "@berrysdk/cli", + "version": "0.1.9", + "description": "CLI tooling for Berry Protocol sessions and message testing.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@berrysdk/core": "^0.1.9", + "commander": "^14.0.0", + "pino": "^9.6.0" + } +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000..33c7c75 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,111 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import { Command } from "commander"; +import qrcode from "qrcode-terminal"; +import { BerryClient, type BerryAuthMethod } from "@berrysdk/core"; + +const program = new Command(); + +const createClient = (sessionId: string, method: BerryAuthMethod, phoneNumber?: string) => + new BerryClient({ + sessionId, + authFolder: process.env.BERRY_AUTH_FOLDER, + auth: { + method, + phoneNumber, + }, + }); + +const formatError = (error: unknown): string => (error instanceof Error ? error.message : String(error)); + +const attachCommonListeners = ( + client: BerryClient, + method: BerryAuthMethod, + phoneNumber?: string, +): void => { + client.on("auth.link", ({ value }) => { + if (method === "link") { + console.log(value); + } + }); + + client.on("auth.qr", ({ value }) => { + if (method === "qr") { + qrcode.generate(value, { small: true }); + } + }); + + client.on("auth.pairing_code", ({ code }) => { + if (method === "pairing_code") { + console.log(`Pairing code (${phoneNumber}): ${code}`); + } + }); + + client.on("connection.open", (state) => console.log("open", state)); + client.on("connection.close", (state) => console.log("close", state)); + client.on("message.received", (message) => console.log(JSON.stringify(message, null, 2))); + client.on("auth.error", (error) => console.error(error)); +}; + +program.name("berry").description("BerryProtocol CLI").requiredOption("-s, --session ", "session id"); + +program + .command("connect") + .option("--auth ", "auth method: link | qr | pairing_code", "link") + .option("--phone ", "phone number for pairing code") + .option("--pairing-code ", "custom pairing code with 8 numeric digits") + .action(async (options, command) => { + const root = command.parent?.opts() as { session?: string } | undefined; + if (!root?.session) { + throw new Error("Session id is required."); + } + + const method = options.auth as BerryAuthMethod; + if (method === "pairing_code" && !options.phone) { + throw new Error("--phone is required when --auth pairing_code is used."); + } + + const client = createClient(root.session, method, options.phone); + attachCommonListeners(client, method, options.phone); + try { + await client.connect({ + method, + phoneNumber: options.phone, + customPairingCode: options.pairingCode, + }); + } catch (error) { + console.error(`Connection failed: ${formatError(error)}`); + if (method === "pairing_code") { + console.error( + "Tip: use a new --session or logout the existing one before requesting a new pairing code.", + ); + } + process.exitCode = 1; + } + }); + +program + .command("send-text") + .requiredOption("-t, --to ", "recipient jid") + .requiredOption("-m, --message ", "text") + .action(async (options, command) => { + const root = command.parent?.opts() as { session?: string } | undefined; + if (!root?.session) { + throw new Error("Session id is required."); + } + + const client = new BerryClient({ + sessionId: root.session, + authFolder: process.env.BERRY_AUTH_FOLDER, + }); + await client.connect(); + await client.sendText(options.to, options.message); + await client.disconnect(); + }); + +program.parseAsync(process.argv); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000..75647e1 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core" + } + ] +} + diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..3c19bd8 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,37 @@ +{ + "name": "@berrysdk/core", + "version": "0.1.10", + "description": "Public SDK for Berry Protocol WhatsApp integrations.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "engines": { + "node": "20.x || >=22 <27" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@berrysdk/auth": "^0.1.2", + "@berrysdk/events": "^0.1.4", + "@berrysdk/media": "^0.1.2", + "@berrysdk/messages": "^0.1.3", + "@berrysdk/socket": "^0.1.10", + "@berrysdk/store": "^0.1.4", + "pino": "^9.6.0", + "qrcode-terminal": "^0.12.0" + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..f60b7cf --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,912 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import pino, { type Logger } from "pino"; +import qrcode from "qrcode-terminal"; +import { randomUUID } from "node:crypto"; +import { type URL } from "node:url"; +import type { AnyMessageContent } from "@berrysdk/transport"; +import { SessionManager, SQLiteSessionStore } from "@berrysdk/auth"; +import { + type BerryAuthOptions, + BerryEventBus, + type BerryEventMap, + type ButtonsPayload, + type CarouselCard, + type CarouselCardType, + type CarouselMessagePayload, + type ContactPayload, + type IncomingMessage, + type InteractivePayload, + type ListPayload, + type LocationPayload, + type MediaPayload, + type MessageAck, + type OutgoingMessage, + type PresenceRecord, +} from "@berrysdk/events"; +import { MediaManager } from "@berrysdk/media"; +import { + createContactMessage, + createCarouselMessage, + createLocationMessage, + createReactionMessage, + createTextMessage, +} from "@berrysdk/messages"; +import { BerrySocket } from "@berrysdk/socket"; +import { SQLiteStore } from "@berrysdk/store"; + +export interface BerryClientOptions { + sessionId: string; + databasePath?: string; + authFolder?: string; + auth?: BerryAuthOptions; + logger?: Logger; + reconnectMaxAttempts?: number; + reconnectDelayMs?: number; + printQrInTerminal?: boolean; + qrSmall?: boolean; +} + +export interface ConnectFlowOptions { + waitForConnection?: boolean; +} + +type BerryMediaSource = Buffer | { url: string | URL }; +const MAX_CAROUSEL_CARDS = 10; + +const assertWhatsAppJid = (jid: string): string => { + const normalized = jid.trim(); + if (!normalized || !normalized.includes("@")) { + throw new Error( + `Invalid WhatsApp JID "${jid}". Use a private, group, newsletter or status JID like "5511999999999@s.whatsapp.net".`, + ); + } + + return normalized; +}; + +export interface BerrySendMessageContent { + ai?: boolean; + text?: string; + cards?: CarouselCard[]; + carouselCardType?: CarouselCardType; + image?: BerryMediaSource; + audio?: BerryMediaSource; + document?: BerryMediaSource; + caption?: string; + mimetype?: string; + fileName?: string; + ptt?: boolean; + footer?: string; + buttons?: Array<{ + buttonId?: string; + buttonText?: { displayText?: string }; + type?: number; + }>; + headerType?: number; + react?: { + text?: string; + key?: { + id?: string; + remoteJid?: string; + }; + }; + location?: { + degreesLatitude?: number; + degreesLongitude?: number; + name?: string; + address?: string; + }; + contacts?: { + displayName?: string; + contacts: Array<{ + displayName?: string; + vcard?: string; + }>; + }; + list?: ListPayload; + listMessage?: ListPayload; + buttonsMessage?: ButtonsPayload; + interactiveMessage?: InteractivePayload; + [key: string]: unknown; +} + +export interface BerrySendRawOptions { + quoted?: unknown; + mentions?: string[]; + contextInfo?: Record; + ephemeralExpiration?: number; + forwardingScore?: number; + statusJidList?: string[]; + [key: string]: unknown; +} + +class MessageQueue { + private tail = Promise.resolve(); + + enqueue(job: () => Promise): Promise { + const next = this.tail.then(job, job); + this.tail = next.then(() => undefined, () => undefined); + return next; + } +} + +export class BerryClient { + private readonly logger: Logger; + private readonly store: SQLiteStore; + private readonly sessions: SessionManager; + private readonly media = new MediaManager(); + private readonly bus = new BerryEventBus(); + private readonly queue = new MessageQueue(); + private readonly socket: BerrySocket; + private lastQr?: string; + + constructor(private readonly options: BerryClientOptions) { + this.logger = options.logger ?? pino({ name: "berry-client" }); + this.store = new SQLiteStore(options.databasePath); + this.sessions = new SessionManager(new SQLiteSessionStore(this.store)); + this.socket = new BerrySocket( + { + sessionId: options.sessionId, + logger: this.logger, + reconnectDelayMs: options.reconnectDelayMs, + reconnectMaxAttempts: options.reconnectMaxAttempts, + authFolder: options.authFolder, + auth: options.auth, + }, + this.bus, + ); + this.bindInternals(); + } + + on( + event: EventName, + listener: (payload: BerryEventMap[EventName]) => void, + ): this { + this.bus.on(event, listener); + return this; + } + + once( + event: EventName, + listener: (payload: BerryEventMap[EventName]) => void, + ): this { + this.bus.once(event, listener); + return this; + } + + off( + event: EventName, + listener: (payload: BerryEventMap[EventName]) => void, + ): this { + this.bus.off(event, listener); + return this; + } + + async connect(auth?: BerryAuthOptions): Promise { + await this.sessions.get(this.options.sessionId); + await this.socket.connect(auth); + } + + async connectWithLink(options?: ConnectFlowOptions): Promise { + await this.connectForAuthFlow({ method: "link" }, "auth.link", options); + } + + async connectWithQr(options?: ConnectFlowOptions): Promise { + await this.connectForAuthFlow({ method: "qr" }, "auth.qr", options); + } + + async connectWithPairingCode( + phoneNumber: string, + customPairingCode?: string, + options?: ConnectFlowOptions, + ): Promise { + await this.connectForAuthFlow( + { + method: "pairing_code", + phoneNumber, + customPairingCode, + }, + "auth.pairing_code", + options, + ); + } + + async waitUntilConnected(timeoutMs = 120_000): Promise { + if (this.socket.isConnected()) { + return; + } + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + cleanup(); + reject(new Error(`Timed out waiting for WhatsApp connection after ${timeoutMs}ms.`)); + }, timeoutMs); + + const cleanup = () => { + clearTimeout(timeout); + this.bus.off("connection.open", handleOpen); + this.bus.off("auth.error", handleAuthError); + this.bus.off("connection.close", handleClose); + }; + + const handleOpen = () => { + cleanup(); + resolve(); + }; + + const handleAuthError = ({ error }: BerryEventMap["auth.error"]) => { + cleanup(); + reject(new Error(error)); + }; + + const handleClose = ({ reason }: BerryEventMap["connection.close"]) => { + if (reason === "restart_required") { + return; + } + + cleanup(); + reject(new Error(`WhatsApp connection closed before opening (${reason}).`)); + }; + + this.bus.on("connection.open", handleOpen); + this.bus.on("auth.error", handleAuthError); + this.bus.on("connection.close", handleClose); + }); + } + + async disconnect(): Promise { + await this.socket.disconnect(); + } + + async reconnect(): Promise { + await this.socket.reconnect(); + } + + async logout(): Promise { + await this.socket.logout(); + await this.sessions.clear(this.options.sessionId); + } + + getQrCode(): string | undefined { + return this.lastQr; + } + + async sendText(to: string, text: string): Promise { + return this.sendMessage(to, { text }); + } + + async sendImage(to: string, media: MediaPayload): Promise { + const loaded = await this.media.load(media); + return this.sendMessage(to, { + image: loaded.buffer, + caption: media.caption, + mimetype: media.mimetype ?? loaded.metadata.mimetype, + fileName: media.fileName ?? loaded.metadata.fileName, + }); + } + + async sendAudio(to: string, media: MediaPayload): Promise { + const loaded = await this.media.load(media); + return this.sendMessage(to, { + audio: loaded.buffer, + mimetype: media.mimetype ?? loaded.metadata.mimetype, + ptt: false, + }); + } + + async sendDocument(to: string, media: MediaPayload): Promise { + const loaded = await this.media.load(media); + return this.sendMessage(to, { + document: loaded.buffer, + mimetype: media.mimetype ?? loaded.metadata.mimetype ?? "application/octet-stream", + fileName: media.fileName ?? loaded.metadata.fileName, + caption: media.caption, + }); + } + + async sendButtons(to: string, payload: ButtonsPayload): Promise { + return this.sendMessage(to, { buttonsMessage: payload }); + } + + async sendList(to: string, list: ListPayload): Promise { + return this.sendMessage(to, { list }); + } + + async sendCarousel( + to: string, + payload: CarouselMessagePayload, + ): Promise { + return this.sendMessage(to, payload as BerrySendMessageContent); + } + + async sendLegacyButtons(to: string, payload: ButtonsPayload): Promise { + return this.queue.enqueue(async () => { + const message: OutgoingMessage = { + id: randomUUID(), + to, + timestamp: new Date().toISOString(), + ack: "pending", + type: "buttons", + buttons: payload, + }; + + const result = await this.socket.sendLegacyButtonsMessage(to, payload); + const sent = { + ...message, + id: result.key.id ?? message.id, + ack: "sent" as const, + }; + this.bus.emit("message.sent", sent); + return sent; + }); + } + + async sendReaction( + to: string, + emoji: string, + targetMessageId: string, + ): Promise { + return this.sendMessage(to, { + react: { + text: emoji, + key: { + id: targetMessageId, + remoteJid: to, + }, + }, + }); + } + + async sendLocation(to: string, location: LocationPayload): Promise { + return this.sendMessage(to, { + location: { + degreesLatitude: location.latitude, + degreesLongitude: location.longitude, + name: location.name, + address: location.address, + }, + }); + } + + async sendContact(to: string, contact: ContactPayload): Promise { + return this.sendMessage(to, { + contacts: { + displayName: contact.displayName, + contacts: [ + { + displayName: contact.displayName, + vcard: contact.vcard, + }, + ], + }, + }); + } + + async editMessage(to: string, messageId: string, text: string): Promise { + return this.queue.enqueue(async () => { + const message = createTextMessage(to, text); + const result = await this.socket.editMessage(to, messageId, text); + const sent = { + ...message, + id: result.key.id ?? message.id, + ack: "sent" as const, + }; + this.bus.emit("message.sent", sent); + return sent; + }); + } + + async fetchGroups() { + return this.socket.fetchGroups(); + } + + async subscribePresence(jid: string): Promise { + await this.socket.subscribePresence(jid); + } + + async sendPresence( + status: "available" | "composing" | "recording" | "paused" | "unavailable", + jid?: string, + ): Promise { + await this.socket.sendPresenceUpdate(status, jid); + } + + async sendMessage( + to: string, + content: BerrySendMessageContent, + ): Promise { + return this.queue.enqueue(async () => { + const recipientJid = assertWhatsAppJid(to); + const normalized = await this.normalizeOutgoingMessage(to, content); + const result = await normalized.dispatch(recipientJid); + const sent = { + ...normalized.message, + id: result.key.id ?? normalized.message.id, + ack: "sent" as const, + }; + this.bus.emit("message.sent", sent); + return sent; + }); + } + + async sendRaw( + to: string, + content: Record, + options?: BerrySendRawOptions, + ): Promise { + return this.queue.enqueue(async () => { + const recipientJid = assertWhatsAppJid(to); + const result = await this.socket.sendTransportMessage( + recipientJid, + content as AnyMessageContent, + options as Record | undefined, + ); + + const sent = { + id: result.key.id ?? randomUUID(), + to: recipientJid, + timestamp: new Date().toISOString(), + ack: "sent", + type: "interactive", + } as OutgoingMessage; + this.bus.emit("message.sent", sent); + return sent; + }); + } + + private async normalizeOutgoingMessage( + to: string, + content: BerrySendMessageContent, + ): Promise<{ + message: OutgoingMessage; + dispatch: (recipientJid: string) => Promise<{ key: { id?: string | null } }>; + }> { + if (content.cards?.length) { + const carousel = await this.normalizeCarouselPayload(content); + return { + message: createCarouselMessage(to, carousel), + dispatch: (recipientJid) => + this.socket.sendCarouselMessage(recipientJid, carousel, { + ai: content.ai, + }), + }; + } + + if (typeof content.text === "string") { + const text = content.text; + const message = createTextMessage(to, text); + return { + message, + dispatch: (recipientJid) => + this.socket.sendTransportMessage(recipientJid, { text, ...(content.ai ? { ai: true } : {}) }), + }; + } + + if (content.image) { + const media = await this.loadMediaPayload(content.image, { + caption: content.caption, + mimetype: content.mimetype, + fileName: content.fileName, + }); + return { + message: { + id: randomUUID(), + to, + timestamp: new Date().toISOString(), + ack: "pending", + type: "image", + media, + }, + dispatch: (recipientJid) => + this.socket.sendTransportMessage(recipientJid, { + ...content, + image: media.buffer!, + caption: media.caption, + mimetype: media.mimetype, + fileName: media.fileName, + }), + }; + } + + if (content.audio) { + const media = await this.loadMediaPayload(content.audio, { + caption: undefined, + mimetype: content.mimetype, + fileName: content.fileName, + }); + return { + message: { + id: randomUUID(), + to, + timestamp: new Date().toISOString(), + ack: "pending", + type: "audio", + media, + }, + dispatch: (recipientJid) => + this.socket.sendTransportMessage(recipientJid, { + ...content, + audio: media.buffer!, + mimetype: media.mimetype, + }), + }; + } + + if (content.document) { + const media = await this.loadMediaPayload(content.document, { + caption: content.caption, + mimetype: content.mimetype, + fileName: content.fileName, + }); + return { + message: { + id: randomUUID(), + to, + timestamp: new Date().toISOString(), + ack: "pending", + type: "document", + media, + }, + dispatch: (recipientJid) => + this.socket.sendTransportMessage(recipientJid, { + ...content, + document: media.buffer!, + caption: media.caption, + mimetype: media.mimetype ?? "application/octet-stream", + fileName: media.fileName, + }), + }; + } + + if (content.buttonsMessage) { + const message: OutgoingMessage = { + id: randomUUID(), + to, + timestamp: new Date().toISOString(), + ack: "pending", + type: "buttons", + buttons: content.buttonsMessage, + }; + + return { + message, + dispatch: (recipientJid) => this.socket.sendReplyButtonsMessage(recipientJid, content.buttonsMessage!), + }; + } + + const list = content.list ?? content.listMessage; + if (list) { + const message: OutgoingMessage = { + id: randomUUID(), + to, + timestamp: new Date().toISOString(), + ack: "pending", + type: "list", + list, + }; + + return { + message, + dispatch: (recipientJid) => this.socket.sendListMessage(recipientJid, list), + }; + } + + if (content.interactiveMessage) { + const message: OutgoingMessage = { + id: randomUUID(), + to, + timestamp: new Date().toISOString(), + ack: "pending", + type: "interactive", + interactive: content.interactiveMessage, + }; + + return { + message, + dispatch: (recipientJid) => this.socket.sendInteractiveMessage(recipientJid, content.interactiveMessage!), + }; + } + + if (content.react) { + const message = createReactionMessage( + to, + content.react.text ?? "", + content.react.key?.id ?? "", + ); + return { + message, + dispatch: (recipientJid) => + this.socket.sendTransportMessage(recipientJid, { + react: content.react!, + ...(content.ai ? { ai: true } : {}), + }), + }; + } + + if (content.location) { + const location = { + latitude: content.location.degreesLatitude ?? 0, + longitude: content.location.degreesLongitude ?? 0, + name: content.location.name ?? undefined, + address: content.location.address ?? undefined, + }; + return { + message: createLocationMessage(to, location), + dispatch: (recipientJid) => + this.socket.sendTransportMessage(recipientJid, { + location: content.location!, + ...(content.ai ? { ai: true } : {}), + }), + }; + } + + if (content.contacts?.contacts?.[0]) { + const contact = { + displayName: content.contacts.displayName ?? content.contacts.contacts[0].displayName ?? "", + vcard: content.contacts.contacts[0].vcard ?? "", + }; + return { + message: createContactMessage(to, contact), + dispatch: (recipientJid) => + this.socket.sendTransportMessage(recipientJid, { + contacts: content.contacts!, + ...(content.ai ? { ai: true } : {}), + }), + }; + } + + throw new Error( + "Unsupported message content. Use text, image, audio, document, buttonsMessage, list/listMessage, cards, interactiveMessage, react, location or contacts.", + ); + } + + private async loadMediaPayload( + source: BerryMediaSource, + extras: Pick, + ): Promise { + if (Buffer.isBuffer(source)) { + return { + ...extras, + buffer: source, + }; + } + + if (typeof source === "object" && source && "url" in source) { + const loaded = await this.media.load({ + url: String(source.url), + ...extras, + }); + return { + ...extras, + buffer: loaded.buffer, + mimetype: extras.mimetype ?? loaded.metadata.mimetype, + fileName: extras.fileName ?? loaded.metadata.fileName, + }; + } + + throw new Error("Only Buffer or { url } media inputs are supported for BerryProtocol-compatible sends."); + } + + private async normalizeCarouselPayload( + content: Pick, + ): Promise { + const cards = content.cards ?? []; + if (!cards.length) { + throw new Error("Carousel payload requires at least one card."); + } + + if (cards.length > MAX_CAROUSEL_CARDS) { + throw new Error(`Carousel payload supports at most ${MAX_CAROUSEL_CARDS} cards.`); + } + + this.assertCarouselCardType(content.carouselCardType, cards); + this.assertCarouselCardMedia(cards); + + return { + text: content.text ?? "", + footer: content.footer, + carouselCardType: content.carouselCardType, + ai: content.ai, + cards: await Promise.all(cards.map((card) => this.normalizeCarouselCard(card))), + }; + } + + private assertCarouselCardMedia(cards: CarouselCard[]): void { + for (const [index, card] of cards.entries()) { + const hasImage = !!card.image; + const hasVideo = !!card.video; + + if (!hasImage && !hasVideo) { + throw new Error(`Carousel card ${index + 1} must contain image or video.`); + } + + if (hasImage && hasVideo) { + throw new Error(`Carousel card ${index + 1} cannot contain both image and video.`); + } + } + } + + private assertCarouselCardType( + carouselCardType: CarouselCardType | undefined, + cards: CarouselCard[], + ): void { + if (!carouselCardType || carouselCardType === "mixed") { + return; + } + + for (const [index, card] of cards.entries()) { + if (carouselCardType === "image" && card.video) { + throw new Error(`Carousel card ${index + 1} contains video but carouselCardType is "image".`); + } + + if (carouselCardType === "video" && card.image) { + throw new Error(`Carousel card ${index + 1} contains image but carouselCardType is "video".`); + } + } + } + + private async normalizeCarouselCard(card: CarouselCard): Promise { + return { + ...card, + image: await this.normalizeCarouselMedia(card.image), + video: await this.normalizeCarouselMedia(card.video), + }; + } + + private async normalizeCarouselMedia( + media?: CarouselCard["image"] | CarouselCard["video"], + ): Promise { + if (!media) { + return undefined; + } + + if (media.buffer || media.url) { + return media; + } + + if (media.path) { + const loaded = await this.media.load(media); + return { + ...media, + buffer: loaded.buffer, + mimetype: media.mimetype ?? loaded.metadata.mimetype, + fileName: media.fileName ?? loaded.metadata.fileName, + }; + } + + throw new Error("Carousel media must provide url, path or buffer."); + } + + private async connectForAuthFlow( + auth: BerryAuthOptions, + readyEvent: "auth.qr" | "auth.link" | "auth.pairing_code", + options?: ConnectFlowOptions, + ): Promise { + if (options?.waitForConnection) { + await this.connect(auth); + return; + } + + await this.sessions.get(this.options.sessionId); + + const waitForArtifactOrOpen = new Promise((resolve, reject) => { + const cleanup = () => { + this.bus.off(readyEvent, handleReady); + this.bus.off("connection.open", handleOpen); + this.bus.off("auth.error", handleAuthError); + this.bus.off("connection.close", handleClose); + }; + + const handleReady = () => { + cleanup(); + resolve(); + }; + + const handleOpen = () => { + cleanup(); + resolve(); + }; + + const handleAuthError = ({ error }: BerryEventMap["auth.error"]) => { + cleanup(); + reject(new Error(error)); + }; + + const handleClose = ({ reason }: BerryEventMap["connection.close"]) => { + if (reason === "restart_required") { + return; + } + + cleanup(); + reject(new Error(`WhatsApp connection closed during auth flow (${reason}).`)); + }; + + this.bus.on(readyEvent, handleReady); + this.bus.on("connection.open", handleOpen); + this.bus.on("auth.error", handleAuthError); + this.bus.on("connection.close", handleClose); + }); + + const connectPromise = this.socket.connect(auth); + void connectPromise.catch(() => undefined); + + await Promise.race([waitForArtifactOrOpen, connectPromise]); + } + + private bindInternals(): void { + this.bus.on("qr", async (qr) => { + this.lastQr = qr; + await this.sessions.update(this.options.sessionId, { qr }); + }); + + this.bus.on("auth.link", async ({ value }) => { + await this.sessions.update(this.options.sessionId, { + authMethod: "link", + linkCode: value, + }); + }); + + this.bus.on("auth.qr", async ({ value }) => { + await this.sessions.update(this.options.sessionId, { + authMethod: "qr", + qr: value, + }); + + if (this.options.printQrInTerminal !== false) { + qrcode.generate(value, { + small: this.options.qrSmall ?? true, + }); + } + }); + + this.bus.on("auth.pairing_code", async ({ code }) => { + await this.sessions.update(this.options.sessionId, { + authMethod: "pairing_code", + pairingCode: code, + }); + }); + + this.bus.on("connection.open", async () => { + const session = await this.sessions.update(this.options.sessionId, { registered: true }); + this.bus.emit("auth.success", session); + }); + + this.bus.on("protocol.error", async ({ error }) => { + this.bus.emit("auth.error", { + sessionId: this.options.sessionId, + error, + }); + }); + + this.bus.on("message.received", (message: IncomingMessage) => { + this.store.upsertMessages(this.options.sessionId, [message]); + }); + + this.bus.on("message.ack", (ack: MessageAck) => { + this.store.upsertAck(this.options.sessionId, ack); + }); + + this.bus.on("presence.update", (_presence: PresenceRecord) => undefined); + this.bus.on("chats.update", (chats) => { + this.store.upsertChats(this.options.sessionId, chats); + }); + this.bus.on("sync.contacts", (contacts) => { + this.store.upsertContacts(this.options.sessionId, contacts); + }); + this.bus.on("sync.groups", (groups) => { + this.store.upsertGroups(this.options.sessionId, groups); + }); + } +} + +export class BerryProtocol extends BerryClient {} + +export default BerryProtocol; + +export * from "@berrysdk/events"; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..c9caa01 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../auth" + }, + { + "path": "../events" + }, + { + "path": "../media" + }, + { + "path": "../messages" + }, + { + "path": "../socket" + }, + { + "path": "../store" + } + ] +} + diff --git a/packages/events/package.json b/packages/events/package.json new file mode 100644 index 0000000..a7cdd67 --- /dev/null +++ b/packages/events/package.json @@ -0,0 +1,22 @@ +{ + "name": "@berrysdk/events", + "version": "0.1.4", + "description": "Shared event and message contracts for Berry Protocol.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/events/src/index.ts b/packages/events/src/index.ts new file mode 100644 index 0000000..a53ff95 --- /dev/null +++ b/packages/events/src/index.ts @@ -0,0 +1,351 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import { EventEmitter } from "node:events"; + +export type AckStatus = "pending" | "sent" | "delivered" | "read" | "failed"; + +export interface ConnectionState { + sessionId: string; + connectedAt?: string; + disconnectedAt?: string; + reason?: string; +} + +export interface AuthStateSnapshot { + sessionId: string; + registered: boolean; + clientId?: string; + serverToken?: string; + clientToken?: string; + qr?: string; + linkCode?: string; + pairingCode?: string; + authMethod?: BerryAuthMethod; +} + +export type BerryAuthMethod = "link" | "qr" | "pairing_code"; + +export interface BerryAuthOptions { + method?: BerryAuthMethod; + phoneNumber?: string; + customPairingCode?: string; +} + +export interface ContactRecord { + id: string; + name?: string; + pushName?: string; + shortName?: string; +} + +export interface ChatRecord { + id: string; + name?: string; + unreadCount?: number; + lastMessageAt?: string; +} + +export interface GroupRecord { + id: string; + subject: string; + participants: string[]; +} + +export interface PresenceRecord { + id: string; + status: "available" | "composing" | "recording" | "paused" | "unavailable"; + lastSeenAt?: string; +} + +export interface LocationPayload { + latitude: number; + longitude: number; + name?: string; + address?: string; +} + +export interface ContactPayload { + displayName: string; + vcard: string; +} + +export interface ButtonRow { + id: string; + title: string; + kind?: "reply" | "quick_reply" | "copy_code" | "cta_url"; + code?: string; + url?: string; + nativeFlowName?: string; + buttonParamsJson?: string; +} + +export interface InteractiveHeader { + title?: string; + subtitle?: string; + hasMediaAttachment?: boolean; +} + +export interface InteractiveBody { + text: string; +} + +export interface InteractiveFooter { + text?: string; +} + +export interface InteractiveNativeButton { + name: string; + buttonParamsJson: string; +} + +export interface InteractiveNativeFlowPayload { + buttons: InteractiveNativeButton[]; + messageParamsJson?: string; + messageVersion?: number; +} + +export interface InteractivePayload { + header?: InteractiveHeader; + body: InteractiveBody; + footer?: InteractiveFooter; + nativeFlowMessage?: InteractiveNativeFlowPayload; +} + +export interface ButtonsPayload { + text: string; + footer?: string; + buttons: ButtonRow[]; +} + +export type CarouselCardType = "image" | "video" | "mixed"; + +export interface CarouselButton { + id?: string; + title?: string; + kind?: "reply" | "quick_reply" | "copy_code" | "cta_url"; + code?: string; + url?: string; + nativeFlowName?: string; + buttonParamsJson?: string; + name?: string; +} + +export interface CarouselCard { + title?: string; + body?: string; + footer?: string; + image?: MediaPayload; + video?: MediaPayload; + buttons?: CarouselButton[]; +} + +export interface CarouselMessagePayload { + text: string; + footer?: string; + cards: CarouselCard[]; + carouselCardType?: CarouselCardType; + ai?: boolean; +} + +export interface ListRow { + id: string; + title: string; + description?: string; +} + +export interface ListSection { + title: string; + rows: ListRow[]; +} + +export interface ListPayload { + title?: string; + text: string; + footer?: string; + buttonText: string; + sections: ListSection[]; +} + +export interface MediaPayload { + url?: string; + path?: string; + buffer?: Buffer; + fileName?: string; + mimetype?: string; + caption?: string; +} + +export interface BaseMessage { + id: string; + to: string; + chatId?: string; + remoteJid?: string; + from?: string; + timestamp: string; + ack: AckStatus; + buttonId?: string; + selectedButtonId?: string; + rawButtonParamsJson?: string; + type: + | "text" + | "image" + | "audio" + | "document" + | "buttons" + | "list" + | "carousel" + | "interactive" + | "reaction" + | "location" + | "contact"; +} + +export interface TextMessage extends BaseMessage { + type: "text"; + text: string; +} + +export interface ImageMessage extends BaseMessage { + type: "image"; + media: MediaPayload; +} + +export interface AudioMessage extends BaseMessage { + type: "audio"; + media: MediaPayload; +} + +export interface DocumentMessage extends BaseMessage { + type: "document"; + media: MediaPayload; +} + +export interface ButtonsMessage extends BaseMessage { + type: "buttons"; + buttons: ButtonsPayload; +} + +export interface ListMessage extends BaseMessage { + type: "list"; + list: ListPayload; +} + +export interface CarouselMessage extends BaseMessage { + type: "carousel"; + carousel: CarouselMessagePayload; +} + +export interface InteractiveMessage extends BaseMessage { + type: "interactive"; + interactive: InteractivePayload; +} + +export interface ReactionMessage extends BaseMessage { + type: "reaction"; + emoji: string; + targetMessageId: string; +} + +export interface LocationMessage extends BaseMessage { + type: "location"; + location: LocationPayload; +} + +export interface ContactMessage extends BaseMessage { + type: "contact"; + contact: ContactPayload; +} + +export type OutgoingMessage = + | TextMessage + | ImageMessage + | AudioMessage + | DocumentMessage + | ButtonsMessage + | ListMessage + | CarouselMessage + | InteractiveMessage + | ReactionMessage + | LocationMessage + | ContactMessage; + +export type IncomingMessage = OutgoingMessage & { + from: string; +}; + +export interface MessageAck { + messageId: string; + remoteJid: string; + ack: AckStatus; + updatedAt: string; +} + +export interface SyncBundle { + contacts: ContactRecord[]; + chats: ChatRecord[]; + groups: GroupRecord[]; + messages: IncomingMessage[]; +} + +export interface BerryEventMap { + qr: string; + "auth.link": { sessionId: string; value: string }; + "auth.qr": { sessionId: string; value: string }; + "auth.pairing_code": { sessionId: string; phoneNumber: string; code: string }; + "connection.open": ConnectionState; + "connection.close": ConnectionState; + "connection.reconnecting": { sessionId: string; attempt: number; delayMs: number }; + "auth.success": AuthStateSnapshot; + "auth.error": { sessionId: string; error: string }; + "message.received": IncomingMessage; + "message.sent": OutgoingMessage; + "message.ack": MessageAck; + "presence.update": PresenceRecord; + "chats.update": ChatRecord[]; + "sync.history": SyncBundle; + "sync.contacts": ContactRecord[]; + "sync.groups": GroupRecord[]; + "sync.messages": IncomingMessage[]; + "raw.frame": Buffer; + "protocol.error": { sessionId: string; error: string }; +} + +export class BerryEventBus { + private readonly emitter = new EventEmitter(); + + on( + event: EventName, + listener: (payload: BerryEventMap[EventName]) => void, + ): this { + this.emitter.on(event, listener); + return this; + } + + once( + event: EventName, + listener: (payload: BerryEventMap[EventName]) => void, + ): this { + this.emitter.once(event, listener); + return this; + } + + off( + event: EventName, + listener: (payload: BerryEventMap[EventName]) => void, + ): this { + this.emitter.off(event, listener); + return this; + } + + emit( + event: EventName, + payload: BerryEventMap[EventName], + ): boolean { + return this.emitter.emit(event, payload); + } +} diff --git a/packages/events/tsconfig.json b/packages/events/tsconfig.json new file mode 100644 index 0000000..f0720b6 --- /dev/null +++ b/packages/events/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ] +} + diff --git a/packages/gateway/package.json b/packages/gateway/package.json new file mode 100644 index 0000000..c304f85 --- /dev/null +++ b/packages/gateway/package.json @@ -0,0 +1,27 @@ +{ + "name": "@berrysdk/gateway", + "version": "0.1.9", + "description": "HTTP gateway for multi-instance Berry Protocol control.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@berrysdk/core": "^0.1.9", + "express": "^5.1.0", + "pino": "^9.6.0" + } +} diff --git a/packages/gateway/src/index.ts b/packages/gateway/src/index.ts new file mode 100644 index 0000000..d6be60d --- /dev/null +++ b/packages/gateway/src/index.ts @@ -0,0 +1,148 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import express from "express"; +import pino from "pino"; +import { BerryClient, type BerryAuthMethod, type ListPayload, type MediaPayload } from "@berrysdk/core"; + +const logger = pino({ name: "berry-gateway" }); +const app = express(); +app.use(express.json({ limit: "25mb" })); + +type InstanceAuthState = { + link?: string; + qr?: string; + pairingCode?: string; +}; + +const clients = new Map(); +const authStates = new Map(); + +const getClient = (id: string, method: BerryAuthMethod = "link", phoneNumber?: string): BerryClient => { + const existing = clients.get(id); + if (existing) { + return existing; + } + + const client = new BerryClient({ + sessionId: id, + databasePath: process.env.BERRY_SQLITE_PATH, + authFolder: process.env.BERRY_AUTH_FOLDER, + auth: { + method, + phoneNumber, + }, + }); + + client.on("auth.link", ({ value }) => { + authStates.set(id, { ...authStates.get(id), link: value }); + }); + + client.on("auth.qr", ({ value }) => { + authStates.set(id, { ...authStates.get(id), qr: value }); + }); + + client.on("auth.pairing_code", ({ code }) => { + authStates.set(id, { ...authStates.get(id), pairingCode: code }); + }); + + clients.set(id, client); + return client; +}; + +app.post("/instances", async (req, res) => { + const sessionId = req.body.sessionId as string | undefined; + const authMethod = (req.body.authMethod as BerryAuthMethod | undefined) ?? "link"; + const phoneNumber = req.body.phoneNumber as string | undefined; + + if (!sessionId) { + res.status(400).json({ error: "sessionId is required" }); + return; + } + + if (authMethod === "pairing_code" && !phoneNumber) { + res.status(400).json({ error: "phoneNumber is required for pairing_code auth" }); + return; + } + + const client = getClient(sessionId, authMethod, phoneNumber); + await client.connect({ + method: authMethod, + phoneNumber, + }); + res.status(201).json({ sessionId, status: "connecting", authMethod }); +}); + +app.get("/instances/:id/link", async (req, res) => { + const auth = authStates.get(req.params.id); + if (!auth?.link) { + res.status(404).json({ error: "Link auth value not available" }); + return; + } + + res.json({ sessionId: req.params.id, link: auth.link }); +}); + +app.get("/instances/:id/qr", async (req, res) => { + const auth = authStates.get(req.params.id); + if (!auth?.qr) { + res.status(404).json({ error: "QR code not available" }); + return; + } + + res.json({ sessionId: req.params.id, qr: auth.qr }); +}); + +app.get("/instances/:id/pairing-code", async (req, res) => { + const auth = authStates.get(req.params.id); + if (!auth?.pairingCode) { + res.status(404).json({ error: "Pairing code not available" }); + return; + } + + res.json({ sessionId: req.params.id, pairingCode: auth.pairingCode }); +}); + +app.post("/instances/:id/send-text", async (req, res) => { + const client = getClient(req.params.id); + const result = await client.sendText(req.body.to, req.body.text); + res.json(result); +}); + +app.post("/instances/:id/send-image", async (req, res) => { + const client = getClient(req.params.id); + const media = req.body.media as MediaPayload; + const result = await client.sendImage(req.body.to, media); + res.json(result); +}); + +app.post("/instances/:id/send-list", async (req, res) => { + const client = getClient(req.params.id); + const list = req.body.list as ListPayload; + const result = await client.sendList(req.body.to, list); + res.json(result); +}); + +app.delete("/instances/:id/logout", async (req, res) => { + const client = getClient(req.params.id); + await client.logout(); + clients.delete(req.params.id); + authStates.delete(req.params.id); + res.status(204).send(); +}); + +app.use((error: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => { + logger.error({ err: error }, "request failed"); + res.status(500).json({ error: error.message }); +}); + +const port = Number(process.env.BERRY_HTTP_PORT ?? 3000); +const host = process.env.BERRY_HTTP_HOST ?? "0.0.0.0"; + +app.listen(port, host, () => { + logger.info({ host, port }, "BerryProtocol gateway listening"); +}); diff --git a/packages/gateway/tsconfig.json b/packages/gateway/tsconfig.json new file mode 100644 index 0000000..75647e1 --- /dev/null +++ b/packages/gateway/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core" + } + ] +} + diff --git a/packages/media/package.json b/packages/media/package.json new file mode 100644 index 0000000..7dacf41 --- /dev/null +++ b/packages/media/package.json @@ -0,0 +1,25 @@ +{ + "name": "@berrysdk/media", + "version": "0.1.2", + "description": "Media loading and normalization helpers for Berry Protocol.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "engines": { + "node": "20.x || >=22 <27" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/media/src/index.ts b/packages/media/src/index.ts new file mode 100644 index 0000000..288fb8f --- /dev/null +++ b/packages/media/src/index.ts @@ -0,0 +1,68 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import { createReadStream, promises as fs } from "node:fs"; +import { Readable } from "node:stream"; +import { MediaPayload } from "@berrysdk/events"; + +export interface MediaDescriptor { + fileName?: string; + mimetype?: string; + byteLength: number; +} + +export class MediaManager { + async load(payload: MediaPayload): Promise<{ buffer: Buffer; metadata: MediaDescriptor }> { + if (payload.buffer) { + return { + buffer: payload.buffer, + metadata: { + fileName: payload.fileName, + mimetype: payload.mimetype, + byteLength: payload.buffer.byteLength, + }, + }; + } + + if (payload.path) { + const buffer = await fs.readFile(payload.path); + return { + buffer, + metadata: { + fileName: payload.fileName ?? payload.path.split(/[\\/]/).at(-1), + mimetype: payload.mimetype, + byteLength: buffer.byteLength, + }, + }; + } + + if (payload.url) { + const response = await fetch(payload.url); + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + return { + buffer, + metadata: { + fileName: payload.fileName, + mimetype: payload.mimetype ?? response.headers.get("content-type") ?? undefined, + byteLength: buffer.byteLength, + }, + }; + } + + throw new Error("Media payload requires buffer, path or url."); + } + + async download(url: string): Promise { + const response = await fetch(url); + return Buffer.from(await response.arrayBuffer()); + } + + stream(path: string): Readable { + return createReadStream(path); + } +} diff --git a/packages/media/tsconfig.json b/packages/media/tsconfig.json new file mode 100644 index 0000000..1fa4f81 --- /dev/null +++ b/packages/media/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../events" + } + ] +} + diff --git a/packages/messages/package.json b/packages/messages/package.json new file mode 100644 index 0000000..b39b904 --- /dev/null +++ b/packages/messages/package.json @@ -0,0 +1,25 @@ +{ + "name": "@berrysdk/messages", + "version": "0.1.3", + "description": "Outgoing message factories for Berry Protocol.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@berrysdk/events": "^0.1.4" + } +} diff --git a/packages/messages/src/index.ts b/packages/messages/src/index.ts new file mode 100644 index 0000000..e5659ea --- /dev/null +++ b/packages/messages/src/index.ts @@ -0,0 +1,95 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import { + AudioMessage, + ButtonsPayload, + CarouselMessage, + CarouselMessagePayload, + ContactPayload, + ContactMessage, + DocumentMessage, + ImageMessage, + ListPayload, + ListMessage, + LocationPayload, + LocationMessage, + MediaPayload, + OutgoingMessage, + ReactionMessage, + TextMessage, +} from "@berrysdk/events"; +import { randomUUID } from "node:crypto"; + +const buildBase = (type: T, to: string) => ({ + id: randomUUID(), + to, + timestamp: new Date().toISOString(), + ack: "pending" as const, + type, +}); + +export const createTextMessage = (to: string, text: string): TextMessage => ({ + ...buildBase("text", to), + text, +}); + +export const createImageMessage = (to: string, media: MediaPayload): ImageMessage => ({ + ...buildBase("image", to), + media, +}); + +export const createAudioMessage = (to: string, media: MediaPayload): AudioMessage => ({ + ...buildBase("audio", to), + media, +}); + +export const createDocumentMessage = (to: string, media: MediaPayload): DocumentMessage => ({ + ...buildBase("document", to), + media, +}); + +export const createButtonsMessage = (to: string, buttons: ButtonsPayload): OutgoingMessage => ({ + ...buildBase("buttons", to), + buttons, +}); + +export const createListMessage = (to: string, list: ListPayload): ListMessage => ({ + ...buildBase("list", to), + list, +}); + +export const createCarouselMessage = ( + to: string, + carousel: CarouselMessagePayload, +): CarouselMessage => ({ + ...buildBase("carousel", to), + carousel, +}); + +export const createReactionMessage = ( + to: string, + emoji: string, + targetMessageId: string, +): ReactionMessage => ({ + ...buildBase("reaction", to), + emoji, + targetMessageId, +}); + +export const createLocationMessage = ( + to: string, + location: LocationPayload, +): LocationMessage => ({ + ...buildBase("location", to), + location, +}); + +export const createContactMessage = (to: string, contact: ContactPayload): ContactMessage => ({ + ...buildBase("contact", to), + contact, +}); diff --git a/packages/messages/tsconfig.json b/packages/messages/tsconfig.json new file mode 100644 index 0000000..1fa4f81 --- /dev/null +++ b/packages/messages/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../events" + } + ] +} + diff --git a/packages/protocol/package.json b/packages/protocol/package.json new file mode 100644 index 0000000..ed0e7c8 --- /dev/null +++ b/packages/protocol/package.json @@ -0,0 +1,29 @@ +{ + "name": "@berrysdk/proto", + "version": "0.1.2", + "description": "Berry Protocol proto wrapper around WhatsApp Web protobuf definitions.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "engines": { + "node": "20.x || >=22 <27" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@wppconnect/wa-proto": "^2.3000.1030818676", + "protobufjs": "^7.4.0" + } +} diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts new file mode 100644 index 0000000..1f1901b --- /dev/null +++ b/packages/protocol/src/index.ts @@ -0,0 +1,86 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import protobuf from "protobufjs"; +import * as whatsappProto from "@wppconnect/wa-proto"; + +export interface ProtocolFrame { + stanza: string; + attrs: Record; + payload: Buffer; +} + +export interface WhatsAppWebConfig { + webSocketUrl: string; + origin: string; + userAgent: string; +} + +export class ProtocolError extends Error { + constructor(message: string) { + super(message); + this.name = "ProtocolError"; + } +} + +const frameSchema = ` +syntax = "proto3"; +message WaEnvelope { + string stanza = 1; + map attrs = 2; + bytes payload = 3; +} +`; + +const root = protobuf.parse(frameSchema).root; +const envelopeType = root.lookupType("WaEnvelope"); + +export class BinaryFrameCodec { + encode(frame: ProtocolFrame): Buffer { + const payload = envelopeType.create({ + stanza: frame.stanza, + attrs: frame.attrs, + payload: frame.payload, + }); + + return Buffer.from(envelopeType.encode(payload).finish()); + } + + decode(buffer: Buffer): ProtocolFrame { + try { + const decoded = envelopeType.decode(buffer); + const object = envelopeType.toObject(decoded, { + bytes: Buffer, + }) as { + stanza: string; + attrs?: Record; + payload?: Buffer; + }; + + return { + stanza: object.stanza, + attrs: object.attrs ?? {}, + payload: object.payload ?? Buffer.alloc(0), + }; + } catch (error) { + throw new ProtocolError( + `Unable to decode incoming frame as Berry envelope: ${(error as Error).message}`, + ); + } + } +} + +export const defaultWhatsAppWebConfig: WhatsAppWebConfig = { + webSocketUrl: process.env.BERRY_WHATSAPP_WS_URL ?? "wss://web.whatsapp.com/ws/chat", + origin: "https://web.whatsapp.com", + userAgent: + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", +}; + +export const proto = whatsappProto.waproto; +export const WAProto = proto; +export type WAProtoNamespace = typeof proto; diff --git a/packages/protocol/tsconfig.json b/packages/protocol/tsconfig.json new file mode 100644 index 0000000..f0720b6 --- /dev/null +++ b/packages/protocol/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ] +} + diff --git a/packages/socket/package.json b/packages/socket/package.json new file mode 100644 index 0000000..bd0ce39 --- /dev/null +++ b/packages/socket/package.json @@ -0,0 +1,33 @@ +{ + "name": "@berrysdk/socket", + "version": "0.1.10", + "description": "Internal transport adapter for Berry Protocol.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "engines": { + "node": "20.x || >=22 <27" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@berrysdk/events": "^0.1.4", + "@berrysdk/proto": "^0.1.2", + "@berrysdk/transport": "^0.1.9", + "@berrysdk/wa-message": "^0.1.10", + "pino": "^9.6.0", + "ws": "^8.18.1" + } +} diff --git a/packages/socket/src/index.ts b/packages/socket/src/index.ts new file mode 100644 index 0000000..dfe1b5c --- /dev/null +++ b/packages/socket/src/index.ts @@ -0,0 +1,900 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import makeWASocket, { + type AnyMessageContent, + type BinaryNode, + DisconnectReason, + downloadMediaMessage, + fetchLatestBerryWebVersion, + generateWAMessageFromContent, + makeCacheableSignalKeyStore, + prepareWAMessageMedia, + proto, + useMultiFileAuthState, + type WAMessage, + type WASocket, +} from "@berrysdk/transport"; +import pino, { type Logger } from "pino"; +import { + type BerryAuthOptions, + type BerryEventBus, + type ButtonsPayload, + type CarouselCard, + type CarouselMessagePayload, + type ChatRecord, + type ContactRecord, + type GroupRecord, + type IncomingMessage, + type InteractivePayload, + type ListPayload, + type MessageAck, + type PresenceRecord, + type SyncBundle, +} from "@berrysdk/events"; +import { + ackFromWebMessageStatus, + buttonsPayloadToLegacyButtonsMessageContent, + buttonsPayloadToNativeFlowInteractiveContent, + carouselButtonToNativeFlowButton, + carouselPayloadToMessageContent, + interactivePayloadToMessageContent, + interactiveNativeFlowAdditionalNodes, + listToInteractiveMessageContent, + normalizeIncomingMessage, +} from "@berrysdk/wa-message"; + +export interface SocketOptions { + sessionId: string; + logger?: Logger; + reconnectMaxAttempts?: number; + reconnectDelayMs?: number; + authFolder?: string; + auth?: BerryAuthOptions; +} + +type MessageContent = Record; +const shouldDebugOutgoingMessages = process.env.BERRY_DEBUG_WA_MESSAGE === "1"; +const MAX_CAROUSEL_CARDS = 10; +const BIZ_BOT_SUPPORT_PAYLOAD = "{}"; + +const debugJsonReplacer = (_key: string, value: unknown) => { + if (Buffer.isBuffer(value)) { + return ``; + } + + if (value instanceof Uint8Array) { + return ``; + } + + return value; +}; + +const normalizePhoneNumber = (value: string): string => value.replace(/\D/g, ""); + +const assertTransportJid = (jid: string): string => { + const normalized = jid.trim(); + if (!normalized || !normalized.includes("@")) { + throw new Error( + `Invalid WhatsApp JID "${jid}". BerryProtocol expected something like "5511999999999@s.whatsapp.net".`, + ); + } + + return normalized; +}; + +const isPrivateChatJid = (jid: string): boolean => + jid.endsWith("@s.whatsapp.net") || jid.endsWith("@lid"); + +const aiLabelAdditionalNode = (): BinaryNode => ({ + tag: "bot", + attrs: { + biz_bot: "1", + }, + content: undefined, +}); + +const generateNumericPairingCode = (): string => + Math.floor(10_000_000 + Math.random() * 90_000_000).toString(); + +const getStatusCode = (error: unknown): number | undefined => { + if (!error || typeof error !== "object") { + return undefined; + } + + const candidate = error as { + output?: { statusCode?: number }; + data?: { attrs?: { code?: string | number } }; + message?: string; + }; + + const directCode = candidate.output?.statusCode; + if (typeof directCode === "number") { + return directCode; + } + + const streamCode = candidate.data?.attrs?.code; + if (typeof streamCode === "number") { + return streamCode; + } + + if (typeof streamCode === "string" && /^\d+$/.test(streamCode)) { + return Number(streamCode); + } + + if (typeof candidate.message === "string") { + if (candidate.message.toLowerCase().includes("restart required")) { + return DisconnectReason.restartRequired; + } + + const match = candidate.message.match(/\b(401|408|411|428|440|500|503|515)\b/); + if (match) { + return Number(match[1]); + } + } + + return undefined; +}; + +const toChatRecord = (chat: Record): ChatRecord => ({ + id: String(chat.id ?? ""), + name: typeof chat.name === "string" ? chat.name : undefined, + unreadCount: typeof chat.unreadCount === "number" ? chat.unreadCount : undefined, + lastMessageAt: + typeof chat.conversationTimestamp === "number" + ? new Date(chat.conversationTimestamp * 1000).toISOString() + : undefined, +}); + +const toContactRecord = (contact: Record): ContactRecord => ({ + id: String(contact.id ?? ""), + name: typeof contact.name === "string" ? contact.name : undefined, + pushName: typeof contact.notify === "string" ? contact.notify : undefined, + shortName: typeof contact.verifiedName === "string" ? contact.verifiedName : undefined, +}); + +const toGroupRecord = (group: Record): GroupRecord => ({ + id: String(group.id ?? ""), + subject: String(group.subject ?? ""), + participants: Array.isArray(group.participants) + ? group.participants + .map((participant) => + typeof participant === "object" && participant && "id" in participant + ? String((participant as { id?: string }).id ?? "") + : "", + ) + .filter(Boolean) + : [], +}); + +export class BerrySocket { + private readonly logger: Logger; + private reconnectAttempts = 0; + private reconnectTimer?: NodeJS.Timeout; + private manualClose = false; + private connected = false; + private sock?: WASocket; + private connectPromise?: Promise; + private authFolder?: string; + private auth: BerryAuthOptions; + private pairingCodeRequested = false; + private readonly retryCounterMap = new Map(); + private readonly msgRetryCounterCache = { + get: (key: string) => this.retryCounterMap.get(key) as T | undefined, + set: (key: string, value: T) => { + this.retryCounterMap.set(key, value); + return true; + }, + del: (key: string) => this.retryCounterMap.delete(key), + flushAll: () => this.retryCounterMap.clear(), + }; + private readonly sentMessages = new Map(); + + constructor( + private readonly options: SocketOptions, + private readonly bus: BerryEventBus, + ) { + this.logger = options.logger ?? pino({ name: "berry-socket" }); + this.auth = options.auth ?? { method: "link" }; + } + + setAuth(auth: BerryAuthOptions): void { + const method = auth.method ?? "link"; + const customPairingCode = + method === "pairing_code" + ? this.resolvePairingCode(auth.customPairingCode) + : auth.customPairingCode; + + this.auth = { + method, + phoneNumber: auth.phoneNumber ? normalizePhoneNumber(auth.phoneNumber) : undefined, + customPairingCode, + }; + this.pairingCodeRequested = false; + } + + async connect(auth?: BerryAuthOptions): Promise { + if (auth) { + this.setAuth(auth); + } + + if (this.connectPromise) { + return this.connectPromise; + } + + this.manualClose = false; + this.pairingCodeRequested = false; + this.connectPromise = this.connectUntilReady(); + return this.connectPromise.finally(() => { + this.connectPromise = undefined; + }); + } + + async disconnect(reason = "manual"): Promise { + this.manualClose = true; + clearTimeout(this.reconnectTimer); + this.connected = false; + this.sock?.end(new Error(reason)); + this.sock?.ws.close(); + this.sock = undefined; + } + + async reconnect(): Promise { + await this.disconnect("reconnect"); + await this.connect(); + } + + async logout(): Promise { + this.manualClose = true; + clearTimeout(this.reconnectTimer); + this.connected = false; + await this.sock?.logout(); + this.sock = undefined; + } + + isConnected(): boolean { + return this.connected && Boolean(this.sock?.user?.id); + } + + async sendMessage( + to: string, + content: MessageContent, + options?: Record, + ): Promise { + if (!this.sock) { + throw new Error("Socket is not connected."); + } + + const result = await this.sock.sendMessage(to, content as never, options as never); + if (!result) { + throw new Error("WhatsApp did not return a message receipt for the send operation."); + } + + if (result.key.id) { + this.sentMessages.set(result.key.id, result); + } + + return result; + } + + async sendTransportMessage( + to: string, + content: AnyMessageContent, + options?: Record, + ): Promise { + return this.sendMessage(assertTransportJid(to), content as MessageContent, options); + } + + async editMessage(to: string, messageId: string, text: string): Promise { + const recipientJid = assertTransportJid(to); + return this.sendTransportMessage(recipientJid, { + text, + edit: { + remoteJid: recipientJid, + fromMe: true, + id: messageId, + }, + }); + } + + async sendLegacyButtonsMessage(to: string, buttons: ButtonsPayload): Promise { + if (!this.sock?.user?.id) { + throw new Error("Socket is not connected."); + } + + const recipientJid = assertTransportJid(to); + const content = buttonsPayloadToLegacyButtonsMessageContent(buttons); + const fullMessage = generateWAMessageFromContent(recipientJid, content, { + userJid: this.sock.user.id, + }); + this.logOutgoingMessage("buttons-legacy", fullMessage); + + await this.sock.relayMessage(recipientJid, fullMessage.message!, { + messageId: fullMessage.key.id!, + }); + + return fullMessage as WAMessage; + } + + async sendInteractiveMessage(to: string, interactive: InteractivePayload): Promise { + if (!this.sock?.user?.id) { + throw new Error("Socket is not connected."); + } + + const recipientJid = assertTransportJid(to); + const content = interactivePayloadToMessageContent(interactive); + const fullMessage = generateWAMessageFromContent(recipientJid, content, { + userJid: this.sock.user.id, + }); + this.logOutgoingMessage("interactive", fullMessage); + + await this.sock.relayMessage(recipientJid, fullMessage.message!, { + messageId: fullMessage.key.id!, + additionalNodes: interactiveNativeFlowAdditionalNodes(), + }); + + return fullMessage as WAMessage; + } + + async sendReplyButtonsMessage(to: string, buttons: ButtonsPayload): Promise { + if (!this.sock?.user?.id) { + throw new Error("Socket is not connected."); + } + + const recipientJid = assertTransportJid(to); + const content = buttonsPayloadToNativeFlowInteractiveContent(buttons); + const fullMessage = generateWAMessageFromContent(recipientJid, content, { + userJid: this.sock.user.id, + }); + this.logOutgoingMessage("buttons", fullMessage); + + await this.sock.relayMessage(recipientJid, fullMessage.message!, { + messageId: fullMessage.key.id!, + additionalNodes: interactiveNativeFlowAdditionalNodes(), + }); + + return fullMessage as WAMessage; + } + + async sendListMessage(to: string, list: ListPayload): Promise { + if (!this.sock?.user?.id) { + throw new Error("Socket is not connected."); + } + + const recipientJid = assertTransportJid(to); + const content = listToInteractiveMessageContent(list); + const fullMessage = generateWAMessageFromContent(recipientJid, content, { + userJid: this.sock.user.id, + }); + this.logOutgoingMessage("list", fullMessage); + + await this.sock.relayMessage(recipientJid, fullMessage.message!, { + messageId: fullMessage.key.id!, + }); + + return fullMessage as WAMessage; + } + + async sendCarouselMessage( + to: string, + payload: CarouselMessagePayload, + options?: { ai?: boolean }, + ): Promise { + if (!this.sock?.user?.id) { + throw new Error("Socket is not connected."); + } + + const recipientJid = assertTransportJid(to); + const cards = payload.cards ?? []; + + if (!cards.length) { + throw new Error("Carousel payload requires at least one card."); + } + + if (cards.length > MAX_CAROUSEL_CARDS) { + throw new Error(`Carousel payload supports at most ${MAX_CAROUSEL_CARDS} cards.`); + } + + this.assertCarouselCardType(payload); + + const preparedCards = await Promise.all(cards.map((card, index) => this.prepareCarouselCard(card, index))); + const content = carouselPayloadToMessageContent({ + text: payload.text, + footer: payload.footer, + cards: preparedCards, + }); + const fullMessage = generateWAMessageFromContent(recipientJid, content, { + userJid: this.sock.user.id, + }); + + if (options?.ai) { + this.applyAiLabelToCarouselMessage(recipientJid, fullMessage); + } + + this.logOutgoingMessage("carousel", fullMessage); + + const additionalNodes: BinaryNode[] = []; + if (preparedCards.some((card) => !!card.nativeFlowMessage?.buttons?.length)) { + additionalNodes.push(...interactiveNativeFlowAdditionalNodes()); + } + + if (options?.ai) { + additionalNodes.push(aiLabelAdditionalNode()); + } + + await this.sock.relayMessage(recipientJid, fullMessage.message!, { + messageId: fullMessage.key.id!, + ...(additionalNodes.length ? { additionalNodes } : {}), + }); + + return fullMessage as WAMessage; + } + + async subscribePresence(jid: string): Promise { + if (!this.sock) { + throw new Error("Socket is not connected."); + } + + await this.sock.presenceSubscribe(jid); + } + + async sendPresenceUpdate( + status: "available" | "composing" | "recording" | "paused" | "unavailable", + jid?: string, + ): Promise { + if (!this.sock) { + throw new Error("Socket is not connected."); + } + + await this.sock.sendPresenceUpdate(status, jid); + } + + async fetchGroups(): Promise { + if (!this.sock) { + throw new Error("Socket is not connected."); + } + + const groups = await this.sock.groupFetchAllParticipating(); + return Object.values(groups).map((group) => + toGroupRecord(group as unknown as Record), + ); + } + + async downloadMedia(message: WAMessage): Promise { + const data = await downloadMediaMessage(message, "buffer", {}); + return Buffer.isBuffer(data) ? data : Buffer.from(data); + } + + private async connectUntilReady(): Promise { + while (true) { + const outcome = await this.createSocketAndWaitUntilOpen(); + if (outcome === "open") { + return; + } + + if (outcome === "restart_required") { + this.bus.emit("connection.reconnecting", { + sessionId: this.options.sessionId, + attempt: this.reconnectAttempts + 1, + delayMs: 0, + }); + this.sock = undefined; + continue; + } + + return; + } + } + + private assertCarouselCardType(payload: CarouselMessagePayload): void { + if (!payload.carouselCardType || payload.carouselCardType === "mixed") { + return; + } + + for (const [index, card] of payload.cards.entries()) { + if (payload.carouselCardType === "image" && card.video) { + throw new Error(`Carousel card ${index + 1} contains video but carouselCardType is "image".`); + } + + if (payload.carouselCardType === "video" && card.image) { + throw new Error(`Carousel card ${index + 1} contains image but carouselCardType is "video".`); + } + } + } + + private applyAiLabelToCarouselMessage(jid: string, message: WAMessage): void { + if (!isPrivateChatJid(jid)) { + throw new Error("AI labeled carousel messages are only allowed in private chat."); + } + + const viewOnceMessage = message.message?.viewOnceMessage; + const innerMessage = viewOnceMessage?.message; + if (!innerMessage) { + throw new Error("BerryProtocol expected a viewOnceMessage wrapper for carousel AI label injection."); + } + + innerMessage.messageContextInfo ||= {}; + innerMessage.messageContextInfo.supportPayload = BIZ_BOT_SUPPORT_PAYLOAD; + } + + private async prepareCarouselCard( + card: CarouselCard, + index: number, + ): Promise { + const hasImage = !!card.image; + const hasVideo = !!card.video; + + if (!hasImage && !hasVideo) { + throw new Error(`Carousel card ${index + 1} must contain image or video.`); + } + + if (hasImage && hasVideo) { + throw new Error(`Carousel card ${index + 1} cannot contain both image and video.`); + } + + const header: { + title?: string; + hasMediaAttachment: boolean; + imageMessage?: proto.Message.IImageMessage; + videoMessage?: proto.Message.IVideoMessage; + } = { + title: card.title ?? "", + hasMediaAttachment: true, + }; + + if (card.image) { + const media = await prepareWAMessageMedia( + this.toCarouselImageMediaMessage(card.image), + { + upload: this.sock!.waUploadToServer, + logger: this.logger, + }, + ); + header.imageMessage = media.imageMessage ?? undefined; + } else if (card.video) { + const media = await prepareWAMessageMedia( + this.toCarouselVideoMediaMessage(card.video), + { + upload: this.sock!.waUploadToServer, + logger: this.logger, + }, + ); + header.videoMessage = media.videoMessage ?? undefined; + } + + return { + header, + body: card.body + ? { + text: card.body, + } + : undefined, + footer: card.footer + ? { + text: card.footer, + } + : undefined, + nativeFlowMessage: + card.buttons && card.buttons.length > 0 + ? { + buttons: card.buttons.map((button, buttonIndex) => + carouselButtonToNativeFlowButton(button, buttonIndex), + ), + messageParamsJson: "", + messageVersion: 1, + } + : undefined, + }; + } + + private toCarouselImageMediaMessage(media: NonNullable) { + const source = media.buffer ?? (media.url ? { url: media.url } : undefined); + if (!source) { + throw new Error("Carousel image media must provide buffer or url before reaching the socket layer."); + } + + return { + image: source, + ...(media.mimetype ? { mimetype: media.mimetype } : {}), + ...(media.fileName ? { fileName: media.fileName } : {}), + }; + } + + private toCarouselVideoMediaMessage(media: NonNullable) { + const source = media.buffer ?? (media.url ? { url: media.url } : undefined); + if (!source) { + throw new Error("Carousel video media must provide buffer or url before reaching the socket layer."); + } + + return { + video: source, + ...(media.mimetype ? { mimetype: media.mimetype } : {}), + ...(media.fileName ? { fileName: media.fileName } : {}), + }; + } + + private async createSocketAndWaitUntilOpen(): Promise<"open" | "restart_required"> { + const sessionDir = await this.ensureAuthDir(); + const { state, saveCreds } = await useMultiFileAuthState(sessionDir); + const { version } = await fetchLatestBerryWebVersion(); + + return new Promise<"open" | "restart_required">((resolve, reject) => { + const sock = makeWASocket({ + version, + logger: this.logger, + auth: { + creds: state.creds, + keys: makeCacheableSignalKeyStore(state.keys, this.logger), + }, + printQRInTerminal: false, + syncFullHistory: true, + markOnlineOnConnect: false, + emitOwnEvents: true, + msgRetryCounterCache: this.msgRetryCounterCache, + getMessage: async (key) => { + if (!key.id) { + return undefined; + } + + return this.sentMessages.get(key.id)?.message ?? undefined; + }, + }); + + this.sock = sock; + + sock.ev.on("creds.update", () => { + void saveCreds(); + }); + + sock.ev.on("connection.update", (update) => { + if (update.qr) { + this.bus.emit("qr", update.qr); + this.bus.emit("auth.link", { + sessionId: this.options.sessionId, + value: update.qr, + }); + this.bus.emit("auth.qr", { + sessionId: this.options.sessionId, + value: update.qr, + }); + void this.maybeRequestPairingCode(sock); + } + + if (update.connection === "open") { + this.connected = true; + this.reconnectAttempts = 0; + this.bus.emit("connection.open", { + sessionId: this.options.sessionId, + connectedAt: new Date().toISOString(), + }); + resolve("open"); + return; + } + + if (update.connection === "close") { + this.connected = false; + const statusCode = getStatusCode(update.lastDisconnect?.error); + const reason = + statusCode === DisconnectReason.loggedOut + ? "logged_out" + : statusCode === DisconnectReason.restartRequired + ? "restart_required" + : "connection_closed"; + + this.bus.emit("connection.close", { + sessionId: this.options.sessionId, + disconnectedAt: new Date().toISOString(), + reason, + }); + + if (statusCode === DisconnectReason.loggedOut) { + reject(new Error("WhatsApp session logged out.")); + return; + } + + if (statusCode === DisconnectReason.restartRequired) { + resolve("restart_required"); + return; + } + + if (!this.manualClose) { + this.scheduleReconnect(); + } + } + }); + + sock.ev.on("messages.upsert", ({ messages, type }) => { + for (const message of messages) { + const normalized = normalizeIncomingMessage(message); + if (!normalized) { + continue; + } + + if (!message.key.fromMe && type === "notify") { + this.bus.emit("message.received", normalized); + } + } + }); + + sock.ev.on("messages.update", (updates) => { + for (const item of updates) { + const ack: MessageAck = { + messageId: item.key.id ?? "", + remoteJid: item.key.remoteJid ?? "", + ack: ackFromWebMessageStatus(item.update.status), + updatedAt: new Date().toISOString(), + }; + this.bus.emit("message.ack", ack); + } + }); + + sock.ev.on("message-receipt.update", (receipts) => { + for (const receipt of receipts) { + this.bus.emit("message.ack", { + messageId: receipt.key.id ?? "", + remoteJid: receipt.key.remoteJid ?? "", + ack: receipt.receipt.readTimestamp ? "read" : "delivered", + updatedAt: new Date().toISOString(), + }); + } + }); + + sock.ev.on("presence.update", ({ id, presences }) => { + for (const presence of Object.values(presences)) { + const payload: PresenceRecord = { + id, + status: (presence.lastKnownPresence as PresenceRecord["status"]) ?? "available", + lastSeenAt: + typeof presence.lastSeen === "number" + ? new Date(presence.lastSeen * 1000).toISOString() + : undefined, + }; + this.bus.emit("presence.update", payload); + } + }); + + sock.ev.on("chats.update", (chats) => { + this.bus.emit( + "chats.update", + chats.map((chat) => toChatRecord(chat as unknown as Record)), + ); + }); + + sock.ev.on("contacts.upsert", (contacts) => { + this.bus.emit( + "sync.contacts", + contacts.map((contact) => toContactRecord(contact as unknown as Record)), + ); + }); + + sock.ev.on("groups.upsert", (groups) => { + this.bus.emit( + "sync.groups", + groups.map((group) => toGroupRecord(group as unknown as Record)), + ); + }); + + sock.ev.on("messaging-history.set", ({ chats, contacts, messages }) => { + const incomingMessages = messages + .map((message) => normalizeIncomingMessage(message)) + .filter((message): message is IncomingMessage => !!message); + + const payload: SyncBundle = { + contacts: contacts.map((contact) => + toContactRecord(contact as unknown as Record), + ), + chats: chats.map((chat) => toChatRecord(chat as unknown as Record)), + groups: [], + messages: incomingMessages, + }; + + this.bus.emit("sync.history", payload); + this.bus.emit("sync.messages", incomingMessages); + }); + }); + } + + private scheduleReconnect(): void { + const maxAttempts = this.options.reconnectMaxAttempts ?? 10; + if (this.reconnectAttempts >= maxAttempts) { + return; + } + + this.reconnectAttempts += 1; + const delayMs = (this.options.reconnectDelayMs ?? 3_000) * this.reconnectAttempts; + this.bus.emit("connection.reconnecting", { + sessionId: this.options.sessionId, + attempt: this.reconnectAttempts, + delayMs, + }); + + this.reconnectTimer = setTimeout(() => { + void this.connect().catch((error) => { + this.bus.emit("protocol.error", { + sessionId: this.options.sessionId, + error: (error as Error).message, + }); + }); + }, delayMs); + } + + private async ensureAuthDir(): Promise { + const baseFolder = this.options.authFolder ?? join(process.cwd(), ".berry-sessions"); + const sessionDir = join(baseFolder, this.options.sessionId); + await mkdir(sessionDir, { recursive: true }); + this.authFolder = sessionDir; + return sessionDir; + } + + private async maybeRequestPairingCode(sock: WASocket): Promise { + if (this.auth.method !== "pairing_code") { + return; + } + + if (this.pairingCodeRequested || sock.authState.creds.registered) { + return; + } + + if (!this.auth.phoneNumber) { + this.bus.emit("protocol.error", { + sessionId: this.options.sessionId, + error: "phoneNumber is required for pairing_code authentication.", + }); + return; + } + + this.pairingCodeRequested = true; + + try { + const code = await sock.requestPairingCode( + this.auth.phoneNumber, + this.auth.customPairingCode, + ); + this.bus.emit("auth.pairing_code", { + sessionId: this.options.sessionId, + phoneNumber: this.auth.phoneNumber, + code, + }); + } catch (error) { + this.pairingCodeRequested = false; + this.bus.emit("protocol.error", { + sessionId: this.options.sessionId, + error: `Unable to request pairing code: ${(error as Error).message}`, + }); + } + } + + private resolvePairingCode(customPairingCode?: string): string { + if (!customPairingCode) { + return generateNumericPairingCode(); + } + + const normalized = customPairingCode.replace(/\D/g, ""); + if (!/^\d{8}$/.test(normalized)) { + throw new Error("Pairing code must be exactly 8 numeric digits."); + } + + return normalized; + } + + private logOutgoingMessage(kind: string, message: WAMessage): void { + if (!shouldDebugOutgoingMessages) { + return; + } + + this.logger.info( + { + kind, + key: message.key, + message: JSON.parse(JSON.stringify(message.message, debugJsonReplacer)), + }, + "berry outgoing message", + ); + } +} diff --git a/packages/socket/tsconfig.json b/packages/socket/tsconfig.json new file mode 100644 index 0000000..e122c94 --- /dev/null +++ b/packages/socket/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../events" + }, + { + "path": "../protocol" + }, + { + "path": "../transport" + }, + { + "path": "../wa-message" + } + ] +} + diff --git a/packages/store/package.json b/packages/store/package.json new file mode 100644 index 0000000..7bf6447 --- /dev/null +++ b/packages/store/package.json @@ -0,0 +1,29 @@ +{ + "name": "@berrysdk/store", + "version": "0.1.4", + "description": "SQLite-backed persistence for Berry Protocol sessions and metadata.", + "author": "Lipe Devv and Berry Protocol", + "license": "Apache-2.0", + "type": "module", + "engines": { + "node": "20.x || >=22 <27" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@berrysdk/events": "^0.1.4", + "better-sqlite3": "^12.11.1" + } +} diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts new file mode 100644 index 0000000..87e9a65 --- /dev/null +++ b/packages/store/src/index.ts @@ -0,0 +1,188 @@ +/** + * @module modulo + * @description Enhanced message sending capabilities including albums, cards, + * interactive buttons, and rich media support beyond standard WhatsApp messages. + * @license Apache-2.0 + * @author Lipe Devv and Berry Protocol + */ +import Database from "better-sqlite3"; +import { + AuthStateSnapshot, + ChatRecord, + ContactRecord, + GroupRecord, + IncomingMessage, + MessageAck, +} from "@berrysdk/events"; + +export class SQLiteStore { + readonly db: Database.Database; + + constructor(path = process.env.BERRY_SQLITE_PATH ?? "berrysdk.db") { + this.db = new Database(path); + this.migrate(); + } + + private migrate(): void { + this.db.exec(` + CREATE TABLE IF NOT EXISTS auth_sessions ( + session_id TEXT PRIMARY KEY, + payload TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS chats ( + id TEXT NOT NULL, + session_id TEXT NOT NULL, + payload TEXT NOT NULL, + PRIMARY KEY (id, session_id) + ); + + CREATE TABLE IF NOT EXISTS contacts ( + id TEXT NOT NULL, + session_id TEXT NOT NULL, + payload TEXT NOT NULL, + PRIMARY KEY (id, session_id) + ); + + CREATE TABLE IF NOT EXISTS groups ( + id TEXT NOT NULL, + session_id TEXT NOT NULL, + payload TEXT NOT NULL, + PRIMARY KEY (id, session_id) + ); + + CREATE TABLE IF NOT EXISTS messages ( + id TEXT NOT NULL, + session_id TEXT NOT NULL, + remote_jid TEXT NOT NULL, + payload TEXT NOT NULL, + PRIMARY KEY (id, session_id) + ); + + CREATE TABLE IF NOT EXISTS message_acks ( + message_id TEXT NOT NULL, + session_id TEXT NOT NULL, + payload TEXT NOT NULL, + PRIMARY KEY (message_id, session_id) + ); + `); + } + + saveSession(sessionId: string, snapshot: AuthStateSnapshot): void { + this.db + .prepare( + ` + INSERT INTO auth_sessions(session_id, payload, updated_at) + VALUES (@session_id, @payload, @updated_at) + ON CONFLICT(session_id) DO UPDATE SET + payload = excluded.payload, + updated_at = excluded.updated_at + `, + ) + .run({ + session_id: sessionId, + payload: JSON.stringify(snapshot), + updated_at: new Date().toISOString(), + }); + } + + getSession(sessionId: string): AuthStateSnapshot | null { + const row = this.db + .prepare(`SELECT payload FROM auth_sessions WHERE session_id = ?`) + .get(sessionId) as { payload: string } | undefined; + + return row ? (JSON.parse(row.payload) as AuthStateSnapshot) : null; + } + + deleteSession(sessionId: string): void { + this.db.prepare(`DELETE FROM auth_sessions WHERE session_id = ?`).run(sessionId); + } + + upsertChats(sessionId: string, chats: ChatRecord[]): void { + const statement = this.db.prepare(` + INSERT INTO chats(id, session_id, payload) + VALUES (@id, @session_id, @payload) + ON CONFLICT(id, session_id) DO UPDATE SET payload = excluded.payload + `); + const transaction = this.db.transaction((items: ChatRecord[]) => { + for (const chat of items) { + statement.run({ + id: chat.id, + session_id: sessionId, + payload: JSON.stringify(chat), + }); + } + }); + transaction(chats); + } + + upsertContacts(sessionId: string, contacts: ContactRecord[]): void { + const statement = this.db.prepare(` + INSERT INTO contacts(id, session_id, payload) + VALUES (@id, @session_id, @payload) + ON CONFLICT(id, session_id) DO UPDATE SET payload = excluded.payload + `); + const transaction = this.db.transaction((items: ContactRecord[]) => { + for (const contact of items) { + statement.run({ + id: contact.id, + session_id: sessionId, + payload: JSON.stringify(contact), + }); + } + }); + transaction(contacts); + } + + upsertGroups(sessionId: string, groups: GroupRecord[]): void { + const statement = this.db.prepare(` + INSERT INTO groups(id, session_id, payload) + VALUES (@id, @session_id, @payload) + ON CONFLICT(id, session_id) DO UPDATE SET payload = excluded.payload + `); + const transaction = this.db.transaction((items: GroupRecord[]) => { + for (const group of items) { + statement.run({ + id: group.id, + session_id: sessionId, + payload: JSON.stringify(group), + }); + } + }); + transaction(groups); + } + + upsertMessages(sessionId: string, messages: IncomingMessage[]): void { + const statement = this.db.prepare(` + INSERT INTO messages(id, session_id, remote_jid, payload) + VALUES (@id, @session_id, @remote_jid, @payload) + ON CONFLICT(id, session_id) DO UPDATE SET payload = excluded.payload + `); + const transaction = this.db.transaction((items: IncomingMessage[]) => { + for (const message of items) { + statement.run({ + id: message.id, + session_id: sessionId, + remote_jid: message.chatId ?? message.remoteJid ?? message.to ?? message.from ?? "", + payload: JSON.stringify(message), + }); + } + }); + transaction(messages); + } + + upsertAck(sessionId: string, ack: MessageAck): void { + this.db + .prepare(` + INSERT INTO message_acks(message_id, session_id, payload) + VALUES (@message_id, @session_id, @payload) + ON CONFLICT(message_id, session_id) DO UPDATE SET payload = excluded.payload + `) + .run({ + message_id: ack.messageId, + session_id: sessionId, + payload: JSON.stringify(ack), + }); + } +} diff --git a/packages/store/tsconfig.json b/packages/store/tsconfig.json new file mode 100644 index 0000000..1fa4f81 --- /dev/null +++ b/packages/store/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../events" + } + ] +} + diff --git a/packages/studio/.gitignore b/packages/studio/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/packages/studio/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/studio/README.md b/packages/studio/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/packages/studio/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/packages/studio/components.json b/packages/studio/components.json new file mode 100644 index 0000000..5c23ec4 --- /dev/null +++ b/packages/studio/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/packages/studio/eslint.config.js b/packages/studio/eslint.config.js new file mode 100644 index 0000000..e7f2d04 --- /dev/null +++ b/packages/studio/eslint.config.js @@ -0,0 +1,26 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + rules: { + 'react-hooks/set-state-in-effect': 'off', + 'react-refresh/only-export-components': 'off', + }, + }, +]) diff --git a/packages/studio/index.html b/packages/studio/index.html new file mode 100644 index 0000000..789f7a6 --- /dev/null +++ b/packages/studio/index.html @@ -0,0 +1,18 @@ + + + + + + + + + Berry Studio · WhatsApp Flow Builder + + +
+ + + diff --git a/packages/studio/package.json b/packages/studio/package.json new file mode 100644 index 0000000..5393abf --- /dev/null +++ b/packages/studio/package.json @@ -0,0 +1,43 @@ +{ + "name": "studio", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-label": "^2.1.7", + "@tailwindcss/vite": "^4.3.0", + "@xyflow/react": "^12.10.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.16.0", + "qrcode": "^1.5.4", + "radix-ui": "^1.4.3", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-router-dom": "^7.15.1", + "reactflow": "^11.11.4", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } +} diff --git a/packages/studio/public/favicon.svg b/packages/studio/public/favicon.svg new file mode 100644 index 0000000..8b810b3 --- /dev/null +++ b/packages/studio/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/studio/public/icons.svg b/packages/studio/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/packages/studio/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/studio/src/App.css b/packages/studio/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/packages/studio/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx new file mode 100644 index 0000000..f148792 --- /dev/null +++ b/packages/studio/src/App.tsx @@ -0,0 +1,41 @@ +import { lazy, Suspense } from "react"; +import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; + +import { StudioAuthGate } from "@/components/flow/StudioAuthGate"; +import { Skeleton } from "@/components/ui/skeleton"; + +const AnalyticsPage = lazy(() => import("@/pages/AnalyticsPage")); +const ConnectionsPage = lazy(() => import("@/pages/ConnectionsPage")); +const FlowEditorPage = lazy(() => import("@/pages/FlowEditorPage")); +const HomePage = lazy(() => import("@/pages/HomePage")); +const NotificationsPage = lazy(() => import("@/pages/NotificationsPage")); +const StudioPage = lazy(() => import("@/pages/StudioPage")); +const TutorialsPage = lazy(() => import("@/pages/TutorialsPage")); + +export default function App() { + return ( + + + + + + + } + > + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); +} diff --git a/packages/studio/src/assets/hero.png b/packages/studio/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/packages/studio/src/assets/hero.png differ diff --git a/packages/studio/src/assets/react.svg b/packages/studio/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/packages/studio/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/studio/src/assets/vite.svg b/packages/studio/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/packages/studio/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/packages/studio/src/components/flow/AnalysisView.tsx b/packages/studio/src/components/flow/AnalysisView.tsx new file mode 100644 index 0000000..797b03b --- /dev/null +++ b/packages/studio/src/components/flow/AnalysisView.tsx @@ -0,0 +1,132 @@ +import { useEffect, useMemo, useState } from "react"; +import { BarChart3, MessageSquare, Users, Target, TrendingUp } from "lucide-react"; +import { getStudioAnalytics } from "@/lib/studio-api"; + +type Metric = { + visitors: number; + conversations: number; + leads: number; + conversions: number; + conversionRate: number; +}; + +export function AnalysisView({ flowId, flowName }: { flowId: string; flowName: string }) { + const [metrics, setMetrics] = useState({ + visitors: 0, + conversations: 0, + leads: 0, + conversions: 0, + conversionRate: 0, + }); + const [series, setSeries] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let active = true; + + const load = async () => { + setLoading(true); + setError(null); + try { + const analytics = await getStudioAnalytics(flowId, 7); + if (!active) { + return; + } + + setMetrics({ + visitors: analytics.totals.visitors, + conversations: analytics.totals.conversations, + leads: analytics.totals.leads, + conversions: analytics.totals.conversions, + conversionRate: analytics.totals.conversionRate, + }); + setSeries(analytics.activity.map((item) => item.value)); + } catch (loadError) { + if (!active) { + return; + } + + setError(loadError instanceof Error ? loadError.message : "Failed to load analytics."); + } finally { + if (active) { + setLoading(false); + } + } + }; + + void load(); + + return () => { + active = false; + }; + }, [flowId]); + + const max = useMemo(() => Math.max(1, ...series), [series]); + + const stats = [ + { icon: MessageSquare, label: "Conversations", value: metrics.conversations }, + { icon: Users, label: "Leads", value: metrics.leads }, + { icon: Target, label: "Conversion", value: `${metrics.conversionRate}%` }, + { icon: TrendingUp, label: "Visitors", value: metrics.visitors }, + ]; + + return ( +
+
+
+
+ +
+
+

Flow analytics

+

Performance for “{flowName}” over the last 7 days.

+
+
+ + {error ? ( +
+ {error} +
+ ) : null} + +
+ {stats.map((stat) => { + const Icon = stat.icon; + return ( +
+
+ +
+
{loading ? "…" : stat.value}
+
{stat.label}
+
+ ); + })} +
+ +
+

Conversations per day

+ {loading ? ( +
+ ) : ( +
+ {series.length === 0 ? ( +
+ No executions recorded yet. +
+ ) : ( + series.map((value, index) => ( +
+
+
D{index + 1}
+
+ )) + )} +
+ )} +
+
+
+ ); +} diff --git a/packages/studio/src/components/flow/AppShell.tsx b/packages/studio/src/components/flow/AppShell.tsx new file mode 100644 index 0000000..08b2ebe --- /dev/null +++ b/packages/studio/src/components/flow/AppShell.tsx @@ -0,0 +1,26 @@ +import type { ReactNode } from "react" + +import { Separator } from "@/components/ui/separator" +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar" + +import { Sidebar } from "./Sidebar" + +export function AppShell({ children }: { children: ReactNode }) { + return ( + + + +
+ + + Berry Studio +
+
{children}
+
+
+ ) +} diff --git a/packages/studio/src/components/flow/BlockMenu.tsx b/packages/studio/src/components/flow/BlockMenu.tsx new file mode 100644 index 0000000..533a10c --- /dev/null +++ b/packages/studio/src/components/flow/BlockMenu.tsx @@ -0,0 +1,73 @@ +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" + +import { BLOCK_META, type BlockKind } from "./BlockNode" + +const ORDER: BlockKind[] = [ + "message", + "action", + "input", + "condition", + "setVariable", + "delay", + "note", + "end", +] + +export function BlockMenu({ + x, + y, + onPick, + onClose, +}: { + x: number + y: number + onPick: (kind: BlockKind) => void + onClose: () => void +}) { + return ( + <> + + ) + })} + + + + ) +} diff --git a/packages/studio/src/components/flow/BlockNode.tsx b/packages/studio/src/components/flow/BlockNode.tsx new file mode 100644 index 0000000..39698c2 --- /dev/null +++ b/packages/studio/src/components/flow/BlockNode.tsx @@ -0,0 +1,217 @@ +import { Handle, Position, type NodeProps } from "@xyflow/react" +import { + Braces, + CircleStop, + Clock3, + Copy, + Flag, + GitBranch, + MessageSquare, + MousePointerClick, + Pencil, + Play, + Plus, + StickyNote, + TextCursorInput, + Trash2, +} from "lucide-react" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { cn } from "@/lib/utils" + +export type BlockKind = + | "start" + | "message" + | "action" + | "input" + | "condition" + | "setVariable" + | "delay" + | "end" + | "note" + +export const BLOCK_META: Record< + BlockKind, + { label: string; icon: typeof MessageSquare; description: string } +> = { + start: { label: "Início", icon: Play, description: "Ponto de entrada" }, + message: { label: "Mensagem", icon: MessageSquare, description: "Método BerryProtocol" }, + action: { label: "Ação", icon: MousePointerClick, description: "Editar, reagir ou apagar" }, + input: { label: "Capturar resposta", icon: TextCursorInput, description: "Salva uma resposta" }, + condition: { label: "Condição", icon: GitBranch, description: "Cria dois caminhos" }, + setVariable: { label: "Definir variável", icon: Braces, description: "Atualiza o contexto" }, + delay: { label: "Aguardar", icon: Clock3, description: "Pausa a execução" }, + end: { label: "Fim", icon: CircleStop, description: "Encerra o fluxo" }, + note: { label: "Nota", icon: StickyNote, description: "Visível só no editor" }, +} + +export type BlockNodeData = { + kind: BlockKind + label?: string + text?: string + messageType?: string + capabilityLabel?: string + payload?: Record + prompt?: string + variableName?: string + variable?: string + operator?: string + value?: string + key?: string + delayMs?: number + onAdd?: (id: string, handle?: string) => void + onEdit?: (id: string) => void + onDuplicate?: (id: string) => void + onDelete?: (id: string) => void +} + +const getSummary = (data: BlockNodeData) => { + if (data.kind === "message" || data.kind === "action") { + const payload = data.payload ?? {} + return String( + payload.text + ?? payload.caption + ?? payload.title + ?? data.capabilityLabel + ?? data.messageType + ?? "Configure o método", + ) + } + + if (data.kind === "input") { + return data.prompt || `Salvar em {{${data.variableName || "lastInput"}}}` + } + + if (data.kind === "condition") { + return `${data.variable || "lastInput"} ${data.operator || "equals"} ${data.value || "…"}` + } + + if (data.kind === "setVariable") { + return `${data.key || "variavel"} = ${data.value || "…"}` + } + + if (data.kind === "delay") { + return `${data.delayMs ?? 1000} ms` + } + + return data.text || BLOCK_META[data.kind].description +} + +export function BlockNode({ id, data, selected }: NodeProps) { + const block = data as unknown as BlockNodeData + const meta = BLOCK_META[block.kind] ?? BLOCK_META.message + const Icon = meta.icon + const isStart = block.kind === "start" + const isEnd = block.kind === "end" + const isCondition = block.kind === "condition" + const isNote = block.kind === "note" + + return ( +
+ {!isStart && !isNote ? ( + + ) : null} + + block.onEdit?.(id)} + > + + + + +
+ {block.label || meta.label} +

+ {block.capabilityLabel || meta.description} +

+
+ {isStart ? Entrada : null} +
+ +

+ {getSummary(block)} +

+ {isCondition ? ( +
+ Verdadeiro + Falso +
+ ) : null} +
+
+ +
+ + {!isStart ? ( + + ) : null} + {!isStart ? ( + + ) : null} +
+ + {isCondition ? ( + <> + + + + ) : !isEnd && !isNote ? ( + + ) : null} + + {!isEnd && !isNote ? ( + + ) : null} + + {isStart ? ( + + ) : null} +
+ ) +} diff --git a/packages/studio/src/components/flow/ConstructorView.tsx b/packages/studio/src/components/flow/ConstructorView.tsx new file mode 100644 index 0000000..7638cd2 --- /dev/null +++ b/packages/studio/src/components/flow/ConstructorView.tsx @@ -0,0 +1,148 @@ +import type { Edge, Node } from "@xyflow/react" +import { Pencil, Trash2 } from "lucide-react" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Card, + CardAction, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from "@/components/ui/empty" + +import { BLOCK_META, type BlockNodeData } from "./BlockNode" + +export function ConstructorView({ + nodes, + edges, + onEdit, + onDelete, +}: { + nodes: Node[] + edges: Edge[] + onEdit: (id: string) => void + onDelete: (id: string) => void +}) { + const startNode = nodes.find( + (node) => (node.data as unknown as BlockNodeData).kind === "start", + ) + const ordered: Node[] = [] + const seen = new Set() + + const walk = (id: string | undefined) => { + if (!id || seen.has(id)) return + const node = nodes.find((item) => item.id === id) + if (!node) return + seen.add(id) + ordered.push(node) + const outgoing = edges.filter((edge) => edge.source === id) + outgoing.forEach((edge) => walk(edge.target)) + } + + walk(startNode?.id) + nodes.forEach((node) => { + if (!seen.has(node.id)) ordered.push(node) + }) + + return ( +
+
+
+

Visão linear

+

+ Revise a sequência e abra qualquer bloco para editar no canvas. +

+
+ + {!ordered.length ? ( + + + + + + Nenhum bloco no fluxo + + Volte ao canvas e adicione o primeiro bloco. + + + + ) : null} + + {ordered.map((node, index) => { + const data = node.data as unknown as BlockNodeData + const meta = BLOCK_META[data.kind] ?? BLOCK_META.message + const Icon = meta.icon + const outgoing = edges.filter((edge) => edge.source === node.id) + + return ( + + +
+ + + +
+ + {index + 1}. {data.label || meta.label} + + + {data.capabilityLabel || meta.description} + +
+
+ + {data.kind !== "start" && data.kind !== "end" ? ( + + ) : null} + {data.kind !== "start" ? ( + + ) : null} + +
+ + {node.type} + {outgoing.length ? ( + outgoing.map((edge) => ( + + {edge.sourceHandle && edge.sourceHandle !== "next" + ? `${edge.sourceHandle} → ` + : "Próximo → "} + {ordered.findIndex((item) => item.id === edge.target) + 1} + + )) + ) : ( + + Sem próxima etapa + + )} + +
+ ) + })} +
+
+ ) +} diff --git a/packages/studio/src/components/flow/EditPanel.tsx b/packages/studio/src/components/flow/EditPanel.tsx new file mode 100644 index 0000000..bb2797b --- /dev/null +++ b/packages/studio/src/components/flow/EditPanel.tsx @@ -0,0 +1,431 @@ +import type { Node } from "@xyflow/react" +import { Braces, Trash2 } from "lucide-react" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Field, + FieldDescription, + FieldGroup, + FieldLabel, +} from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Switch } from "@/components/ui/switch" +import { Textarea } from "@/components/ui/textarea" +import type { + StudioCapability, + StudioFieldDefinition, +} from "@/lib/studio-api" + +import { BLOCK_META, type BlockKind, type BlockNodeData } from "./BlockNode" + +const payloadInputValue = (value: unknown, field: StudioFieldDefinition) => { + if (field.type === "json" && value !== undefined && typeof value !== "string") { + return JSON.stringify(value, null, 2) + } + return value == null ? "" : String(value) +} + +const defaultPayload = (capability: StudioCapability) => + Object.fromEntries( + capability.fields.map((field) => [ + field.key, + field.defaultValue ?? (field.type === "checkbox" ? false : ""), + ]), + ) + +export function EditPanel({ + node, + messageCapabilities, + actionCapabilities, + conditionOperators, + onClose, + onUpdateNodeData, + onDelete, +}: { + node: Node + messageCapabilities: StudioCapability[] + actionCapabilities: StudioCapability[] + conditionOperators: Array<{ label: string; value: string }> + onClose: () => void + onUpdateNodeData: (id: string, patch: Record) => void + onDelete: (id: string) => void +}) { + const data = node.data as unknown as BlockNodeData + const kind = (data.kind || node.type || "message") as BlockKind + const meta = BLOCK_META[kind] ?? BLOCK_META.message + const capabilities = kind === "action" ? actionCapabilities : messageCapabilities + const selectedCapability = capabilities.find( + (capability) => capability.id === data.messageType, + ) + const payload = data.payload ?? {} + + const updatePayload = (key: string, value: unknown) => { + onUpdateNodeData(node.id, { + payload: { + ...payload, + [key]: value, + }, + }) + } + + return ( + !open && onClose()}> + + + + + {meta.label} + + + Configure o comportamento deste bloco. Use {"{{variavel}}"} em textos + e estruturas JSON. + + + +
+ + + Nome do bloco + + onUpdateNodeData(node.id, { label: event.target.value }) + } + /> + + + {kind === "message" || kind === "action" ? ( + <> + + Método do BerryProtocol + + {selectedCapability ? ( + + {selectedCapability.description} + + ) : null} + + + {selectedCapability?.fields.map((field) => ( + updatePayload(field.key, value)} + /> + ))} + + ) : null} + + {kind === "input" ? ( + <> + + + Mensagem de pergunta + +