From 8884522c74be5ffe41e72bc31a00f20b9b635779 Mon Sep 17 00:00:00 2001 From: Samuel Corsan <120322525+samuelcorsan@users.noreply.github.com> Date: Wed, 25 Feb 2026 04:51:36 +0100 Subject: [PATCH 1/4] feat: add WhatsApp support --- packages/adapter-shared/src/card-utils.ts | 8 +- packages/adapter-whatsapp-web/README.md | 90 ++ packages/adapter-whatsapp-web/package.json | 56 + .../adapter-whatsapp-web/src/index.test.ts | 100 ++ packages/adapter-whatsapp-web/src/index.ts | 575 ++++++++ .../adapter-whatsapp-web/src/markdown.test.ts | 107 ++ packages/adapter-whatsapp-web/src/markdown.ts | 161 +++ packages/adapter-whatsapp-web/src/types.ts | 12 + packages/adapter-whatsapp-web/tsconfig.json | 10 + packages/adapter-whatsapp-web/tsup.config.ts | 10 + .../adapter-whatsapp-web/vitest.config.ts | 14 + pnpm-lock.yaml | 1254 ++++++++++++++++- turbo.json | 1 + 13 files changed, 2381 insertions(+), 17 deletions(-) create mode 100644 packages/adapter-whatsapp-web/README.md create mode 100644 packages/adapter-whatsapp-web/package.json create mode 100644 packages/adapter-whatsapp-web/src/index.test.ts create mode 100644 packages/adapter-whatsapp-web/src/index.ts create mode 100644 packages/adapter-whatsapp-web/src/markdown.test.ts create mode 100644 packages/adapter-whatsapp-web/src/markdown.ts create mode 100644 packages/adapter-whatsapp-web/src/types.ts create mode 100644 packages/adapter-whatsapp-web/tsconfig.json create mode 100644 packages/adapter-whatsapp-web/tsup.config.ts create mode 100644 packages/adapter-whatsapp-web/vitest.config.ts diff --git a/packages/adapter-shared/src/card-utils.ts b/packages/adapter-shared/src/card-utils.ts index 27a364a2..9a13c1d7 100644 --- a/packages/adapter-shared/src/card-utils.ts +++ b/packages/adapter-shared/src/card-utils.ts @@ -11,7 +11,12 @@ import { convertEmojiPlaceholders } from "chat"; /** * Supported platform names for adapter utilities. */ -export type PlatformName = "slack" | "gchat" | "teams" | "discord"; +export type PlatformName = + | "slack" + | "gchat" + | "teams" + | "discord" + | "whatsapp"; /** * Button style mappings per platform. @@ -27,6 +32,7 @@ export const BUTTON_STYLE_MAPPINGS: Record< gchat: { primary: "primary", danger: "danger" }, // Colors handled via buttonColor teams: { primary: "positive", danger: "destructive" }, discord: { primary: "primary", danger: "danger" }, + whatsapp: { primary: "primary", danger: "danger" }, }; /** diff --git a/packages/adapter-whatsapp-web/README.md b/packages/adapter-whatsapp-web/README.md new file mode 100644 index 00000000..2453cf0f --- /dev/null +++ b/packages/adapter-whatsapp-web/README.md @@ -0,0 +1,90 @@ +# @chat-adapter/whatsapp-web + +WhatsApp Web adapter for [Chat SDK](https://chat-sdk.dev/docs). Uses [whatsapp-web.js](https://wwebjs.dev/) to connect via WhatsApp Web (Puppeteer-based). + +## Installation + +```bash +npm install chat @chat-adapter/whatsapp-web +``` + +## Usage + +Unlike Slack or Teams, WhatsApp uses a real-time connection instead of webhooks. Call `adapter.start()` after initialization and scan the QR code with your phone. + +```typescript +import { Chat } from "chat"; +import { createWhatsAppAdapter } from "@chat-adapter/whatsapp-web"; + +const adapter = createWhatsAppAdapter({ + userName: "My Bot", + sessionPath: process.env.WHATSAPP_SESSION_PATH ?? ".wwebjs_auth", +}); + +const bot = new Chat({ + userName: "My Bot", + adapters: { whatsapp: adapter }, + state: yourStateAdapter, +}); + +await bot.initialize(); +await adapter.start(); + +// QR code available via adapter.getQRCode() - display for user to scan +// Once connected, adapter.isConnected() returns true + +bot.onNewMessage(/.*/, async (thread, message) => { + await thread.subscribe(); + await thread.post(`Echo: ${message.text}`); +}); +``` + +## Configuration + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `logger` | `Logger` | `ConsoleLogger("info")` | Logger instance | +| `userName` | `string` | `"bot"` | Bot display name | +| `sessionPath` | `string` | `".wwebjs_auth"` or `WHATSAPP_SESSION_PATH` | Path for session persistence | +| `puppeteerOptions` | `object` | `{}` | Options passed to Puppeteer | + +## Thread ID Format + +`whatsapp:{chatId}` + +- DMs: `whatsapp:1234567890@c.us` +- Groups: `whatsapp:1234567890-1234567890@g.us` + +## Features + +- **Messages**: Send and receive text, markdown (`*bold*`, `_italic_`, `~strikethrough~`), files +- **Reactions**: Add and remove emoji reactions +- **Typing indicator**: `thread.startTyping()` +- **Message history**: `adapter.fetchMessages()` +- **Cards**: Rendered as text fallback (WhatsApp has no native cards) + +## Limitations + +- **No message editing**: WhatsApp Web API does not support editing messages +- **No webhooks**: Uses WebSocket-style events; `handleWebhook` returns a status response only +- **Persistent process**: Requires a long-running Node process (not suitable for serverless) +- **Puppeteer/Chrome**: whatsapp-web.js uses headless Chrome + +## Testing + +```bash +# Unit tests (no WhatsApp connection) +pnpm --filter @chat-adapter/whatsapp-web test +``` + +A live test script is available at `scripts/test-live.ts` for manual verification. It requires `@chat-adapter/state-memory`, `tsx`, and `qrcode-terminal` as dev dependencies. Have another phone or account send a message to your linked number—messaging yourself does not work, as the SDK skips messages from the bot. + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `WHATSAPP_SESSION_PATH` | Session storage path (default: `.wwebjs_auth`) | + +## License + +MIT diff --git a/packages/adapter-whatsapp-web/package.json b/packages/adapter-whatsapp-web/package.json new file mode 100644 index 00000000..b6700482 --- /dev/null +++ b/packages/adapter-whatsapp-web/package.json @@ -0,0 +1,56 @@ +{ + "name": "@chat-adapter/whatsapp-web", + "version": "4.14.0", + "description": "WhatsApp Web adapter for chat", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest run --coverage", + "test:watch": "vitest", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "dependencies": { + "@chat-adapter/shared": "workspace:*", + "chat": "workspace:*", + "whatsapp-web.js": "^1.26.0" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsup": "^8.3.5", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vercel/chat.git", + "directory": "packages/adapter-whatsapp-web" + }, + "homepage": "https://github.com/vercel/chat#readme", + "bugs": { + "url": "https://github.com/vercel/chat/issues" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "chat", + "whatsapp", + "bot", + "adapter" + ], + "license": "MIT" +} diff --git a/packages/adapter-whatsapp-web/src/index.test.ts b/packages/adapter-whatsapp-web/src/index.test.ts new file mode 100644 index 00000000..09b0ef86 --- /dev/null +++ b/packages/adapter-whatsapp-web/src/index.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; +import { WhatsAppFormatConverter } from "./markdown"; + +describe("WhatsAppFormatConverter", () => { + const converter = new WhatsAppFormatConverter(); + + describe("toAst", () => { + it("should parse plain text", () => { + const result = converter.toAst("Hello world"); + expect(result.type).toBe("root"); + expect(result.children.length).toBeGreaterThan(0); + }); + + it("should convert WhatsApp bold (*text*) to markdown AST", () => { + const result = converter.toAst("This is *bold* text"); + const text = converter.extractPlainText("This is *bold* text"); + expect(text).toContain("bold"); + }); + + it("should convert WhatsApp strikethrough (~text~) to markdown AST", () => { + const result = converter.toAst("This is ~strikethrough~ text"); + expect(result.type).toBe("root"); + }); + }); + + describe("fromAst", () => { + it("should render plain text", () => { + const ast = converter.toAst("Hello world"); + const result = converter.fromAst(ast); + expect(result).toContain("Hello"); + expect(result).toContain("world"); + }); + }); + + describe("renderPostable", () => { + it("should render string messages", () => { + const result = converter.renderPostable("Hello world"); + expect(result).toBe("Hello world"); + }); + + it("should render raw messages", () => { + const result = converter.renderPostable({ raw: "Hello *bold*" }); + expect(result).toBe("Hello *bold*"); + }); + + it("should render markdown messages", () => { + const result = converter.renderPostable({ markdown: "**bold** text" }); + expect(result).toContain("*bold*"); + }); + }); +}); + +describe("WhatsAppAdapter", () => { + describe("encodeThreadId / decodeThreadId", () => { + it("should encode and decode thread IDs correctly", async () => { + const { WhatsAppAdapter } = await import("./index"); + const { ConsoleLogger } = await import("chat"); + + const adapter = new WhatsAppAdapter({ + logger: new ConsoleLogger("silent"), + }); + + const encoded = adapter.encodeThreadId({ + chatId: "1234567890@c.us", + }); + expect(encoded).toBe("whatsapp:1234567890@c.us"); + + const decoded = adapter.decodeThreadId("whatsapp:1234567890@c.us"); + expect(decoded.chatId).toBe("1234567890@c.us"); + }); + + it("should handle group chat IDs", async () => { + const { WhatsAppAdapter } = await import("./index"); + const { ConsoleLogger } = await import("chat"); + + const adapter = new WhatsAppAdapter({ + logger: new ConsoleLogger("silent"), + }); + + const encoded = adapter.encodeThreadId({ + chatId: "1234567890-1234567890@g.us", + }); + expect(encoded).toBe("whatsapp:1234567890-1234567890@g.us"); + }); + }); + + describe("isDM", () => { + it("should correctly identify DM chats", async () => { + const { WhatsAppAdapter } = await import("./index"); + const { ConsoleLogger } = await import("chat"); + + const adapter = new WhatsAppAdapter({ + logger: new ConsoleLogger("silent"), + }); + + expect(adapter.isDM("whatsapp:1234567890@c.us")).toBe(true); + expect(adapter.isDM("whatsapp:1234567890-1234567890@g.us")).toBe(false); + }); + }); +}); diff --git a/packages/adapter-whatsapp-web/src/index.ts b/packages/adapter-whatsapp-web/src/index.ts new file mode 100644 index 00000000..a93ae485 --- /dev/null +++ b/packages/adapter-whatsapp-web/src/index.ts @@ -0,0 +1,575 @@ +/** + * WhatsApp Web adapter for chat-sdk. + * + * Uses whatsapp-web.js library which manages WhatsApp Web through Puppeteer. + * This adapter requires a persistent session to maintain the WhatsApp connection. + * + * Important: WhatsApp doesn't have a traditional webhook model. Instead, the client + * connects via WebSocket and receives events in real-time. The handleWebhook method + * is provided for consistency but the primary message flow is through event listeners. + */ + +import { + cardToFallbackText, + extractCard, + extractFiles, + NetworkError, + toBuffer, + ValidationError, +} from "@chat-adapter/shared"; +import type { + Adapter, + AdapterPostableMessage, + ChannelInfo, + ChatInstance, + EmojiValue, + FetchOptions, + FetchResult, + FormattedContent, + Logger, + RawMessage, + ThreadInfo, + WebhookOptions, +} from "chat"; +import { + ConsoleLogger, + convertEmojiPlaceholders, + defaultEmojiResolver, + Message, +} from "chat"; +import type WAWebJS from "whatsapp-web.js"; +import { WhatsAppFormatConverter } from "./markdown"; +import type { WhatsAppAdapterConfig, WhatsAppThreadId } from "./types"; + +export class WhatsAppAdapter implements Adapter { + readonly name = "whatsapp"; + readonly userName: string; + readonly botUserId?: string; + + private client: WAWebJS.Client | null = null; + private chat: ChatInstance | null = null; + private readonly logger: Logger; + private readonly formatConverter = new WhatsAppFormatConverter(); + private readonly sessionPath: string; + private readonly puppeteerOptions: Record; + private isReady = false; + private qrCode: string | null = null; + + constructor(config: WhatsAppAdapterConfig) { + this.logger = config.logger; + this.userName = config.userName ?? "bot"; + this.sessionPath = config.sessionPath ?? ".wwebjs_auth"; + this.puppeteerOptions = config.puppeteerOptions ?? {}; + } + + async initialize(chat: ChatInstance): Promise { + this.chat = chat; + + const wa = await import("whatsapp-web.js"); + const mod = wa as unknown as { default?: typeof wa; Client?: unknown; LocalAuth?: new (opts?: { dataPath?: string }) => unknown }; + const { Client, LocalAuth } = mod.default ?? mod; + + this.client = new (Client as new (opts: unknown) => WAWebJS.Client)({ + authStrategy: new (LocalAuth as new (opts?: { dataPath?: string }) => unknown)({ + dataPath: this.sessionPath, + }), + puppeteer: { + headless: true, + args: ["--no-sandbox", "--disable-setuid-sandbox"], + ...this.puppeteerOptions, + }, + }); + + this.setupEventListeners(); + + this.logger.info("WhatsApp adapter initializing..."); + } + + private setupEventListeners(): void { + if (!this.client) return; + + this.client.on("qr", (qr: string) => { + this.qrCode = qr; + this.logger.info( + "WhatsApp QR code received. Scan with your phone to authenticate." + ); + }); + + this.client.on("authenticated", () => { + this.logger.info("WhatsApp authenticated successfully"); + }); + + this.client.on("auth_failure", (msg: string) => { + this.logger.error("WhatsApp authentication failed", { error: msg }); + }); + + this.client.on("ready", () => { + this.isReady = true; + const info = this.client?.info; + if (info) { + (this as { botUserId: string }).botUserId = info.wid._serialized; + } + this.logger.info("WhatsApp client ready", { + botUserId: this.botUserId, + }); + }); + + this.client.on("disconnected", (reason: string) => { + this.isReady = false; + this.logger.warn("WhatsApp disconnected", { reason }); + }); + + this.client.on("message_create", async (message: WAWebJS.Message) => { + if (message.fromMe) { + return; + } + await this.handleIncomingMessage(message); + }); + + this.client.on( + "message_reaction", + async (reaction: WAWebJS.Reaction) => { + await this.handleReaction(reaction); + } + ); + } + + async start(): Promise { + if (!this.client) { + throw new Error( + "WhatsApp client not initialized. Call initialize() first." + ); + } + await this.client.initialize(); + } + + async stop(): Promise { + if (this.client) { + await this.client.destroy(); + this.isReady = false; + } + } + + getQRCode(): string | null { + return this.qrCode; + } + + isConnected(): boolean { + return this.isReady; + } + + async handleWebhook( + _request: Request, + _options?: WebhookOptions + ): Promise { + return new Response( + JSON.stringify({ + message: + "WhatsApp uses real-time WebSocket connection, not webhooks. Use start() to connect.", + isConnected: this.isReady, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + private async handleIncomingMessage(message: WAWebJS.Message): Promise { + if (!this.chat) { + this.logger.warn("Chat instance not initialized, ignoring message"); + return; + } + + const chatId = message.from; + const threadId = this.encodeThreadId({ chatId }); + + const contact = await message.getContact(); + const isMe = message.fromMe; + + const isMention = await this.checkIfMentioned(message); + + const chatMessage = new Message({ + id: message.id._serialized, + threadId, + text: this.formatConverter.extractPlainText(message.body), + formatted: this.formatConverter.toAst(message.body), + raw: message, + author: { + userId: contact.id._serialized, + userName: contact.pushname || contact.name || contact.id.user || "unknown", + fullName: contact.name || contact.pushname || contact.id.user || "unknown", + isBot: false, + isMe, + }, + metadata: { + dateSent: new Date(message.timestamp * 1000), + edited: false, + }, + attachments: message.hasMedia + ? [ + { + type: "file" as const, + url: undefined, + name: undefined, + mimeType: undefined, + fetchData: async () => { + const media = await message.downloadMedia(); + return Buffer.from(media.data, "base64"); + }, + }, + ] + : [], + isMention, + }); + + try { + await this.chat.handleIncomingMessage(this, threadId, chatMessage); + } catch (error) { + this.logger.error("Error handling incoming message", { + error: String(error), + messageId: message.id._serialized, + }); + } + } + + private async checkIfMentioned(message: WAWebJS.Message): Promise { + if (!this.botUserId) return false; + const mentions = await message.getMentions(); + return mentions.some((m) => m.id._serialized === this.botUserId); + } + + private async handleReaction(reaction: WAWebJS.Reaction): Promise { + if (!this.chat) return; + + const threadId = this.encodeThreadId({ chatId: reaction.id.remote }); + const rawEmoji = reaction.reaction; + const normalizedEmoji = defaultEmojiResolver.fromGChat(rawEmoji); + + const added = !!reaction.reaction; + + const reactionEvent = { + adapter: this as Adapter, + threadId, + messageId: reaction.msgId._serialized, + emoji: normalizedEmoji, + rawEmoji, + added, + user: { + userId: reaction.senderId, + userName: reaction.senderId, + fullName: reaction.senderId, + isBot: false, + isMe: reaction.senderId === this.botUserId, + }, + raw: reaction, + }; + + this.chat.processReaction(reactionEvent); + } + + async postMessage( + threadId: string, + message: AdapterPostableMessage + ): Promise> { + if (!this.client || !this.isReady) { + throw new NetworkError("whatsapp", "WhatsApp client not connected"); + } + + const { chatId } = this.decodeThreadId(threadId); + + const card = extractCard(message); + let content: string; + + if (card) { + content = cardToFallbackText(card, { + boldFormat: "*", + lineBreak: "\n", + platform: "whatsapp", + }); + } else { + content = convertEmojiPlaceholders( + this.formatConverter.renderPostable(message), + "whatsapp" + ); + } + + const files = extractFiles(message); + + this.logger.debug("WhatsApp: sending message", { + chatId, + contentLength: content.length, + fileCount: files.length, + }); + + let result: WAWebJS.Message; + + if (files.length > 0) { + const wa = await import("whatsapp-web.js"); + const { MessageMedia } = (wa as unknown as { default?: typeof wa }).default ?? wa; + for (const file of files) { + const buffer = await toBuffer(file.data, { platform: "whatsapp" }); + if (!buffer) continue; + const media = new MessageMedia( + file.mimeType || "application/octet-stream", + buffer.toString("base64"), + file.filename + ); + result = await this.client.sendMessage(chatId, media, { + caption: content, + }); + } + } else { + result = await this.client.sendMessage(chatId, content); + } + + this.logger.debug("WhatsApp: message sent", { + messageId: result!.id._serialized, + }); + + return { + id: result!.id._serialized, + threadId, + raw: result!, + }; + } + + async editMessage( + _threadId: string, + _messageId: string, + _message: AdapterPostableMessage + ): Promise> { + throw new NetworkError( + "whatsapp", + "WhatsApp does not support editing messages via the Web API" + ); + } + + async deleteMessage(threadId: string, messageId: string): Promise { + if (!this.client || !this.isReady) { + throw new NetworkError("whatsapp", "WhatsApp client not connected"); + } + + const { chatId } = this.decodeThreadId(threadId); + const chat = await this.client.getChatById(chatId); + const messages = await chat.fetchMessages({ limit: 50 }); + const targetMessage = messages.find((m) => m.id._serialized === messageId); + + if (targetMessage && targetMessage.fromMe) { + await targetMessage.delete(true); + this.logger.debug("WhatsApp: message deleted", { messageId }); + } else { + this.logger.warn("WhatsApp: cannot delete message (not found or not ours)", { + messageId, + }); + } + } + + async addReaction( + threadId: string, + messageId: string, + emoji: EmojiValue | string + ): Promise { + if (!this.client || !this.isReady) { + throw new NetworkError("whatsapp", "WhatsApp client not connected"); + } + + const { chatId } = this.decodeThreadId(threadId); + const chat = await this.client.getChatById(chatId); + const messages = await chat.fetchMessages({ limit: 50 }); + const targetMessage = messages.find((m) => m.id._serialized === messageId); + + if (targetMessage) { + const emojiStr = + typeof emoji === "string" ? emoji : defaultEmojiResolver.toGChat(emoji); + await targetMessage.react(emojiStr); + this.logger.debug("WhatsApp: reaction added", { messageId, emoji: emojiStr }); + } + } + + async removeReaction( + threadId: string, + messageId: string, + _emoji: EmojiValue | string + ): Promise { + if (!this.client || !this.isReady) { + throw new NetworkError("whatsapp", "WhatsApp client not connected"); + } + + const { chatId } = this.decodeThreadId(threadId); + const chat = await this.client.getChatById(chatId); + const messages = await chat.fetchMessages({ limit: 50 }); + const targetMessage = messages.find((m) => m.id._serialized === messageId); + + if (targetMessage) { + await targetMessage.react(""); + this.logger.debug("WhatsApp: reaction removed", { messageId }); + } + } + + async startTyping(threadId: string, _status?: string): Promise { + if (!this.client || !this.isReady) { + return; + } + + const { chatId } = this.decodeThreadId(threadId); + const chat = await this.client.getChatById(chatId); + await chat.sendStateTyping(); + } + + async fetchMessages( + threadId: string, + options: FetchOptions = {} + ): Promise> { + if (!this.client || !this.isReady) { + throw new NetworkError("whatsapp", "WhatsApp client not connected"); + } + + const { chatId } = this.decodeThreadId(threadId); + const chat = await this.client.getChatById(chatId); + const limit = options.limit || 50; + + const rawMessages = await chat.fetchMessages({ limit }); + + const messages = await Promise.all( + rawMessages.map(async (msg) => { + const contact = await msg.getContact(); + return new Message({ + id: msg.id._serialized, + threadId, + text: this.formatConverter.extractPlainText(msg.body), + formatted: this.formatConverter.toAst(msg.body), + raw: msg, + author: { + userId: contact.id._serialized, + userName: + contact.pushname || contact.name || contact.id.user || "unknown", + fullName: + contact.name || contact.pushname || contact.id.user || "unknown", + isBot: false, + isMe: msg.fromMe, + }, + metadata: { + dateSent: new Date(msg.timestamp * 1000), + edited: false, + }, + attachments: [], + }); + }) + ); + + return { + messages, + nextCursor: undefined, + }; + } + + async fetchThread(threadId: string): Promise { + if (!this.client || !this.isReady) { + throw new NetworkError("whatsapp", "WhatsApp client not connected"); + } + + const { chatId } = this.decodeThreadId(threadId); + const chat = await this.client.getChatById(chatId); + const isGroup = chat.isGroup; + + return { + id: threadId, + channelId: chatId, + channelName: chat.name, + isDM: !isGroup, + metadata: { + isGroup, + raw: chat, + }, + }; + } + + async openDM(userId: string): Promise { + const chatId = userId.includes("@c.us") ? userId : `${userId}@c.us`; + return this.encodeThreadId({ chatId }); + } + + isDM(threadId: string): boolean { + const { chatId } = this.decodeThreadId(threadId); + return chatId.includes("@c.us"); + } + + encodeThreadId(platformData: WhatsAppThreadId): string { + return `whatsapp:${platformData.chatId}`; + } + + decodeThreadId(threadId: string): WhatsAppThreadId { + const parts = threadId.split(":"); + if (parts.length < 2 || parts[0] !== "whatsapp") { + throw new ValidationError( + "whatsapp", + `Invalid WhatsApp thread ID: ${threadId}` + ); + } + return { + chatId: parts.slice(1).join(":"), + }; + } + + parseMessage(raw: unknown): Message { + const msg = raw as WAWebJS.Message; + const threadId = this.encodeThreadId({ chatId: msg.from }); + + return new Message({ + id: msg.id._serialized, + threadId, + text: this.formatConverter.extractPlainText(msg.body), + formatted: this.formatConverter.toAst(msg.body), + raw: msg, + author: { + userId: msg.author || msg.from, + userName: msg.author || msg.from, + fullName: msg.author || msg.from, + isBot: false, + isMe: msg.fromMe, + }, + metadata: { + dateSent: new Date(msg.timestamp * 1000), + edited: false, + }, + attachments: [], + }); + } + + renderFormatted(content: FormattedContent): string { + return this.formatConverter.fromAst(content); + } + + channelIdFromThreadId(threadId: string): string { + return threadId; + } + + async fetchChannelInfo(channelId: string): Promise { + const threadInfo = await this.fetchThread(channelId); + return { + id: channelId, + name: threadInfo.channelName, + isDM: threadInfo.isDM, + metadata: threadInfo.metadata, + }; + } +} + +export function createWhatsAppAdapter( + config?: Partial +): WhatsAppAdapter { + const resolved: WhatsAppAdapterConfig = { + logger: config?.logger ?? new ConsoleLogger("info").child("whatsapp"), + userName: config?.userName, + sessionPath: config?.sessionPath ?? process.env.WHATSAPP_SESSION_PATH, + puppeteerOptions: config?.puppeteerOptions, + }; + return new WhatsAppAdapter(resolved); +} + +export { + WhatsAppFormatConverter, + WhatsAppFormatConverter as WhatsAppMarkdownConverter, +} from "./markdown"; +export type { WhatsAppAdapterConfig, WhatsAppThreadId } from "./types"; diff --git a/packages/adapter-whatsapp-web/src/markdown.test.ts b/packages/adapter-whatsapp-web/src/markdown.test.ts new file mode 100644 index 00000000..a503528b --- /dev/null +++ b/packages/adapter-whatsapp-web/src/markdown.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from "vitest"; +import { WhatsAppFormatConverter } from "./markdown"; + +describe("WhatsAppFormatConverter", () => { + const converter = new WhatsAppFormatConverter(); + + describe("toAst", () => { + it("parses plain text", () => { + const ast = converter.toAst("Hello world"); + expect(ast.type).toBe("root"); + expect(ast.children.length).toBeGreaterThan(0); + }); + + it("parses WhatsApp bold format", () => { + const text = "This is *bold* text"; + const ast = converter.toAst(text); + expect(ast.type).toBe("root"); + }); + + it("parses WhatsApp italic format", () => { + const text = "This is _italic_ text"; + const ast = converter.toAst(text); + expect(ast.type).toBe("root"); + }); + + it("parses WhatsApp strikethrough format", () => { + const text = "This is ~strikethrough~ text"; + const ast = converter.toAst(text); + expect(ast.type).toBe("root"); + }); + }); + + describe("fromAst", () => { + it("renders bold as *text*", () => { + const ast = converter.toAst("This is **bold** text"); + const result = converter.fromAst(ast); + expect(result).toContain("*bold*"); + }); + + it("renders italic as _text_", () => { + const ast = converter.toAst("This is _italic_ text"); + const result = converter.fromAst(ast); + expect(result).toContain("_italic_"); + }); + + it("renders strikethrough as ~text~", () => { + const ast = converter.toAst("This is ~~strikethrough~~ text"); + const result = converter.fromAst(ast); + expect(result).toContain("~strikethrough~"); + }); + + it("renders links as plain text with URL", () => { + const ast = converter.toAst("[Click here](https://example.com)"); + const result = converter.fromAst(ast); + expect(result).toContain("https://example.com"); + }); + + it("renders unordered lists with bullets", () => { + const ast = converter.toAst("- Item 1\n- Item 2"); + const result = converter.fromAst(ast); + expect(result).toContain("•"); + expect(result).toContain("Item 1"); + expect(result).toContain("Item 2"); + }); + + it("renders ordered lists with numbers", () => { + const ast = converter.toAst("1. First\n2. Second"); + const result = converter.fromAst(ast); + expect(result).toContain("1."); + expect(result).toContain("2."); + }); + }); + + describe("renderPostable", () => { + it("handles string input", () => { + const result = converter.renderPostable("Hello"); + expect(result).toBe("Hello"); + }); + + it("handles raw input", () => { + const result = converter.renderPostable({ raw: "*bold*" }); + expect(result).toBe("*bold*"); + }); + + it("handles markdown input", () => { + const result = converter.renderPostable({ markdown: "**bold**" }); + expect(result).toContain("*bold*"); + }); + + it("handles ast input", () => { + const ast = converter.toAst("Hello world"); + const result = converter.renderPostable({ ast }); + expect(result).toContain("Hello"); + expect(result).toContain("world"); + }); + }); + + describe("extractPlainText", () => { + it("extracts plain text from WhatsApp formatted text", () => { + const text = "Hello *bold* and _italic_ text"; + const result = converter.extractPlainText(text); + expect(result).toContain("Hello"); + expect(result).toContain("bold"); + expect(result).toContain("italic"); + }); + }); +}); diff --git a/packages/adapter-whatsapp-web/src/markdown.ts b/packages/adapter-whatsapp-web/src/markdown.ts new file mode 100644 index 00000000..d5f7f9b9 --- /dev/null +++ b/packages/adapter-whatsapp-web/src/markdown.ts @@ -0,0 +1,161 @@ +/** + * WhatsApp-specific format conversion using AST-based parsing. + * + * WhatsApp uses a format similar to markdown but with some differences: + * - Bold: *text* + * - Italic: _text_ + * - Strikethrough: ~text~ + * - Monospace: ```text``` + * - No link syntax (URLs are auto-linked by the client) + */ + +import { + type AdapterPostableMessage, + BaseFormatConverter, + type Content, + getNodeChildren, + getNodeValue, + isBlockquoteNode, + isCodeNode, + isDeleteNode, + isEmphasisNode, + isInlineCodeNode, + isLinkNode, + isListItemNode, + isListNode, + isParagraphNode, + isStrongNode, + isTextNode, + parseMarkdown, + type Root, +} from "chat"; + +export class WhatsAppFormatConverter extends BaseFormatConverter { + override renderPostable(message: AdapterPostableMessage): string { + if (typeof message === "string") { + return message; + } + if ("raw" in message) { + return message.raw; + } + if ("markdown" in message) { + return this.fromAst(parseMarkdown(message.markdown)); + } + if ("ast" in message) { + return this.fromAst(message.ast); + } + return ""; + } + + fromAst(ast: Root): string { + return this.fromAstWithNodeConverter(ast, (node) => + this.nodeToWhatsApp(node) + ); + } + + toAst(text: string): Root { + let markdown = text; + + // WhatsApp bold: *text* is already standard markdown single asterisk + // but we treat it as bold (like Slack), so convert to double ** + markdown = markdown.replace(/(? ~~text~~ + markdown = markdown.replace(/(? this.nodeToWhatsApp(child)) + .join(""); + } + + if (isTextNode(node)) { + return node.value; + } + + if (isStrongNode(node)) { + const content = getNodeChildren(node) + .map((child) => this.nodeToWhatsApp(child)) + .join(""); + return `*${content}*`; + } + + if (isEmphasisNode(node)) { + const content = getNodeChildren(node) + .map((child) => this.nodeToWhatsApp(child)) + .join(""); + return `_${content}_`; + } + + if (isDeleteNode(node)) { + const content = getNodeChildren(node) + .map((child) => this.nodeToWhatsApp(child)) + .join(""); + return `~${content}~`; + } + + if (isInlineCodeNode(node)) { + return `\`\`\`${node.value}\`\`\``; + } + + if (isCodeNode(node)) { + return `\`\`\`${node.value}\`\`\``; + } + + if (isLinkNode(node)) { + const linkText = getNodeChildren(node) + .map((child) => this.nodeToWhatsApp(child)) + .join(""); + // WhatsApp auto-links URLs, so just output the URL if text matches, otherwise text + URL + if (linkText === node.url || !linkText) { + return node.url; + } + return `${linkText}: ${node.url}`; + } + + if (isBlockquoteNode(node)) { + return getNodeChildren(node) + .map((child) => `> ${this.nodeToWhatsApp(child)}`) + .join("\n"); + } + + if (isListNode(node)) { + return getNodeChildren(node) + .map((item, i) => { + const prefix = node.ordered ? `${i + 1}.` : "•"; + const content = getNodeChildren(item) + .map((child) => this.nodeToWhatsApp(child)) + .join(""); + return `${prefix} ${content}`; + }) + .join("\n"); + } + + if (isListItemNode(node)) { + return getNodeChildren(node) + .map((child) => this.nodeToWhatsApp(child)) + .join(""); + } + + if (node.type === "break") { + return "\n"; + } + + if (node.type === "thematicBreak") { + return "---"; + } + + const children = getNodeChildren(node); + if (children.length > 0) { + return children.map((child) => this.nodeToWhatsApp(child)).join(""); + } + return getNodeValue(node); + } +} diff --git a/packages/adapter-whatsapp-web/src/types.ts b/packages/adapter-whatsapp-web/src/types.ts new file mode 100644 index 00000000..ab6e596d --- /dev/null +++ b/packages/adapter-whatsapp-web/src/types.ts @@ -0,0 +1,12 @@ +import type { Logger } from "chat"; + +export interface WhatsAppAdapterConfig { + logger: Logger; + userName?: string; + sessionPath?: string; + puppeteerOptions?: Record; +} + +export interface WhatsAppThreadId { + chatId: string; +} diff --git a/packages/adapter-whatsapp-web/tsconfig.json b/packages/adapter-whatsapp-web/tsconfig.json new file mode 100644 index 00000000..8768f5bd --- /dev/null +++ b/packages/adapter-whatsapp-web/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "strictNullChecks": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/adapter-whatsapp-web/tsup.config.ts b/packages/adapter-whatsapp-web/tsup.config.ts new file mode 100644 index 00000000..ac5075b2 --- /dev/null +++ b/packages/adapter-whatsapp-web/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + clean: true, + sourcemap: true, + external: ["whatsapp-web.js"], +}); diff --git a/packages/adapter-whatsapp-web/vitest.config.ts b/packages/adapter-whatsapp-web/vitest.config.ts new file mode 100644 index 00000000..5b01228b --- /dev/null +++ b/packages/adapter-whatsapp-web/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + coverage: { + provider: "v8", + reporter: ["text", "json-summary"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.test.ts"], + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25231fda..1673a267 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,7 +82,7 @@ importers: version: 16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) fumadocs-mdx: specifier: 14.0.4 - version: 14.0.4(fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@5.4.21(@types/node@24.10.13)(lightningcss@1.30.2)) + version: 14.0.4(fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) fumadocs-ui: specifier: 16.2.2 version: 16.2.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18) @@ -414,6 +414,40 @@ importers: specifier: ^2.1.8 version: 2.1.9(@types/node@22.19.3)(lightningcss@1.30.2) + packages/adapter-whatsapp-web: + dependencies: + '@chat-adapter/shared': + specifier: workspace:* + version: link:../adapter-shared + chat: + specifier: workspace:* + version: link:../chat + whatsapp-web.js: + specifier: ^1.26.0 + version: 1.34.6(typescript@5.9.3) + devDependencies: + '@chat-adapter/state-memory': + specifier: workspace:* + version: link:../state-memory + '@types/node': + specifier: ^22.10.2 + version: 22.19.3 + qrcode-terminal: + specifier: ^0.12.0 + version: 0.12.0 + tsup: + specifier: ^8.3.5 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.19.3)(lightningcss@1.30.2) + packages/chat: dependencies: '@workflow/serde': @@ -659,6 +693,10 @@ packages: resolution: {integrity: sha512-lvuAwsDpPDE/jSuVQOBMpLbXuVuLsPNRwWCyK3/6bPlBk0fGWegqoZ0qjZclMWyQ2JNvIY3vHY7hoFmFmFQcOw==} engines: {node: '>=16'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -1724,10 +1762,18 @@ packages: cpu: [x64] os: [win32] + '@pedroslopez/moduleraid@5.0.2': + resolution: {integrity: sha512-wtnBAETBVYZ9GvcbgdswRVSLkFkYAGv1KzwBBTeRXvGT9sb9cPllOgFFWXCn9PyARQ0H+Ijz6mmoRrGateUDxQ==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@puppeteer/browsers@2.13.0': + resolution: {integrity: sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==} + engines: {node: '>=18'} + hasBin: true + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2796,6 +2842,9 @@ packages: '@tailwindcss/postcss@4.1.18': resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -2954,6 +3003,9 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typespec/ts-http-runtime@0.3.2': resolution: {integrity: sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==} engines: {node: '>=20.0.0'} @@ -3118,6 +3170,18 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -3136,16 +3200,34 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + b4a@1.8.0: + resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -3156,6 +3238,44 @@ packages: resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} engines: {node: 20 || >=22} + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.5.4: + resolution: {integrity: sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.6.2: + resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.8.0: + resolution: {integrity: sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.3.2: + resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -3167,6 +3287,10 @@ packages: resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true + basic-ftp@5.2.0: + resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==} + engines: {node: '>=10.0.0'} + bcp-47-match@2.0.3: resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} @@ -3183,9 +3307,22 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + binary@0.3.0: + resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + botbuilder-core@4.23.3: resolution: {integrity: sha512-48iW739I24piBH683b/Unvlu1fSzjB69ViOwZ0PbTkN2yW5cTvHJWlW7bXntO8GSqJfssgPaVthKfyaCW457ig==} @@ -3207,6 +3344,9 @@ packages: botframework-streaming@4.23.3: resolution: {integrity: sha512-GMtciQGfZXtAW6syUqFpFJQ2vDyVbpxL3T1DqFzq/GmmkAu7KTZ1zvo7PTww6+IT1kMW0lmL/XZJVq3Rhg4PQA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -3218,12 +3358,26 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-indexof-polyfill@1.0.2: + resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} + engines: {node: '>=0.10'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + buffers@0.1.1: + resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} + engines: {node: '>=0.2.0'} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -3246,6 +3400,10 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + caniuse-lite@1.0.30001761: resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} @@ -3264,6 +3422,9 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chainsaw@0.1.0: + resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -3299,6 +3460,11 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chromium-bidi@14.0.0: + resolution: {integrity: sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==} + peerDependencies: + devtools-protocol: '*' + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -3312,6 +3478,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + cloudflare-video-element@1.3.5: resolution: {integrity: sha512-zj9gjJa6xW8MNrfc4oKuwgGS0njRLpOlQjdifbuNxvy8k4Y3pKCyKCMG2XIsjd2iQGhgjS57b1P5VWdJlxcXBw==} @@ -3365,9 +3535,16 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -3375,12 +3552,33 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + cross-fetch@4.1.0: resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} @@ -3561,6 +3759,10 @@ packages: dashjs@5.1.1: resolution: {integrity: sha512-BzNXlUgzEjhuZ5M5hlSp1qIyQHZ7NpXAR0loP9DAAFVZj/ntL1DHeZ7qp/L3bvI4rq50X5indkAZQ3zEHWJoCA==} + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + dayjs@1.11.19: resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} @@ -3596,6 +3798,10 @@ packages: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} @@ -3629,6 +3835,9 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + devtools-protocol@0.0.1566079: + resolution: {integrity: sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==} + dexie-react-hooks@4.2.0: resolution: {integrity: sha512-u7KqTX9JpBQK8+tEyA9X0yMGXlSCsbm5AU64N6gjvGk/IutYDpLBInMYEAEC83s3qhIvryFS+W+sqLZUBEvePQ==} peerDependencies: @@ -3681,6 +3890,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -3693,6 +3905,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} @@ -3709,6 +3924,13 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -3744,15 +3966,28 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-util-attach-comments@3.0.0: resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} @@ -3777,12 +4012,19 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -3797,12 +4039,20 @@ packages: extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + fast-content-type-parse@2.0.1: resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -3813,6 +4063,9 @@ packages: fd-package-json@2.0.0: resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -3845,6 +4098,11 @@ packages: fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + fluent-ffmpeg@2.1.3: + resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} + engines: {node: '>=18'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -3881,6 +4139,13 @@ packages: react-dom: optional: true + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fs-extra@11.3.3: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} @@ -3893,11 +4158,19 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fstream@1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + deprecated: This package is no longer supported. + fumadocs-core@16.2.2: resolution: {integrity: sha512-CMU/jp/Gb6lr/qvRrTMRv1FX2VuAixHaqop4yguCwKt/iqkgJP4MJ2SpXcFheSUraJ2hIgDyYVoXIK1onKqagw==} peerDependencies: @@ -3986,6 +4259,10 @@ packages: resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} engines: {node: '>= 4'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.4.0: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} @@ -4002,9 +4279,17 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -4021,6 +4306,10 @@ packages: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -4164,9 +4453,20 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + imsc@1.1.5: resolution: {integrity: sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -4181,12 +4481,19 @@ packages: resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} engines: {node: '>=12.22.0'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -4242,6 +4549,9 @@ packages: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -4307,6 +4617,9 @@ packages: json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -4354,6 +4667,10 @@ packages: layout-base@2.0.1: resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + lie@3.1.1: resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} @@ -4434,6 +4751,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + listenercount@1.0.1: + resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4454,6 +4774,12 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -4487,6 +4813,9 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -4507,6 +4836,10 @@ packages: resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + lucide-react@0.555.0: resolution: {integrity: sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==} peerDependencies: @@ -4756,10 +5089,22 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + minimatch@10.2.2: resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} engines: {node: 18 || 20 || >=22} + minimatch@3.1.4: + resolution: {integrity: sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==} + + minimatch@5.1.8: + resolution: {integrity: sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==} + engines: {node: '>=10'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -4771,6 +5116,13 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -4827,6 +5179,10 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -4884,6 +5240,13 @@ packages: encoding: optional: true + node-webpmux@3.1.7: + resolution: {integrity: sha512-ySkL4lBCto86OyQ0blAGzylWSECcn5I0lM3bYEhe75T8Zxt/BFUMHa8ktUguR7zwXNdS/Hms31VfSsYKN1383g==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + npm-to-yarn@3.0.1: resolution: {integrity: sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4901,6 +5264,9 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} @@ -4956,6 +5322,14 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -4965,9 +5339,17 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -4981,6 +5363,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -5010,6 +5396,9 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -5076,15 +5465,42 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + puppeteer-core@24.37.5: + resolution: {integrity: sha512-ybL7iE78YPN4T6J+sPLO7r0lSByp/0NN6PvfBEql219cOnttoTFzCWKiBOjstXSqi/OKpwae623DWAsL7cn2MQ==} + engines: {node: '>=18'} + + puppeteer@24.37.5: + resolution: {integrity: sha512-3PAOIQLceyEmn1Fi76GkGO2EVxztv5OtdlB1m8hMUZL3f8KDHnlvXbvCXv+Ls7KzF1R0KdKBqLuT/Hhrok12hQ==} + engines: {node: '>=18'} + hasBin: true + + qrcode-terminal@0.12.0: + resolution: {integrity: sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==} + hasBin: true + qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -5167,6 +5583,16 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -5262,6 +5688,14 @@ packages: remend@1.2.0: resolution: {integrity: sha512-NbKrdWweTRuByPYErzQCNpNtsR9M1QQ0hK2UzmnmlSaEqHnkQ5Korlyi8KpdbOJ0rImJfRy4EAY0uDxYnL9Plw==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -5277,6 +5711,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} @@ -5301,6 +5740,9 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -5325,6 +5767,14 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -5370,10 +5820,22 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + smol-toml@1.6.0: resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} engines: {node: '>= 18'} + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -5384,6 +5846,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + source-map@0.7.6: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} @@ -5414,6 +5880,9 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -5422,6 +5891,12 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -5490,6 +5965,19 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tar-fs@3.1.1: + resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -5498,6 +5986,9 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -5549,6 +6040,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + traverse@0.3.9: + resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -5636,6 +6130,9 @@ packages: twitch-video-element@0.1.6: resolution: {integrity: sha512-X7l8gy+DEFKJ/EztUwaVnAYwQN9fUJxPkOVJj2sE62sGvGU4DNLyvmOsmVulM+8Plc5dMg6hYIMNRAPaH+39Uw==} + typed-query-selector@2.12.0: + resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -5711,6 +6208,9 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unzipper@0.10.14: + resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} + url-template@2.0.8: resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} @@ -5873,12 +6373,23 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webdriver-bidi-protocol@0.4.1: + resolution: {integrity: sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + whatsapp-web.js@1.34.6: + resolution: {integrity: sha512-+zgLBqARcVfuCG7b80c7Gkt+4Yh8w+oDWx7lL2gTA6nlaykHBne7NwJ5yGe2r7O9IYraIzs6HiCzNGKfu9AUBg==} + engines: {node: '>=18.0.0'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -5900,6 +6411,9 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -5912,8 +6426,8 @@ packages: utf-8-validate: optional: true - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -5932,12 +6446,31 @@ packages: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + youtube-video-element@1.8.1: resolution: {integrity: sha512-+5UuAGaj+5AnBf39huLVpy/4dLtR0rmJP1TxOHVZ81bac4ZHFpTtQ4Dz2FAn2GPnfXISezvUEaQoAdFW4hH9Xg==} + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -6106,6 +6639,12 @@ snapshots: jsonwebtoken: 9.0.3 uuid: 8.3.2 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -6174,7 +6713,7 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.7.3 + semver: 7.7.4 '@changesets/assemble-release-plan@6.0.9': dependencies: @@ -6183,7 +6722,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 - semver: 7.7.3 + semver: 7.7.4 '@changesets/changelog-git@0.2.1': dependencies: @@ -6241,7 +6780,7 @@ snapshots: '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 - semver: 7.7.3 + semver: 7.7.4 '@changesets/get-release-plan@4.0.14': dependencies: @@ -6376,7 +6915,7 @@ snapshots: '@vladfrangu/async_event_emitter': 2.4.7 discord-api-types: 0.38.37 tslib: 2.8.1 - ws: 8.18.3 + ws: 8.19.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -7071,9 +7610,26 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.16.2': optional: true + '@pedroslopez/moduleraid@5.0.2': {} + '@pkgjs/parseargs@0.11.0': optional: true + '@puppeteer/browsers@2.13.0': + dependencies: + debug: 4.4.3 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.5.0 + semver: 7.7.4 + tar-fs: 3.1.1 + yargs: 17.7.2 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -8142,6 +8698,8 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.18 + '@tootallnate/quickjs-emscripten@0.23.0': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -8282,7 +8840,7 @@ snapshots: '@types/jsonwebtoken@9.0.6': dependencies: - '@types/node': 22.19.3 + '@types/node': 24.10.13 '@types/mdast@4.0.4': dependencies: @@ -8321,11 +8879,16 @@ snapshots: '@types/ws@6.0.4': dependencies: - '@types/node': 22.19.3 + '@types/node': 24.10.13 '@types/ws@8.18.1': dependencies: - '@types/node': 22.19.3 + '@types/node': 24.10.13 + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 24.10.13 + optional: true '@typespec/ts-http-runtime@0.3.2': dependencies: @@ -8381,6 +8944,14 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.3)(lightningcss@1.30.2))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.19.3)(lightningcss@1.30.2) + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@24.10.13)(lightningcss@1.30.2))': dependencies: '@vitest/spy': 2.1.9 @@ -8458,6 +9029,45 @@ snapshots: any-promise@1.3.0: {} + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + optional: true + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + optional: true + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + optional: true + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -8472,8 +9082,17 @@ snapshots: assertion-error@2.0.1: {} + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + astring@1.9.0: {} + async@0.2.10: {} + + async@3.2.6: + optional: true + asynckit@0.4.0: {} axios@1.13.2: @@ -8484,18 +9103,60 @@ snapshots: transitivePeerDependencies: - debug + b4a@1.8.0: {} + bail@2.0.2: {} balanced-match@1.0.2: {} balanced-match@4.0.3: {} + bare-events@2.8.2: {} + + bare-fs@4.5.4: + dependencies: + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.8.0(bare-events@2.8.2) + bare-url: 2.3.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-os@3.6.2: + optional: true + + bare-path@3.0.0: + dependencies: + bare-os: 3.6.2 + optional: true + + bare-stream@2.8.0(bare-events@2.8.2): + dependencies: + streamx: 2.23.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-url@2.3.2: + dependencies: + bare-path: 3.0.0 + optional: true + base64-js@1.5.1: {} base64url@3.0.1: {} baseline-browser-mapping@2.9.11: {} + basic-ftp@5.2.0: {} + bcp-47-match@2.0.3: {} bcp-47-normalize@2.3.0: @@ -8515,8 +9176,27 @@ snapshots: dependencies: is-windows: 1.0.2 + big-integer@1.6.52: + optional: true + bignumber.js@9.3.1: {} + binary@0.3.0: + dependencies: + buffers: 0.1.1 + chainsaw: 0.1.0 + optional: true + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + bluebird@3.4.7: + optional: true + botbuilder-core@4.23.3: dependencies: botbuilder-dialogs-adaptive-runtime-core: 4.23.3-preview @@ -8606,6 +9286,12 @@ snapshots: - bufferutil - utf-8-validate + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + optional: true + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -8618,13 +9304,27 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-crc32@0.2.13: {} + buffer-equal-constant-time@1.0.1: {} + buffer-indexof-polyfill@1.0.2: + optional: true + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + optional: true + buffer@6.0.3: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + buffers@0.1.1: + optional: true + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -8646,6 +9346,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + callsites@3.1.0: {} + caniuse-lite@1.0.30001761: {} castable-video@1.1.11: @@ -8666,6 +9368,11 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chainsaw@0.1.0: + dependencies: + traverse: 0.3.9 + optional: true + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -8700,6 +9407,12 @@ snapshots: dependencies: readdirp: 5.0.0 + chromium-bidi@14.0.0(devtools-protocol@0.0.1566079): + dependencies: + devtools-protocol: 0.0.1566079 + mitt: 3.0.1 + zod: 3.25.76 + ci-info@3.9.0: {} citty@0.2.1: {} @@ -8710,6 +9423,12 @@ snapshots: client-only@0.0.1: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cloudflare-video-element@1.3.5: {} clsx@2.1.1: {} @@ -8752,12 +9471,26 @@ snapshots: commander@8.3.0: {} + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + optional: true + compute-scroll-into-view@3.1.1: {} + concat-map@0.0.1: + optional: true + confbox@0.1.8: {} consola@3.4.2: {} + core-util-is@1.0.3: + optional: true + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -8766,6 +9499,24 @@ snapshots: dependencies: layout-base: 2.0.1 + cosmiconfig@9.0.0(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + crc-32@1.2.2: + optional: true + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + optional: true + cross-fetch@4.1.0: dependencies: node-fetch: 2.7.0 @@ -9001,6 +9752,8 @@ snapshots: - '@svta/cml-structured-field-values' - '@svta/cml-utils' + data-uri-to-buffer@6.0.2: {} + dayjs@1.11.19: {} debug@4.4.3: @@ -9024,6 +9777,12 @@ snapshots: define-lazy-prop@3.0.0: {} + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + delaunator@5.0.1: dependencies: robust-predicates: 3.0.2 @@ -9046,6 +9805,8 @@ snapshots: dependencies: dequal: 2.0.3 + devtools-protocol@0.0.1566079: {} + dexie-react-hooks@4.2.0(@types/react@19.2.7)(dexie@4.3.0)(react@19.2.3): dependencies: '@types/react': 19.2.7 @@ -9113,6 +9874,11 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + optional: true + eastasianwidth@0.2.0: {} ecdsa-sig-formatter@1.0.11: @@ -9123,6 +9889,10 @@ snapshots: emoji-regex@9.2.2: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 @@ -9137,6 +9907,12 @@ snapshots: entities@6.0.1: {} + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -9223,10 +9999,22 @@ snapshots: '@esbuild/win32-ia32': 0.27.2 '@esbuild/win32-x64': 0.27.2 + escalade@3.2.0: {} + escape-string-regexp@5.0.0: {} + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + esprima@4.0.1: {} + estraverse@5.3.0: {} + estree-util-attach-comments@3.0.0: dependencies: '@types/estree': 1.0.8 @@ -9264,10 +10052,18 @@ snapshots: dependencies: '@types/estree': 1.0.8 + esutils@2.0.3: {} + eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + eventsource-parser@3.0.6: {} expect-type@1.3.0: {} @@ -9276,10 +10072,22 @@ snapshots: extendable-error@0.1.7: {} + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + fast-content-type-parse@2.0.1: {} fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9296,6 +10104,10 @@ snapshots: dependencies: walk-up-path: 4.0.0 + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -9325,6 +10137,11 @@ snapshots: mlly: 1.8.0 rollup: 4.54.0 + fluent-ffmpeg@2.1.3: + dependencies: + async: 0.2.10 + which: 1.3.1 + follow-redirects@1.15.11: {} foreground-child@3.3.1: @@ -9353,6 +10170,16 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + fs-constants@1.0.0: + optional: true + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + optional: true + fs-extra@11.3.3: dependencies: graceful-fs: 4.2.11 @@ -9371,9 +10198,20 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fs.realpath@1.0.0: + optional: true + fsevents@2.3.3: optional: true + fstream@1.0.12: + dependencies: + graceful-fs: 4.2.11 + inherits: 2.0.4 + mkdirp: 0.5.6 + rimraf: 2.7.1 + optional: true + fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@formatjs/intl-localematcher': 0.6.2 @@ -9403,7 +10241,7 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@14.0.4(fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@5.4.21(@types/node@24.10.13)(lightningcss@1.30.2)): + fumadocs-mdx@14.0.4(fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 @@ -9427,7 +10265,6 @@ snapshots: optionalDependencies: next: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 - vite: 5.4.21(@types/node@24.10.13)(lightningcss@1.30.2) transitivePeerDependencies: - supports-color @@ -9492,6 +10329,8 @@ snapshots: generic-pool@3.9.0: {} + get-caller-file@2.0.5: {} + get-east-asian-width@1.4.0: {} get-intrinsic@1.3.0: @@ -9514,10 +10353,22 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 + get-uri@6.0.5: + dependencies: + basic-ftp: 5.2.0 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -9539,6 +10390,16 @@ snapshots: minipass: 7.1.3 path-scurry: 2.0.2 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.4 + once: 1.4.0 + path-is-absolute: 1.0.1 + optional: true + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -9783,10 +10644,24 @@ snapshots: immediate@3.0.6: {} + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + imsc@1.1.5: dependencies: sax: 1.2.1 + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + optional: true + + inherits@2.0.4: + optional: true + inline-style-parser@0.2.7: {} internmap@1.0.1: {} @@ -9807,6 +10682,8 @@ snapshots: transitivePeerDependencies: - supports-color + ip-address@10.1.0: {} + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -9814,6 +10691,8 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + is-arrayish@0.2.1: {} + is-decimal@2.0.1: {} is-docker@3.0.0: {} @@ -9850,6 +10729,9 @@ snapshots: dependencies: is-inside-container: 1.0.0 + isarray@1.0.0: + optional: true + isexe@2.0.0: {} isomorphic-unfetch@3.1.0: @@ -9910,6 +10792,8 @@ snapshots: dependencies: bignumber.js: 9.3.1 + json-parse-even-better-errors@2.3.1: {} + json-schema@0.4.0: {} jsonc-parser@3.3.1: {} @@ -9935,7 +10819,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.7.3 + semver: 7.7.4 jwa@2.0.1: dependencies: @@ -9983,6 +10867,11 @@ snapshots: layout-base@2.0.1: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + optional: true + lie@3.1.1: dependencies: immediate: 3.0.6 @@ -10040,6 +10929,9 @@ snapshots: lines-and-columns@1.2.4: {} + listenercount@1.0.1: + optional: true + load-tsconfig@0.2.5: {} localforage@1.10.0: @@ -10056,6 +10948,12 @@ snapshots: lodash.defaults@4.2.0: {} + lodash.difference@4.5.0: + optional: true + + lodash.flatten@4.4.0: + optional: true + lodash.includes@4.3.0: {} lodash.isarguments@3.1.0: {} @@ -10078,6 +10976,9 @@ snapshots: lodash.startcase@4.4.0: {} + lodash.union@4.6.0: + optional: true + lodash@4.17.21: {} longest-streak@3.1.0: {} @@ -10092,6 +10993,8 @@ snapshots: lru-cache@11.2.6: {} + lru-cache@7.18.3: {} + lucide-react@0.555.0(react@19.2.3): dependencies: react: 19.2.3 @@ -10110,7 +11013,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 markdown-extensions@2.0.0: {} @@ -10631,10 +11534,22 @@ snapshots: dependencies: mime-db: 1.52.0 + mime@3.0.0: {} + minimatch@10.2.2: dependencies: brace-expansion: 5.0.2 + minimatch@3.1.4: + dependencies: + brace-expansion: 1.1.12 + optional: true + + minimatch@5.1.8: + dependencies: + brace-expansion: 2.0.2 + optional: true + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -10643,6 +11558,13 @@ snapshots: minipass@7.1.3: {} + mitt@3.0.1: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + optional: true + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -10686,6 +11608,8 @@ snapshots: negotiator@1.0.0: {} + netmask@2.0.2: {} + next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 @@ -10744,6 +11668,11 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-webpmux@3.1.7: {} + + normalize-path@3.0.0: + optional: true + npm-to-yarn@3.0.1: {} nypm@0.6.5: @@ -10756,6 +11685,10 @@ snapshots: object-inspect@1.13.4: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + oniguruma-parser@0.12.1: {} oniguruma-to-es@4.3.4: @@ -10830,6 +11763,24 @@ snapshots: p-try@2.2.0: {} + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + package-json-from-dist@1.0.1: {} package-manager-detector@0.2.11: @@ -10838,6 +11789,10 @@ snapshots: package-manager-detector@1.6.0: {} + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -10848,6 +11803,13 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -10858,6 +11820,9 @@ snapshots: path-exists@4.0.0: {} + path-is-absolute@1.0.1: + optional: true + path-key@3.1.1: {} path-scurry@1.11.1: @@ -10880,6 +11845,8 @@ snapshots: pathval@2.0.1: {} + pend@1.2.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -10936,6 +11903,11 @@ snapshots: prettier@2.8.8: {} + process-nextick-args@2.0.1: + optional: true + + progress@2.0.3: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -10944,8 +11916,62 @@ snapshots: property-information@7.1.0: {} + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + proxy-from-env@1.1.0: {} + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + puppeteer-core@24.37.5: + dependencies: + '@puppeteer/browsers': 2.13.0 + chromium-bidi: 14.0.0(devtools-protocol@0.0.1566079) + debug: 4.4.3 + devtools-protocol: 0.0.1566079 + typed-query-selector: 2.12.0 + webdriver-bidi-protocol: 0.4.1 + ws: 8.19.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - react-native-b4a + - supports-color + - utf-8-validate + + puppeteer@24.37.5(typescript@5.9.3): + dependencies: + '@puppeteer/browsers': 2.13.0 + chromium-bidi: 14.0.0(devtools-protocol@0.0.1566079) + cosmiconfig: 9.0.0(typescript@5.9.3) + devtools-protocol: 0.0.1566079 + puppeteer-core: 24.37.5 + typed-query-selector: 2.12.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - react-native-b4a + - supports-color + - typescript + - utf-8-validate + + qrcode-terminal@0.12.0: {} + qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -11086,6 +12112,29 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + optional: true + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.8 + optional: true + readdirp@4.1.2: {} readdirp@5.0.0: {} @@ -11239,6 +12288,10 @@ snapshots: remend@1.2.0: {} + require-directory@2.1.1: {} + + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -11247,6 +12300,11 @@ snapshots: reusify@1.1.0: {} + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + optional: true + robust-predicates@3.0.2: {} rollup@4.54.0: @@ -11294,6 +12352,9 @@ snapshots: rw@1.3.3: {} + safe-buffer@5.1.2: + optional: true + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} @@ -11310,6 +12371,11 @@ snapshots: semver@7.7.3: {} + semver@7.7.4: {} + + setimmediate@1.0.5: + optional: true + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -11395,8 +12461,23 @@ snapshots: slash@3.0.0: {} + smart-buffer@4.2.0: {} + smol-toml@1.6.0: {} + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 @@ -11404,6 +12485,9 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.6.1: + optional: true + source-map@0.7.6: {} space-separated-tokens@2.0.2: {} @@ -11444,6 +12528,15 @@ snapshots: transitivePeerDependencies: - supports-color + streamx@2.23.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -11456,6 +12549,16 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + optional: true + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -11516,6 +12619,44 @@ snapshots: tapable@2.3.0: {} + tar-fs@3.1.1: + dependencies: + pump: 3.0.3 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.5.4 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + tar-stream@3.1.7: + dependencies: + b4a: 1.8.0 + fast-fifo: 1.3.2 + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + teex@1.0.1: + dependencies: + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + term-size@2.2.1: {} test-exclude@7.0.1: @@ -11524,6 +12665,12 @@ snapshots: glob: 10.5.0 minimatch: 9.0.5 + text-decoder@1.2.7: + dependencies: + b4a: 1.8.0 + transitivePeerDependencies: + - react-native-b4a + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -11561,6 +12708,9 @@ snapshots: tr46@0.0.3: {} + traverse@0.3.9: + optional: true + tree-kill@1.2.2: {} trim-lines@3.0.1: {} @@ -11641,6 +12791,8 @@ snapshots: twitch-video-element@0.1.6: {} + typed-query-selector@2.12.0: {} + typescript@5.9.3: {} ua-parser-js@1.0.41: {} @@ -11720,6 +12872,20 @@ snapshots: universalify@2.0.1: {} + unzipper@0.10.14: + dependencies: + big-integer: 1.6.52 + binary: 0.3.0 + bluebird: 3.4.7 + buffer-indexof-polyfill: 1.0.2 + duplexer2: 0.1.4 + fstream: 1.0.12 + graceful-fs: 4.2.11 + listenercount: 1.0.1 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + optional: true + url-template@2.0.8: {} use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.3): @@ -11842,7 +13008,7 @@ snapshots: vitest@2.1.9(@types/node@22.19.3)(lightningcss@1.30.2): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@24.10.13)(lightningcss@1.30.2)) + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.3)(lightningcss@1.30.2)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -11932,13 +13098,41 @@ snapshots: web-namespaces@2.0.1: {} + webdriver-bidi-protocol@0.4.1: {} + webidl-conversions@3.0.1: {} + whatsapp-web.js@1.34.6(typescript@5.9.3): + dependencies: + '@pedroslopez/moduleraid': 5.0.2 + fluent-ffmpeg: 2.1.3 + mime: 3.0.0 + node-fetch: 2.7.0 + node-webpmux: 3.1.7 + puppeteer: 24.37.5(typescript@5.9.3) + optionalDependencies: + archiver: 5.3.2 + fs-extra: 10.1.0 + unzipper: 0.10.14 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - encoding + - react-native-b4a + - supports-color + - typescript + - utf-8-validate + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -11964,9 +13158,11 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 + wrappy@1.0.2: {} + ws@7.5.10: {} - ws@8.18.3: {} + ws@8.19.0: {} wsl-utils@0.1.0: dependencies: @@ -11976,10 +13172,36 @@ snapshots: dependencies: sax: 1.4.4 + y18n@5.0.8: {} + yallist@4.0.0: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + youtube-video-element@1.8.1: {} + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + optional: true + zod@3.25.76: {} zod@4.3.3: {} diff --git a/turbo.json b/turbo.json index 98bf25f6..3b9ec85e 100644 --- a/turbo.json +++ b/turbo.json @@ -10,6 +10,7 @@ "GOOGLE_CHAT_CREDENTIALS", "GOOGLE_CHAT_PUBSUB_TOPIC", "GOOGLE_CHAT_IMPERSONATE_USER", + "WHATSAPP_SESSION_PATH", "BOT_USERNAME", "REDIS_URL" ], From af4b4677918566e291cafbe0363d9e3d361413cd Mon Sep 17 00:00:00 2001 From: Samuel Corsan <120322525+samuelcorsan@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:44:28 +0100 Subject: [PATCH 2/4] refactor: divide into multiple files & feat: attachment support/filtering methods --- packages/adapter-whatsapp-web/README.md | 19 ++ .../adapter-whatsapp-web/src/attachments.ts | 100 +++++++ packages/adapter-whatsapp-web/src/client.ts | 137 +++++++++ .../adapter-whatsapp-web/src/events.test.ts | 177 ++++++++++++ packages/adapter-whatsapp-web/src/events.ts | 175 ++++++++++++ .../adapter-whatsapp-web/src/index.test.ts | 17 ++ packages/adapter-whatsapp-web/src/index.ts | 261 ++++++++---------- packages/adapter-whatsapp-web/src/types.ts | 8 + packages/chat/src/emoji.ts | 5 +- 9 files changed, 751 insertions(+), 148 deletions(-) create mode 100644 packages/adapter-whatsapp-web/src/attachments.ts create mode 100644 packages/adapter-whatsapp-web/src/client.ts create mode 100644 packages/adapter-whatsapp-web/src/events.test.ts create mode 100644 packages/adapter-whatsapp-web/src/events.ts diff --git a/packages/adapter-whatsapp-web/README.md b/packages/adapter-whatsapp-web/README.md index 2453cf0f..f68852fc 100644 --- a/packages/adapter-whatsapp-web/README.md +++ b/packages/adapter-whatsapp-web/README.md @@ -39,6 +39,21 @@ bot.onNewMessage(/.*/, async (thread, message) => { }); ``` +### Filtering options + +Restrict which messages the bot processes: + +```typescript +const adapter = createWhatsAppAdapter({ + userName: "My Bot", + sessionPath: ".wwebjs_auth", + allowedNumbers: ["34689396755"], + blockedNumbers: ["34600000000"], + allowedGroups: ["123456789-1234567890@g.us"], + requireMentionInGroups: true, +}); +``` + ## Configuration | Option | Type | Default | Description | @@ -47,6 +62,10 @@ bot.onNewMessage(/.*/, async (thread, message) => { | `userName` | `string` | `"bot"` | Bot display name | | `sessionPath` | `string` | `".wwebjs_auth"` or `WHATSAPP_SESSION_PATH` | Path for session persistence | | `puppeteerOptions` | `object` | `{}` | Options passed to Puppeteer | +| `allowedNumbers` | `string[]` | — | If set, only process messages from these numbers (e.g. `"34689396755"` or `"34689396755@c.us"`). DMs and groups both use sender ID. | +| `blockedNumbers` | `string[]` | — | Never process messages from these numbers. Takes precedence over `allowedNumbers`. | +| `allowedGroups` | `string[]` | — | If set, only process messages from these group IDs (e.g. `"123456789-1234567890@g.us"`). DMs are unaffected. | +| `requireMentionInGroups` | `boolean` | `false` | In group chats, only process messages that @mention the bot. DMs are unaffected. | ## Thread ID Format diff --git a/packages/adapter-whatsapp-web/src/attachments.ts b/packages/adapter-whatsapp-web/src/attachments.ts new file mode 100644 index 00000000..45487cb5 --- /dev/null +++ b/packages/adapter-whatsapp-web/src/attachments.ts @@ -0,0 +1,100 @@ +/** + * Attachment handling utilities for WhatsApp adapter. + */ + +import type { Logger } from "chat"; +import type WAWebJS from "whatsapp-web.js"; + +export interface WhatsAppAttachment { + type: "image" | "video" | "audio" | "file"; + url: undefined; + name: string | undefined; + mimeType: string | undefined; + size: number | undefined; + fetchData: () => Promise; +} + +/** + * Infer attachment type and MIME type from WhatsApp message type. + */ +function inferAttachmentMetadata(messageType: string): { + attachmentType: "image" | "video" | "audio" | "file"; + mimeType: string | undefined; +} { + switch (messageType) { + case "image": + return { attachmentType: "image", mimeType: "image/jpeg" }; + case "sticker": + return { attachmentType: "image", mimeType: "image/webp" }; + case "video": + return { attachmentType: "video", mimeType: "video/mp4" }; + case "audio": + case "ptt": + return { attachmentType: "audio", mimeType: "audio/ogg" }; + case "document": + default: + return { attachmentType: "file", mimeType: undefined }; + } +} + +/** + * Create an attachment object from a WhatsApp message. + * Media is downloaded lazily when fetchData() is called. + */ +export function createAttachmentFromMessage( + message: WAWebJS.Message, + logger: Logger +): WhatsAppAttachment { + const messageType = message.type; + const { attachmentType, mimeType } = inferAttachmentMetadata(messageType); + + const isDocument = messageType === "document"; + const filename = isDocument && message.body ? message.body : undefined; + + let cachedMedia: { + data: Buffer; + mimetype: string; + filename?: string; + } | null = null; + + return { + type: attachmentType, + url: undefined, + name: filename, + mimeType, + size: undefined, + + fetchData: async () => { + if (cachedMedia) { + return Object.assign(cachedMedia.data, { + mimetype: cachedMedia.mimetype, + filename: cachedMedia.filename, + }); + } + + const media = await message.downloadMedia(); + if (!media) { + throw new Error("Media download failed - media may have been deleted"); + } + + const buffer = Buffer.from(media.data, "base64"); + cachedMedia = { + data: buffer, + mimetype: media.mimetype, + filename: media.filename ?? undefined, + }; + + logger.debug("WhatsApp: media downloaded", { + mimetype: media.mimetype, + filename: media.filename, + filesize: media.filesize, + bufferSize: buffer.length, + }); + + return Object.assign(buffer, { + mimetype: media.mimetype, + filename: media.filename ?? undefined, + }); + }, + }; +} diff --git a/packages/adapter-whatsapp-web/src/client.ts b/packages/adapter-whatsapp-web/src/client.ts new file mode 100644 index 00000000..c7fdf25a --- /dev/null +++ b/packages/adapter-whatsapp-web/src/client.ts @@ -0,0 +1,137 @@ +/** + * WhatsApp client initialization and lifecycle management. + */ + +import type { Logger } from "chat"; +import type WAWebJS from "whatsapp-web.js"; + +export interface ClientConfig { + sessionPath: string; + puppeteerOptions: Record; +} + +export interface ClientState { + client: WAWebJS.Client | null; + isReady: boolean; + qrCode: string | null; + botUserId: string | undefined; +} + +export interface ClientCallbacks { + onReady: (botUserId: string) => void; + onDisconnected: () => void; + onMessage: (message: WAWebJS.Message) => Promise; + onReaction: (reaction: WAWebJS.Reaction) => Promise; +} + +/** + * Initialize the WhatsApp Web client. + */ +export async function initializeClient( + config: ClientConfig, + logger: Logger +): Promise { + const wa = await import("whatsapp-web.js"); + const mod = wa as unknown as { + default?: typeof wa; + Client?: unknown; + LocalAuth?: new (opts?: { dataPath?: string }) => unknown; + }; + const { Client, LocalAuth } = mod.default ?? mod; + + const client = new (Client as new (opts: unknown) => WAWebJS.Client)({ + authStrategy: new ( + LocalAuth as new (opts?: { dataPath?: string }) => unknown + )({ + dataPath: config.sessionPath, + }), + puppeteer: { + headless: true, + args: ["--no-sandbox", "--disable-setuid-sandbox"], + ...config.puppeteerOptions, + }, + }); + + logger.info("WhatsApp client created"); + return client; +} + +/** + * Set up event listeners on the WhatsApp client. + */ +export function setupClientEventListeners( + client: WAWebJS.Client, + callbacks: ClientCallbacks, + logger: Logger +): { setQrCode: (qr: string | null) => void; setReady: (ready: boolean) => void } { + let qrCode: string | null = null; + let isReady = false; + + client.on("qr", (qr: string) => { + qrCode = qr; + logger.info( + "WhatsApp QR code received. Scan with your phone to authenticate." + ); + }); + + client.on("authenticated", () => { + logger.info("WhatsApp authenticated successfully"); + }); + + client.on("auth_failure", (msg: string) => { + logger.error("WhatsApp authentication failed", { error: msg }); + }); + + client.on("ready", () => { + isReady = true; + const info = client.info; + if (info) { + callbacks.onReady(info.wid._serialized); + } + logger.info("WhatsApp client ready"); + }); + + client.on("disconnected", (reason: string) => { + isReady = false; + callbacks.onDisconnected(); + logger.warn("WhatsApp disconnected", { reason }); + }); + + client.on("message_create", async (message: WAWebJS.Message) => { + if (message.fromMe) return; + await callbacks.onMessage(message); + }); + + client.on("message_reaction", async (reaction: WAWebJS.Reaction) => { + await callbacks.onReaction(reaction); + }); + + return { + setQrCode: (qr: string | null) => { + qrCode = qr; + }, + setReady: (ready: boolean) => { + isReady = ready; + }, + }; +} + +/** + * Dynamically import MessageMedia for sending files. + */ +export async function getMessageMediaClass(): Promise< + new ( + mimetype: string, + data: string, + filename?: string + ) => WAWebJS.MessageMedia +> { + const wa = await import("whatsapp-web.js"); + const { MessageMedia } = (wa as unknown as { default?: typeof wa }).default ?? + wa; + return MessageMedia as new ( + mimetype: string, + data: string, + filename?: string + ) => WAWebJS.MessageMedia; +} diff --git a/packages/adapter-whatsapp-web/src/events.test.ts b/packages/adapter-whatsapp-web/src/events.test.ts new file mode 100644 index 00000000..1eed2e94 --- /dev/null +++ b/packages/adapter-whatsapp-web/src/events.test.ts @@ -0,0 +1,177 @@ +import { describe, expect, it, vi } from "vitest"; +import { handleIncomingMessage } from "./events"; +import { WhatsAppFormatConverter } from "./markdown"; + +function createMockMessage(overrides: { + from?: string; + author?: string; + id?: { _serialized: string }; + body?: string; + hasMedia?: boolean; + type?: string; + fromMe?: boolean; + timestamp?: number; + getContact?: () => Promise<{ id: { _serialized: string }; pushname?: string; name?: string }>; + getMentions?: () => Promise<{ id: { _serialized: string } }[]>; + downloadMedia?: () => Promise; +} = {}) { + return { + from: "34689396755@c.us", + author: undefined, + id: { _serialized: "msg-123" }, + body: "hello", + hasMedia: false, + type: "chat", + fromMe: false, + timestamp: Date.now() / 1000, + getContact: vi.fn().mockResolvedValue({ + id: { + _serialized: + overrides?.author ?? overrides?.from ?? "34689396755@c.us", + }, + pushname: "Test", + name: "Test User", + }), + getMentions: vi.fn().mockResolvedValue([]), + downloadMedia: vi.fn(), + ...overrides, + }; +} + +function createMockContext(overrides: Partial<{ + allowedNumbers: Set; + blockedNumbers: Set; + allowedGroups: Set; + requireMentionInGroups: boolean; + botUserId: string; +}>) { + const processMessage = vi.fn(); + const ctx = { + chat: { processMessage }, + logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + formatConverter: new WhatsAppFormatConverter(), + botUserId: "bot@c.us", + allowedNumbers: new Set(), + blockedNumbers: new Set(), + allowedGroups: new Set(), + requireMentionInGroups: false, + encodeThreadId: (data: { chatId: string }) => `whatsapp:${data.chatId}`, + adapter: {}, + ...overrides, + }; + return { ...ctx, processMessage }; +} + +describe("handleIncomingMessage filtering", () => { + it("blocks messages from blockedNumbers", async () => { + const message = createMockMessage({ author: "34600000000@c.us", from: "34600000000@c.us" }); + const ctx = createMockContext({ + blockedNumbers: new Set(["34600000000@c.us"]), + }); + + await handleIncomingMessage(message as never, ctx as never); + + expect(ctx.chat.processMessage).not.toHaveBeenCalled(); + }); + + it("allows messages when allowedNumbers is empty", async () => { + const message = createMockMessage(); + const ctx = createMockContext(); + + await handleIncomingMessage(message as never, ctx as never); + + expect(ctx.chat.processMessage).toHaveBeenCalled(); + }); + + it("blocks messages from non-allowed numbers when allowedNumbers is set", async () => { + const message = createMockMessage({ author: "34600000000@c.us", from: "34600000000@c.us" }); + const ctx = createMockContext({ + allowedNumbers: new Set(["34689396755@c.us"]), + }); + + await handleIncomingMessage(message as never, ctx as never); + + expect(ctx.chat.processMessage).not.toHaveBeenCalled(); + }); + + it("allows messages from allowed numbers", async () => { + const message = createMockMessage({ author: "34689396755@c.us", from: "34689396755@c.us" }); + const ctx = createMockContext({ + allowedNumbers: new Set(["34689396755@c.us"]), + }); + + await handleIncomingMessage(message as never, ctx as never); + + expect(ctx.chat.processMessage).toHaveBeenCalled(); + }); + + it("blocks messages from non-allowed groups when allowedGroups is set", async () => { + const message = createMockMessage({ + from: "999999999-9999999999@g.us", + author: "34689396755@c.us", + }); + const ctx = createMockContext({ + allowedGroups: new Set(["123456789-1234567890@g.us"]), + }); + + await handleIncomingMessage(message as never, ctx as never); + + expect(ctx.chat.processMessage).not.toHaveBeenCalled(); + }); + + it("allows messages from allowed groups", async () => { + const message = createMockMessage({ + from: "123456789-1234567890@g.us", + author: "34689396755@c.us", + }); + const ctx = createMockContext({ + allowedGroups: new Set(["123456789-1234567890@g.us"]), + }); + + await handleIncomingMessage(message as never, ctx as never); + + expect(ctx.chat.processMessage).toHaveBeenCalled(); + }); + + it("blocks group messages without mention when requireMentionInGroups is true", async () => { + const message = createMockMessage({ + from: "123456789-1234567890@g.us", + author: "34689396755@c.us", + getMentions: vi.fn().mockResolvedValue([]), + }); + const ctx = createMockContext({ + requireMentionInGroups: true, + }); + + await handleIncomingMessage(message as never, ctx as never); + + expect(ctx.chat.processMessage).not.toHaveBeenCalled(); + }); + + it("allows group messages with mention when requireMentionInGroups is true", async () => { + const message = createMockMessage({ + from: "123456789-1234567890@g.us", + author: "34689396755@c.us", + getMentions: vi.fn().mockResolvedValue([{ id: { _serialized: "bot@c.us" } }]), + }); + const ctx = createMockContext({ + requireMentionInGroups: true, + }); + + await handleIncomingMessage(message as never, ctx as never); + + expect(ctx.chat.processMessage).toHaveBeenCalled(); + }); + + it("blockedNumbers takes precedence over allowedNumbers", async () => { + const message = createMockMessage({ author: "34689396755@c.us", from: "34689396755@c.us" }); + const ctx = createMockContext({ + allowedNumbers: new Set(["34689396755@c.us"]), + blockedNumbers: new Set(["34689396755@c.us"]), + }); + + await handleIncomingMessage(message as never, ctx as never); + + expect(ctx.chat.processMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/adapter-whatsapp-web/src/events.ts b/packages/adapter-whatsapp-web/src/events.ts new file mode 100644 index 00000000..77acf7d2 --- /dev/null +++ b/packages/adapter-whatsapp-web/src/events.ts @@ -0,0 +1,175 @@ +/** + * Event handling for WhatsApp adapter. + */ + +import type { Adapter, ChatInstance, Logger } from "chat"; +import { defaultEmojiResolver, Message } from "chat"; +import type WAWebJS from "whatsapp-web.js"; +import { createAttachmentFromMessage } from "./attachments"; +import type { WhatsAppFormatConverter } from "./markdown"; +import type { WhatsAppThreadId } from "./types"; + +export interface EventHandlerContext { + chat: ChatInstance | null; + logger: Logger; + formatConverter: WhatsAppFormatConverter; + botUserId: string | undefined; + allowedNumbers: Set; + blockedNumbers: Set; + allowedGroups: Set; + requireMentionInGroups: boolean; + encodeThreadId: (data: WhatsAppThreadId) => string; + adapter: Adapter; +} + +function isGroupChat(chatId: string): boolean { + return chatId.endsWith("@g.us"); +} + +/** + * Handle incoming WhatsApp message. + */ +export async function handleIncomingMessage( + message: WAWebJS.Message, + ctx: EventHandlerContext +): Promise { + if (!ctx.chat) { + ctx.logger.warn("Chat instance not initialized, ignoring message"); + return; + } + + const senderId = message.author || message.from; + const chatId = message.from; + const isGroup = isGroupChat(chatId); + + if (ctx.blockedNumbers.has(senderId)) { + ctx.logger.debug("Ignoring message from blocked number", { senderId }); + return; + } + + if (ctx.allowedNumbers.size > 0 && !ctx.allowedNumbers.has(senderId)) { + ctx.logger.debug("Ignoring message from non-allowed number", { senderId }); + return; + } + + if (isGroup && ctx.allowedGroups.size > 0 && !ctx.allowedGroups.has(chatId)) { + ctx.logger.debug("Ignoring message from non-allowed group", { chatId }); + return; + } + + const isMention = await checkIfMentioned(message, ctx.botUserId); + if (isGroup && ctx.requireMentionInGroups && !isMention) { + ctx.logger.debug("Ignoring group message without @mention", { + chatId, + messageId: message.id._serialized, + }); + return; + } + + const threadId = ctx.encodeThreadId({ chatId }); + + ctx.logger.info("WhatsApp: message received", { + messageId: message.id._serialized, + hasMedia: message.hasMedia, + type: message.type, + }); + + const contact = await message.getContact(); + const isMe = message.fromMe; + + const chatMessage = new Message({ + id: message.id._serialized, + threadId, + text: ctx.formatConverter.extractPlainText(message.body), + formatted: ctx.formatConverter.toAst(message.body), + raw: message, + author: { + userId: contact.id._serialized, + userName: + contact.pushname || contact.name || contact.id.user || "unknown", + fullName: + contact.name || contact.pushname || contact.id.user || "unknown", + isBot: false, + isMe, + }, + metadata: { + dateSent: new Date(message.timestamp * 1000), + edited: false, + }, + attachments: message.hasMedia + ? [createAttachmentFromMessage(message, ctx.logger)] + : [], + isMention, + }); + + ctx.logger.info("WhatsApp: passing to SDK handler", { + messageId: message.id._serialized, + }); + + ctx.chat.processMessage(ctx.adapter, threadId, chatMessage); +} + +/** + * Check if bot is mentioned in the message. + */ +async function checkIfMentioned( + message: WAWebJS.Message, + botUserId: string | undefined +): Promise { + if (!botUserId) return false; + const mentions = await message.getMentions(); + return mentions.some((m) => m.id._serialized === botUserId); +} + +/** + * Handle WhatsApp reaction event. + */ +export async function handleReaction( + reaction: WAWebJS.Reaction, + ctx: EventHandlerContext +): Promise { + if (!ctx.chat) return; + + const senderId = reaction.senderId; + const chatId = reaction.id.remote; + const isGroup = isGroupChat(chatId); + + if (ctx.blockedNumbers.has(senderId)) { + ctx.logger.debug("Ignoring reaction from blocked number", { senderId }); + return; + } + + if (ctx.allowedNumbers.size > 0 && !ctx.allowedNumbers.has(senderId)) { + ctx.logger.debug("Ignoring reaction from non-allowed number", { senderId }); + return; + } + + if (isGroup && ctx.allowedGroups.size > 0 && !ctx.allowedGroups.has(chatId)) { + ctx.logger.debug("Ignoring reaction from non-allowed group", { chatId }); + return; + } + + const threadId = ctx.encodeThreadId({ chatId }); + const rawEmoji = reaction.reaction; + const normalizedEmoji = defaultEmojiResolver.fromGChat(rawEmoji); + const added = !!reaction.reaction; + + const reactionEvent = { + adapter: ctx.adapter, + threadId, + messageId: reaction.msgId._serialized, + emoji: normalizedEmoji, + rawEmoji, + added, + user: { + userId: reaction.senderId, + userName: reaction.senderId, + fullName: reaction.senderId, + isBot: false, + isMe: reaction.senderId === ctx.botUserId, + }, + raw: reaction, + }; + + ctx.chat.processReaction(reactionEvent); +} diff --git a/packages/adapter-whatsapp-web/src/index.test.ts b/packages/adapter-whatsapp-web/src/index.test.ts index 09b0ef86..fdb5ae15 100644 --- a/packages/adapter-whatsapp-web/src/index.test.ts +++ b/packages/adapter-whatsapp-web/src/index.test.ts @@ -97,4 +97,21 @@ describe("WhatsAppAdapter", () => { expect(adapter.isDM("whatsapp:1234567890-1234567890@g.us")).toBe(false); }); }); + + describe("filtering config", () => { + it("should accept blockedNumbers, allowedGroups, requireMentionInGroups", async () => { + const { createWhatsAppAdapter } = await import("./index"); + const { ConsoleLogger } = await import("chat"); + + const adapter = createWhatsAppAdapter({ + logger: new ConsoleLogger("silent"), + blockedNumbers: ["34600000000"], + allowedGroups: ["123456789-1234567890"], + requireMentionInGroups: true, + }); + + expect(adapter).toBeDefined(); + expect(adapter.name).toBe("whatsapp"); + }); + }); }); diff --git a/packages/adapter-whatsapp-web/src/index.ts b/packages/adapter-whatsapp-web/src/index.ts index a93ae485..9d73c4cc 100644 --- a/packages/adapter-whatsapp-web/src/index.ts +++ b/packages/adapter-whatsapp-web/src/index.ts @@ -38,6 +38,8 @@ import { Message, } from "chat"; import type WAWebJS from "whatsapp-web.js"; +import { getMessageMediaClass, initializeClient } from "./client"; +import { handleIncomingMessage, handleReaction } from "./events"; import { WhatsAppFormatConverter } from "./markdown"; import type { WhatsAppAdapterConfig, WhatsAppThreadId } from "./types"; @@ -52,6 +54,10 @@ export class WhatsAppAdapter implements Adapter { private readonly formatConverter = new WhatsAppFormatConverter(); private readonly sessionPath: string; private readonly puppeteerOptions: Record; + private readonly allowedNumbers: Set; + private readonly blockedNumbers: Set; + private readonly allowedGroups: Set; + private readonly requireMentionInGroups: boolean; private isReady = false; private qrCode: string | null = null; @@ -60,28 +66,34 @@ export class WhatsAppAdapter implements Adapter { this.userName = config.userName ?? "bot"; this.sessionPath = config.sessionPath ?? ".wwebjs_auth"; this.puppeteerOptions = config.puppeteerOptions ?? {}; + this.allowedNumbers = new Set( + (config.allowedNumbers ?? []).map((n) => + n.includes("@") ? n : `${n.replace(/\D/g, "")}@c.us` + ) + ); + this.blockedNumbers = new Set( + (config.blockedNumbers ?? []).map((n) => + n.includes("@") ? n : `${n.replace(/\D/g, "")}@c.us` + ) + ); + this.allowedGroups = new Set( + (config.allowedGroups ?? []).map((g) => + g.includes("@") ? g : `${g}@g.us` + ) + ); + this.requireMentionInGroups = config.requireMentionInGroups ?? false; } async initialize(chat: ChatInstance): Promise { this.chat = chat; - - const wa = await import("whatsapp-web.js"); - const mod = wa as unknown as { default?: typeof wa; Client?: unknown; LocalAuth?: new (opts?: { dataPath?: string }) => unknown }; - const { Client, LocalAuth } = mod.default ?? mod; - - this.client = new (Client as new (opts: unknown) => WAWebJS.Client)({ - authStrategy: new (LocalAuth as new (opts?: { dataPath?: string }) => unknown)({ - dataPath: this.sessionPath, - }), - puppeteer: { - headless: true, - args: ["--no-sandbox", "--disable-setuid-sandbox"], - ...this.puppeteerOptions, + this.client = await initializeClient( + { + sessionPath: this.sessionPath, + puppeteerOptions: this.puppeteerOptions, }, - }); - + this.logger + ); this.setupEventListeners(); - this.logger.info("WhatsApp adapter initializing..."); } @@ -109,9 +121,7 @@ export class WhatsAppAdapter implements Adapter { if (info) { (this as { botUserId: string }).botUserId = info.wid._serialized; } - this.logger.info("WhatsApp client ready", { - botUserId: this.botUserId, - }); + this.logger.info("WhatsApp client ready", { botUserId: this.botUserId }); }); this.client.on("disconnected", (reason: string) => { @@ -120,18 +130,28 @@ export class WhatsAppAdapter implements Adapter { }); this.client.on("message_create", async (message: WAWebJS.Message) => { - if (message.fromMe) { - return; - } - await this.handleIncomingMessage(message); + if (message.fromMe) return; + await handleIncomingMessage(message, this.getEventContext()); }); - this.client.on( - "message_reaction", - async (reaction: WAWebJS.Reaction) => { - await this.handleReaction(reaction); - } - ); + this.client.on("message_reaction", async (reaction: WAWebJS.Reaction) => { + await handleReaction(reaction, this.getEventContext()); + }); + } + + private getEventContext() { + return { + chat: this.chat, + logger: this.logger, + formatConverter: this.formatConverter, + botUserId: this.botUserId, + allowedNumbers: this.allowedNumbers, + blockedNumbers: this.blockedNumbers, + allowedGroups: this.allowedGroups, + requireMentionInGroups: this.requireMentionInGroups, + encodeThreadId: this.encodeThreadId.bind(this), + adapter: this as Adapter, + }; } async start(): Promise { @@ -175,99 +195,6 @@ export class WhatsAppAdapter implements Adapter { ); } - private async handleIncomingMessage(message: WAWebJS.Message): Promise { - if (!this.chat) { - this.logger.warn("Chat instance not initialized, ignoring message"); - return; - } - - const chatId = message.from; - const threadId = this.encodeThreadId({ chatId }); - - const contact = await message.getContact(); - const isMe = message.fromMe; - - const isMention = await this.checkIfMentioned(message); - - const chatMessage = new Message({ - id: message.id._serialized, - threadId, - text: this.formatConverter.extractPlainText(message.body), - formatted: this.formatConverter.toAst(message.body), - raw: message, - author: { - userId: contact.id._serialized, - userName: contact.pushname || contact.name || contact.id.user || "unknown", - fullName: contact.name || contact.pushname || contact.id.user || "unknown", - isBot: false, - isMe, - }, - metadata: { - dateSent: new Date(message.timestamp * 1000), - edited: false, - }, - attachments: message.hasMedia - ? [ - { - type: "file" as const, - url: undefined, - name: undefined, - mimeType: undefined, - fetchData: async () => { - const media = await message.downloadMedia(); - return Buffer.from(media.data, "base64"); - }, - }, - ] - : [], - isMention, - }); - - try { - await this.chat.handleIncomingMessage(this, threadId, chatMessage); - } catch (error) { - this.logger.error("Error handling incoming message", { - error: String(error), - messageId: message.id._serialized, - }); - } - } - - private async checkIfMentioned(message: WAWebJS.Message): Promise { - if (!this.botUserId) return false; - const mentions = await message.getMentions(); - return mentions.some((m) => m.id._serialized === this.botUserId); - } - - private async handleReaction(reaction: WAWebJS.Reaction): Promise { - if (!this.chat) return; - - const threadId = this.encodeThreadId({ chatId: reaction.id.remote }); - const rawEmoji = reaction.reaction; - const normalizedEmoji = defaultEmojiResolver.fromGChat(rawEmoji); - - const added = !!reaction.reaction; - - const reactionEvent = { - adapter: this as Adapter, - threadId, - messageId: reaction.msgId._serialized, - emoji: normalizedEmoji, - rawEmoji, - added, - user: { - userId: reaction.senderId, - userName: reaction.senderId, - fullName: reaction.senderId, - isBot: false, - isMe: reaction.senderId === this.botUserId, - }, - raw: reaction, - }; - - this.chat.processReaction(reactionEvent); - } - async postMessage( threadId: string, message: AdapterPostableMessage @@ -290,7 +217,7 @@ export class WhatsAppAdapter implements Adapter { } else { content = convertEmojiPlaceholders( this.formatConverter.renderPostable(message), - "whatsapp" + "whatsapp" //need to add platform ); } @@ -305,8 +232,8 @@ export class WhatsAppAdapter implements Adapter { let result: WAWebJS.Message; if (files.length > 0) { - const wa = await import("whatsapp-web.js"); - const { MessageMedia } = (wa as unknown as { default?: typeof wa }).default ?? wa; + const MessageMedia = await getMessageMediaClass(); + let lastSent: WAWebJS.Message | undefined; for (const file of files) { const buffer = await toBuffer(file.data, { platform: "whatsapp" }); if (!buffer) continue; @@ -315,22 +242,23 @@ export class WhatsAppAdapter implements Adapter { buffer.toString("base64"), file.filename ); - result = await this.client.sendMessage(chatId, media, { + lastSent = await this.client.sendMessage(chatId, media, { caption: content, }); } + result = lastSent ?? (await this.client.sendMessage(chatId, content)); } else { result = await this.client.sendMessage(chatId, content); } this.logger.debug("WhatsApp: message sent", { - messageId: result!.id._serialized, + messageId: result.id._serialized, }); return { - id: result!.id._serialized, + id: result.id._serialized, threadId, - raw: result!, + raw: result, }; } @@ -345,6 +273,47 @@ export class WhatsAppAdapter implements Adapter { ); } + /** + * Reply directly to a specific message (quotes the original). + * This uses WhatsApp's native reply feature. + */ + async replyToMessage( + threadId: string, + messageId: string, + content: string | AdapterPostableMessage + ): Promise> { + if (!this.client || !this.isReady) { + throw new NetworkError("whatsapp", "WhatsApp client not connected"); + } + + const { chatId } = this.decodeThreadId(threadId); + const chat = await this.client.getChatById(chatId); + const messages = await chat.fetchMessages({ limit: 50 }); + const targetMessage = messages.find((m) => m.id._serialized === messageId); + + if (!targetMessage) { + throw new ValidationError("whatsapp", `Message not found: ${messageId}`); + } + + const text = + typeof content === "string" + ? content + : this.formatConverter.renderPostable(content); + + const result = await targetMessage.reply(text); + + this.logger.debug("WhatsApp: replied to message", { + messageId, + replyId: result.id._serialized, + }); + + return { + id: result.id._serialized, + threadId, + raw: result, + }; + } + async deleteMessage(threadId: string, messageId: string): Promise { if (!this.client || !this.isReady) { throw new NetworkError("whatsapp", "WhatsApp client not connected"); @@ -359,9 +328,10 @@ export class WhatsAppAdapter implements Adapter { await targetMessage.delete(true); this.logger.debug("WhatsApp: message deleted", { messageId }); } else { - this.logger.warn("WhatsApp: cannot delete message (not found or not ours)", { - messageId, - }); + this.logger.warn( + "WhatsApp: cannot delete message (not found or not ours)", + { messageId } + ); } } @@ -383,7 +353,10 @@ export class WhatsAppAdapter implements Adapter { const emojiStr = typeof emoji === "string" ? emoji : defaultEmojiResolver.toGChat(emoji); await targetMessage.react(emojiStr); - this.logger.debug("WhatsApp: reaction added", { messageId, emoji: emojiStr }); + this.logger.debug("WhatsApp: reaction added", { + messageId, + emoji: emojiStr, + }); } } @@ -408,9 +381,7 @@ export class WhatsAppAdapter implements Adapter { } async startTyping(threadId: string, _status?: string): Promise { - if (!this.client || !this.isReady) { - return; - } + if (!this.client || !this.isReady) return; const { chatId } = this.decodeThreadId(threadId); const chat = await this.client.getChatById(chatId); @@ -458,10 +429,7 @@ export class WhatsAppAdapter implements Adapter { }) ); - return { - messages, - nextCursor: undefined, - }; + return { messages, nextCursor: undefined }; } async fetchThread(threadId: string): Promise { @@ -478,10 +446,7 @@ export class WhatsAppAdapter implements Adapter { channelId: chatId, channelName: chat.name, isDM: !isGroup, - metadata: { - isGroup, - raw: chat, - }, + metadata: { isGroup, raw: chat }, }; } @@ -507,9 +472,7 @@ export class WhatsAppAdapter implements Adapter { `Invalid WhatsApp thread ID: ${threadId}` ); } - return { - chatId: parts.slice(1).join(":"), - }; + return { chatId: parts.slice(1).join(":") }; } parseMessage(raw: unknown): Message { @@ -564,6 +527,10 @@ export function createWhatsAppAdapter( userName: config?.userName, sessionPath: config?.sessionPath ?? process.env.WHATSAPP_SESSION_PATH, puppeteerOptions: config?.puppeteerOptions, + allowedNumbers: config?.allowedNumbers, + blockedNumbers: config?.blockedNumbers, + allowedGroups: config?.allowedGroups, + requireMentionInGroups: config?.requireMentionInGroups, }; return new WhatsAppAdapter(resolved); } diff --git a/packages/adapter-whatsapp-web/src/types.ts b/packages/adapter-whatsapp-web/src/types.ts index ab6e596d..9e4602d4 100644 --- a/packages/adapter-whatsapp-web/src/types.ts +++ b/packages/adapter-whatsapp-web/src/types.ts @@ -5,6 +5,14 @@ export interface WhatsAppAdapterConfig { userName?: string; sessionPath?: string; puppeteerOptions?: Record; + /** If set, only process messages from these numbers (e.g. "34689396755" or "34689396755@c.us") */ + allowedNumbers?: string[]; + /** If set, never process messages from these numbers */ + blockedNumbers?: string[]; + /** If set, only process messages from these group IDs (e.g. "123456789-1234567890@g.us"). DMs are unaffected. */ + allowedGroups?: string[]; + /** In group chats, only process messages that @mention the bot. DMs are unaffected. */ + requireMentionInGroups?: boolean; } export interface WhatsAppThreadId { diff --git a/packages/chat/src/emoji.ts b/packages/chat/src/emoji.ts index 2b32f804..e5389862 100644 --- a/packages/chat/src/emoji.ts +++ b/packages/chat/src/emoji.ts @@ -335,7 +335,7 @@ const EMOJI_PLACEHOLDER_REGEX = /\{\{emoji:([a-z0-9_]+)\}\}/gi; */ export function convertEmojiPlaceholders( text: string, - platform: "slack" | "gchat" | "teams" | "discord" | "github" | "linear", + platform: "slack" | "gchat" | "teams" | "discord" | "github" | "linear" | "whatsapp", resolver: EmojiResolver = defaultEmojiResolver ): string { return text.replace(EMOJI_PLACEHOLDER_REGEX, (_, emojiName: string) => { @@ -356,6 +356,9 @@ export function convertEmojiPlaceholders( case "linear": // Linear uses unicode emoji return resolver.toGChat(emojiName); + case "whatsapp": + // WhatsApp uses unicode emoji + return resolver.toGChat(emojiName); default: return resolver.toGChat(emojiName); } From b57f59f091199ccee0b6e13f484762010cbed5ae Mon Sep 17 00:00:00 2001 From: Samuel Corsan <120322525+samuelcorsan@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:50:57 +0100 Subject: [PATCH 3/4] feat: changeset --- .changeset/tiny-memes-happen.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/tiny-memes-happen.md diff --git a/.changeset/tiny-memes-happen.md b/.changeset/tiny-memes-happen.md new file mode 100644 index 00000000..6b2be6ef --- /dev/null +++ b/.changeset/tiny-memes-happen.md @@ -0,0 +1,7 @@ +--- +"@chat-adapter/whatsapp-web": minor +"@chat-adapter/shared": minor +"chat": minor +--- + +Add WhatsApp Web adapter (@chat-adapter/whatsapp-web) From dfbfcebc1ea1c10558069f1a1d96d8c5522d1e30 Mon Sep 17 00:00:00 2001 From: Samuel Corsan <120322525+samuelcorsan@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:47:27 +0100 Subject: [PATCH 4/4] feat: add WhatsApp adapter docs --- apps/docs/content/docs/adapters/index.mdx | 69 ++--- apps/docs/content/docs/adapters/meta.json | 1 + .../content/docs/adapters/whatsapp-web.mdx | 265 ++++++++++++++++++ 3 files changed, 301 insertions(+), 34 deletions(-) create mode 100644 apps/docs/content/docs/adapters/whatsapp-web.mdx diff --git a/apps/docs/content/docs/adapters/index.mdx b/apps/docs/content/docs/adapters/index.mdx index 9691b7f0..db1d5dc6 100644 --- a/apps/docs/content/docs/adapters/index.mdx +++ b/apps/docs/content/docs/adapters/index.mdx @@ -1,6 +1,6 @@ --- title: Overview -description: Platform-specific adapters for Slack, Teams, Google Chat, Discord, GitHub, and Linear. +description: Platform-specific adapters for Slack, Teams, Google Chat, Discord, WhatsApp, GitHub, and Linear. type: overview prerequisites: - /docs/getting-started @@ -12,48 +12,48 @@ Adapters handle webhook verification, message parsing, and API calls for each pl ### Messaging -| Feature | [Slack](/docs/adapters/slack) | [Teams](/docs/adapters/teams) | [Google Chat](/docs/adapters/gchat) | [Discord](/docs/adapters/discord) | [GitHub](/docs/adapters/github) | [Linear](/docs/adapters/linear) | -|---------|-------|-------|-------------|---------|--------|--------| -| Post message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Edit message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Delete message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| File uploads | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | -| Streaming | ✅ Native | ⚠️ Post+Edit | ⚠️ Post+Edit | ⚠️ Post+Edit | ❌ | ❌ | +| Feature | [Slack](/docs/adapters/slack) | [Teams](/docs/adapters/teams) | [Google Chat](/docs/adapters/gchat) | [Discord](/docs/adapters/discord) | [WhatsApp](/docs/adapters/whatsapp-web) | [GitHub](/docs/adapters/github) | [Linear](/docs/adapters/linear) | +|---------|-------|-------|-------------|---------|----------|--------|--------| +| Post message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Edit message | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| Delete message | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| File uploads | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | +| Streaming | ✅ Native | ⚠️ Post+Edit | ⚠️ Post+Edit | ⚠️ Post+Edit | ❌ | ❌ | ❌ | ### Rich content -| Feature | Slack | Teams | Google Chat | Discord | GitHub | Linear | -|---------|-------|-------|-------------|---------|--------|--------| -| Card format | Block Kit | Adaptive Cards | Google Chat Cards | Embeds | GFM Markdown | Markdown | -| Buttons | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | -| Link buttons | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | -| Select menus | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | -| Fields | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Images in cards | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | -| Modals | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Feature | Slack | Teams | Google Chat | Discord | WhatsApp | GitHub | Linear | +|---------|-------|-------|-------------|---------|----------|--------|--------| +| Card format | Block Kit | Adaptive Cards | Google Chat Cards | Embeds | ❌ | GFM Markdown | Markdown | +| Buttons | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | +| Link buttons | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | +| Select menus | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| Fields | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| Images in cards | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | +| Modals | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ### Conversations -| Feature | Slack | Teams | Google Chat | Discord | GitHub | Linear | -|---------|-------|-------|-------------|---------|--------|--------| -| Mentions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Add reactions | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | -| Remove reactions | ✅ | ❌ | ✅ | ✅ | ⚠️ | ⚠️ | -| Typing indicator | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | -| DMs | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | -| Ephemeral messages | ✅ Native | ❌ | ✅ Native | ❌ | ❌ | ❌ | +| Feature | Slack | Teams | Google Chat | Discord | WhatsApp | GitHub | Linear | +|---------|-------|-------|-------------|---------|----------|--------|--------| +| Mentions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Add reactions | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Remove reactions | ✅ | ❌ | ✅ | ✅ | ❌ | ⚠️ | ⚠️ | +| Typing indicator | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | +| DMs | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| Ephemeral messages | ✅ Native | ❌ | ✅ Native | ❌ | ❌ | ❌ | ❌ | ### Message history -| Feature | Slack | Teams | Google Chat | Discord | GitHub | Linear | -|---------|-------|-------|-------------|---------|--------|--------| -| Fetch messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Fetch single message | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| Fetch thread info | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Fetch channel messages | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | -| List threads | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | -| Fetch channel info | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | -| Post channel message | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| Feature | Slack | Teams | Google Chat | Discord | WhatsApp | GitHub | Linear | +|---------|-------|-------|-------------|---------|----------|--------|--------| +| Fetch messages | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| Fetch single message | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Fetch thread info | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| Fetch channel messages | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | +| List threads | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | +| Fetch channel info | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | +| Post channel message | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ⚠️ indicates partial support — the feature works with limitations. See individual adapter pages for details. @@ -67,6 +67,7 @@ Adapters handle webhook verification, message parsing, and API calls for each pl | [Microsoft Teams](/docs/adapters/teams) | `@chat-adapter/teams` | | [Google Chat](/docs/adapters/gchat) | `@chat-adapter/gchat` | | [Discord](/docs/adapters/discord) | `@chat-adapter/discord` | +| [WhatsApp Web](/docs/adapters/whatsapp-web) | `@chat-adapter/whatsapp-web` | | [GitHub](/docs/adapters/github) | `@chat-adapter/github` | | [Linear](/docs/adapters/linear) | `@chat-adapter/linear` | diff --git a/apps/docs/content/docs/adapters/meta.json b/apps/docs/content/docs/adapters/meta.json index 506e7742..59d88e07 100644 --- a/apps/docs/content/docs/adapters/meta.json +++ b/apps/docs/content/docs/adapters/meta.json @@ -6,6 +6,7 @@ "teams", "gchat", "discord", + "whatsapp-web", "github", "linear" ] diff --git a/apps/docs/content/docs/adapters/whatsapp-web.mdx b/apps/docs/content/docs/adapters/whatsapp-web.mdx new file mode 100644 index 00000000..62fb9f69 --- /dev/null +++ b/apps/docs/content/docs/adapters/whatsapp-web.mdx @@ -0,0 +1,265 @@ +--- +title: WhatsApp Web +description: Configure the WhatsApp Web adapter using whatsapp-web.js with QR code authentication. +type: integration +prerequisites: + - /docs/getting-started +--- + +## Installation + +```sh title="Terminal" +pnpm add @chat-adapter/whatsapp-web +``` + +## Usage + +The adapter requires a persistent process for QR code authentication and maintaining the WebSocket connection: + +```typescript title="lib/bot.ts" lineNumbers +import { Chat } from "chat"; +import { createWhatsAppAdapter } from "@chat-adapter/whatsapp-web"; + +const bot = new Chat({ + userName: "mybot", + adapters: { + whatsapp: createWhatsAppAdapter({ + sessionDataPath: "./whatsapp-session", + }), + }, +}); + +bot.onNewMention(async (thread, message) => { + await thread.post("Hello from WhatsApp!"); +}); + +// Initialize to start QR code auth and connect +await bot.adapters.whatsapp.initialize(); +``` + +## WhatsApp setup + +### 1. First run (QR code auth) + +On first run, the adapter generates a QR code in the terminal: + +``` +[WhatsApp] QR Code (scan in WhatsApp): +▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +█ ▄▄▄▄▄ █... +``` + +1. Open WhatsApp on your phone +2. Go to **Linked Devices** (Settings → Linked Devices) +3. Tap **Link a Device** +4. Scan the QR code displayed in the terminal + +### 2. Session persistence + +The adapter saves session data to the path specified in `sessionDataPath`. On subsequent runs, it reconnects automatically without requiring QR code scan. + +```typescript title="lib/bot.ts" +createWhatsAppAdapter({ + sessionDataPath: "./whatsapp-session", // Session stored here +}); +``` + + +Keep the session directory secure. Anyone with access to these files can impersonate your WhatsApp account. + + +## Architecture + +Unlike other adapters that use webhooks, the WhatsApp Web adapter: + +- Maintains a persistent WebSocket connection via Puppeteer +- Uses `whatsapp-web.js` library under the hood +- Requires a long-running process (not suitable for serverless) +- Supports both direct messages and group chats + +## Message filtering + +The adapter supports filtering to control which messages are processed: + +| Option | Description | +|--------|-------------| +| `blockedNumbers` | Array of phone numbers to ignore (e.g., `["1234567890"]`) | +| `allowedGroups` | Array of group IDs to accept messages from (ignores other groups) | +| `requireMentionInGroups` | If `true`, only process group messages where the bot is @mentioned | + +### Filtering examples + +Block specific numbers: + +```typescript title="lib/bot.ts" +createWhatsAppAdapter({ + blockedNumbers: ["1234567890", "0987654321"], +}); +``` + +Only respond in specific groups: + +```typescript title="lib/bot.ts" +createWhatsAppAdapter({ + allowedGroups: ["120363123456789012@g.us"], +}); +``` + +Require @mention in groups (respond to all DMs, but only mentioned messages in groups): + +```typescript title="lib/bot.ts" +createWhatsAppAdapter({ + requireMentionInGroups: true, +}); +``` + +Combine filters: + +```typescript title="lib/bot.ts" +createWhatsAppAdapter({ + blockedNumbers: ["1234567890"], + allowedGroups: ["120363123456789012@g.us"], + requireMentionInGroups: true, +}); +``` + +## Media handling + +### Receiving media + +Incoming media (images, videos, audio, documents) is available via `message.attachments`: + +```typescript title="lib/bot.ts" +bot.onNewMessage(async (thread, message) => { + for (const attachment of message.attachments) { + console.log(`Received: ${attachment.name} (${attachment.mimeType})`); + + // Download the media data + const buffer = await attachment.fetchData(); + // Process buffer... + } +}); +``` + +Media is lazy-loaded — `fetchData()` downloads only when called. + +### Sending media + +Use `thread.post()` with the `files` option: + +```typescript title="lib/bot.ts" +import { FileUpload } from "chat"; + +bot.onNewMessage(async (thread, message) => { + if (message.text === "!image") { + await thread.post({ + markdown: "Here's an image:", + files: [ + FileUpload.fromBuffer(imageBuffer, "photo.jpg", "image/jpeg"), + ], + }); + } +}); +``` + +## Reply to messages + +Use `replyToMessage()` for quoted replies: + +```typescript title="lib/bot.ts" +const adapter = bot.adapters.whatsapp; + +bot.onNewMessage(async (thread, message) => { + // Reply directly to the incoming message (quoted reply) + await adapter.replyToMessage(thread.id, message.id, "Got your message!"); +}); +``` + +## Configuration + +| Option | Required | Description | +|--------|----------|-------------| +| `sessionDataPath` | No | Path to store session data (default: `./.wwebjs_auth`) | +| `blockedNumbers` | No | Array of phone numbers to ignore | +| `allowedGroups` | No | Array of group IDs to accept (ignores others) | +| `requireMentionInGroups` | No | Only process group messages with @mentions | +| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) | + +## Features + +| Feature | Supported | +|---------|-----------| +| Mentions | Yes | +| Reactions (add/remove) | Yes (add only) | +| Cards | No | +| Modals | No | +| Streaming | No | +| DMs | Yes | +| Ephemeral messages | No | +| File uploads | Yes | +| Typing indicator | No | +| Message history | No | + +## Thread ID format + +Thread IDs follow the pattern: `whatsapp:{chatId}` + +- DMs: `whatsapp:1234567890@c.us` +- Groups: `whatsapp:120363123456789012@g.us` + +## Running in production + +Since the adapter requires a persistent connection, deploy as a long-running process: + +```typescript title="bot.ts" +import { Chat } from "chat"; +import { createWhatsAppAdapter } from "@chat-adapter/whatsapp-web"; + +const bot = new Chat({ + userName: "mybot", + adapters: { + whatsapp: createWhatsAppAdapter({ + sessionDataPath: "/data/whatsapp-session", + }), + }, +}); + +bot.onNewMention(async (thread, message) => { + await thread.post(`Hello! You said: ${message.text}`); +}); + +async function main() { + await bot.adapters.whatsapp.initialize(); + console.log("WhatsApp bot is running"); +} + +main().catch(console.error); +``` + +Run with: + +```sh title="Terminal" +node --loader ts-node/esm bot.ts +# or +tsx bot.ts +``` + +## Troubleshooting + +### QR code not appearing + +1. **Check terminal support**: Some terminals don't render QR codes well. Try a different terminal emulator +2. **Verify dependencies**: Ensure `qrcode-terminal` is installed +3. **Check for existing session**: Delete the session folder to force new QR auth + +### Connection drops + +1. **Session expired**: WhatsApp sessions expire after inactivity. Delete session folder and re-authenticate +2. **Rate limiting**: WhatsApp may disconnect if sending too many messages. Add delays between messages +3. **Phone disconnected**: The linked device requires your phone to have internet connectivity + +### Messages not being received + +1. **Check filters**: Verify `blockedNumbers`, `allowedGroups`, and `requireMentionInGroups` settings +2. **Handler registration**: Ensure handlers are registered before calling `initialize()` +3. **Group permissions**: Bot account needs to be a participant in the group