├── config └── jest.setup.js ├── .gitignore ├── examples ├── .env.example ├── package.json ├── README.md ├── basic-usage.js ├── webhook-example.js └── package-lock.json ├── .env ├── CONTRIBUTING.md ├── jest.config.js ├── src ├── index.ts ├── types │ ├── general.ts │ └── event.ts ├── hook.ts └── classes │ ├── processPayload.ts │ ├── server.ts │ └── whatsapp.ts ├── tsconfig.json ├── .npmignore ├── LICENSE ├── package.json ├── __tests__ ├── whatsapp.test.ts └── notifications.test.ts ├── CODE_OF_CONDUCT.md └── README.md /config/jest.setup.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(30000) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | dist/ 4 | examples/.env -------------------------------------------------------------------------------- /examples/.env.example: -------------------------------------------------------------------------------- 1 | TOKEN='' 2 | PHONE_NUMBER_ID='' 3 | RECIPIENT='' 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | LISTEN_PORT=3000 2 | TOKEN=Your token here 3 | PHONE_NUMBER_ID=Your phone number id 4 | TEST_NUMBER=Your test number here 5 | VERIFY_TOKEN=30cca545-3838-48b2-80a7-9e43b1ae8ce4 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | This is an opensource project under ```MIT License``` so any one is welcome to contribute from typo, to source code to documentation, ```JUST FORK IT```. -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | transform: { 5 | '^.+\\.ts?$': 'ts-jest', 6 | }, 7 | transformIgnorePatterns: ['/node_modules/'], 8 | setupFilesAfterEnv: ['./config/jest.setup.js'] 9 | }; 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import WhatsApp from "./classes/whatsapp"; 2 | import ProcessPayload from "./classes/processPayload"; 3 | import Server from "./classes/server"; 4 | import { NotificationPayload } from "./types/event" 5 | 6 | module.exports = { 7 | WhatsApp, 8 | ProcessPayload, 9 | Server 10 | } 11 | 12 | export { 13 | WhatsApp, 14 | ProcessPayload, 15 | Server, 16 | NotificationPayload 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | // these options are overrides used only by ts-node 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "commonjs", 7 | "esModuleInterop": true, 8 | } 9 | }, 10 | "compilerOptions": { 11 | "target": "es6", 12 | "module": "commonjs", 13 | "esModuleInterop": true, 14 | "outDir": "dist", 15 | "declaration": true 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatsapp-cloud-api-examples", 3 | "version": "1.0.0", 4 | "description": "Examples for using @phoscoder/whatsapp-cloud-api", 5 | "main": "basic-usage.js", 6 | "scripts": { 7 | "start": "node basic-usage.js", 8 | "webhook": "node webhook-example.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [ 12 | "whatsapp", 13 | "whatsapp-cloud-api", 14 | "examples" 15 | ], 16 | "author": "", 17 | "license": "ISC", 18 | "type": "commonjs", 19 | "dependencies": { 20 | "@phoscoder/whatsapp-cloud-api": "^1.4.5", 21 | "dotenv": "^16.4.7" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Source files (publish only dist/) 5 | src/ 6 | 7 | # Tests 8 | __tests__/ 9 | *.test.ts 10 | *.test.js 11 | *.spec.ts 12 | *.spec.js 13 | 14 | # Configuration files 15 | tsconfig.json 16 | jest.config.js 17 | .env 18 | .env.* 19 | 20 | # Development files 21 | config/ 22 | 23 | # Examples (optional - remove this line if you want to include examples) 24 | examples/ 25 | 26 | # Git files 27 | .git/ 28 | .gitignore 29 | 30 | # Documentation (optional - keep if you want these in the package) 31 | # CODE_OF_CONDUCT.md 32 | # CONTRIBUTING.md 33 | 34 | # Build artifacts that shouldn't be published 35 | *.tsbuildinfo 36 | 37 | # IDE files 38 | .vscode/ 39 | .idea/ 40 | *.swp 41 | *.swo 42 | 43 | # OS files 44 | .DS_Store 45 | Thumbs.db 46 | 47 | # Logs 48 | *.log 49 | npm-debug.log* 50 | yarn-debug.log* 51 | yarn-error.log* 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 phoscoder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/types/general.ts: -------------------------------------------------------------------------------- 1 | export type ImageMessage = { 2 | messaging_product: "whatsapp"; 3 | recipient_type: string; 4 | to: string; 5 | type: "image"; 6 | image: { link: string; caption: string | null } | { id: string; caption: string | null }; 7 | }; 8 | 9 | export type AudioMessage = { 10 | messaging_product: "whatsapp"; 11 | recipient_type?: string; 12 | to: string; 13 | type: "audio"; 14 | audio: { link: string } | { id: string }; 15 | }; 16 | 17 | export type VideoMessage = { 18 | messaging_product: "whatsapp"; 19 | recipient_type?: string; 20 | to: string; 21 | type: "video"; 22 | video: { link: string, caption: string | null } | { id: string, caption: string | null }; 23 | }; 24 | 25 | export type DocumentMessage = { 26 | messaging_product: "whatsapp"; 27 | recipient_type?: string; 28 | to: string; 29 | type: "document"; 30 | document: { link: string, caption: string | null, filename: string } | { id: string, caption: string | null, filename: string }; 31 | }; 32 | 33 | export type LocationMessage = { 34 | messaging_product: "whatsapp"; 35 | recipient_type?: string; 36 | to: string; 37 | type: "location"; 38 | location: { latitude: number; longitude: number; name: string | null; address: string | null }; 39 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@phoscoder/whatsapp-cloud-api", 3 | "version": "1.4.5", 4 | "description": "wa-cloud-api is a javascript port of heyoo(made in python). heyoo is a python wrapper for the Whatsapp Cloud API", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist/", 9 | "LICENSE", 10 | "README.md" 11 | ], 12 | "prepublish": "tsc", 13 | "scripts": { 14 | "dev": "ts-node-dev --exit-child src/hook.ts", 15 | "start": "ts-node src/hook.ts", 16 | "publish": "npm publish --access=public", 17 | "build": "tsc", 18 | "test": "jest" 19 | }, 20 | "keywords": [ 21 | "api", 22 | "typescript", 23 | "api-wrapper", 24 | "whatsapp", 25 | "cloud-api", 26 | "whatsapp-cloud", 27 | "whatsapp-cloud-api", 28 | "whatsapp-business", 29 | "whatsapp-business-api", 30 | "bot", 31 | "whatsapp-bot", 32 | "chatbot", 33 | "bot-framework", 34 | "nodejs", 35 | "bot-api", 36 | "whatsapp-api", 37 | "business-api" 38 | ], 39 | "author": "JS-Hub-ZW", 40 | "license": "MIT", 41 | "devDependencies": { 42 | "@types/express": "^4.17.13", 43 | "@types/jest": "^27.5.1", 44 | "@types/node": "^17.0.35", 45 | "jest": "^28.1.0", 46 | "supertest": "^6.2.3", 47 | "ts-jest": "^28.0.2", 48 | "ts-node": "^10.8.0", 49 | "ts-node-dev": "^1.1.8", 50 | "typescript": "^4.6.4" 51 | }, 52 | "dependencies": { 53 | "axios": "^1.13.2", 54 | "body-parser": "^1.20.0", 55 | "dotenv": "^16.0.1", 56 | "express": "^4.18.1" 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "git+https://github.com/phoscoder/wa-cloud-api.git" 61 | }, 62 | "type": "commonjs", 63 | "bugs": { 64 | "url": "https://github.com/phoscoder/wa-cloud-api/issues" 65 | }, 66 | "homepage": "https://github.com/phoscoder/wa-cloud-api#readme" 67 | } 68 | -------------------------------------------------------------------------------- /src/hook.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import Server from './classes/server' 3 | 4 | 5 | let notificationServer = new Server( 6 | process.env.LISTEN_PORT, 7 | process.env.VERIFY_TOKEN 8 | ) 9 | 10 | let app = notificationServer.start(async (rawData, processedPayload) => { 11 | // NOTE: raw_data is the raw data you recieve from Whatsapp 12 | // NOTE: processed_data is a object of ProcessPayload, 13 | // NOTE: it contains both the data and helper functions 14 | // TIP FOR BEGINNERS: use processed_data 15 | 16 | 17 | 18 | if (processedPayload.type == "messages"){ 19 | let messages = processedPayload.getMessages() 20 | 21 | for (const message of messages){ 22 | if (message.type == "text"){ 23 | // console.log("Message Data: ", message) 24 | return message 25 | } 26 | 27 | else if (message.type == "image"){ 28 | // console.log("Image Message Data: ", message) 29 | return message 30 | } 31 | 32 | else if (message.type == "sticker"){ 33 | // console.log("Sticker Message Data: ", message) 34 | return message 35 | } 36 | 37 | else if (message.type == "video"){ 38 | // console.log("Video Message Data: ", message) 39 | return message 40 | } 41 | 42 | else if (message.type == "audio"){ 43 | // console.log("Audio Message Data: ", message) 44 | return message 45 | } 46 | 47 | else if (message.type == "document"){ 48 | // console.log("Document Message Data: ", message) 49 | return message 50 | } 51 | 52 | else if (message.type == "location"){ 53 | // console.log("Location Message Data: ", message) 54 | return message 55 | } 56 | 57 | else if (message.type == "contacts"){ 58 | // console.log("Contacts Message Data: ", message) 59 | return message 60 | } 61 | } 62 | } 63 | 64 | // // console.log("Payload Type: ", processedPayload.type) 65 | // // console.log("Payload Data: ", processedPayload.data.entry[0].changes[0].value) 66 | 67 | return "Thanks, notification recieved!" 68 | 69 | 70 | 71 | }) 72 | 73 | export default app 74 | 75 | -------------------------------------------------------------------------------- /src/classes/processPayload.ts: -------------------------------------------------------------------------------- 1 | import { Message, NotificationPayload } from "../types/event"; 2 | 3 | class ProcessPayload{ 4 | data: NotificationPayload 5 | type: string 6 | 7 | constructor(data:NotificationPayload){ 8 | this.data = data 9 | this.getType() 10 | } 11 | 12 | getValue = () => this.data.entry[0].changes[0].value 13 | 14 | getType(){ 15 | let value_keys = Object.keys(this.getValue()) 16 | 17 | if (value_keys.includes("messages")){ 18 | this.type = "messages" 19 | }else if (value_keys.includes("status")){ 20 | this.type = "statuses" 21 | }else if (value_keys.includes("contacts")){ 22 | this.type = "contacts" 23 | } 24 | } 25 | 26 | getMediaLinks(id: string | number){ 27 | // Use the media api to fetch image data 28 | 29 | 30 | 31 | } 32 | 33 | processMessage(m:Message){ 34 | 35 | let possible_media = [ 36 | "image", 37 | "video", 38 | "audio", 39 | "document", 40 | "sticker" 41 | ] 42 | 43 | let message_keys = Object.keys(m) 44 | if (message_keys.includes("location")){ 45 | m.type = "location" 46 | } 47 | 48 | if (message_keys.includes("contacts")){ 49 | m.type = "contacts" 50 | } 51 | 52 | if (message_keys.includes("referral")){ 53 | m.type = "referral" 54 | } 55 | 56 | m.has_media = false 57 | 58 | for (const key of message_keys){ 59 | if (possible_media.includes(key)){ 60 | m.has_media = true 61 | break 62 | } 63 | } 64 | 65 | return m 66 | } 67 | 68 | getMessages(){ 69 | let raw_messages = this.data.entry[0].changes[0].value.messages 70 | 71 | let messages:Message[] = raw_messages.map(m => { 72 | let processed_message = this.processMessage(m) 73 | return processed_message 74 | }) 75 | 76 | return messages 77 | } 78 | 79 | getStatuses(){ 80 | return this.data.entry[0].changes[0].value?.statuses 81 | } 82 | 83 | getContacts(){ 84 | return this.data.entry[0].changes[0].value?.contacts 85 | } 86 | 87 | getErrors(){ 88 | return this.data.entry[0].changes[0].value?.errors 89 | } 90 | 91 | getMetadata(){ 92 | return this.data.entry[0].changes[0].value?.metadata 93 | } 94 | } 95 | 96 | export default ProcessPayload -------------------------------------------------------------------------------- /src/classes/server.ts: -------------------------------------------------------------------------------- 1 | import bodyParser from "body-parser" 2 | import express from "express" 3 | 4 | import { Request, Response } from 'express' 5 | import { NotificationPayload } from "../types/event" 6 | import ProcessPayload from "./processPayload" 7 | 8 | class Server { 9 | port:number | string 10 | verifyToken:string 11 | 12 | constructor( verifyToken:string, port:number|string=6000){ 13 | this.port = port 14 | this.verifyToken = verifyToken 15 | } 16 | 17 | start(notificationCallback: (rawData: NotificationPayload, processedPayload: ProcessPayload) => Promise){ 18 | const app = express() 19 | // Middleware 20 | app.use(bodyParser.urlencoded({ extended: false})) 21 | app.use(bodyParser.json()) 22 | 23 | app.get('/', this.handleVerification) 24 | app.post("/", async (req: Request, res:Response) => { 25 | let data: NotificationPayload = req.body 26 | 27 | let ppayload = new ProcessPayload(data) 28 | let resp = await notificationCallback(data, ppayload) 29 | 30 | return res.json(resp) 31 | }) 32 | 33 | app.listen(this.port, () => { 34 | // console.log(`Example app listening on port ${this.port}`) 35 | }) 36 | 37 | return app 38 | } 39 | 40 | handleVerification = (req: Request, res:Response) => { 41 | console.log("Request Data: ", req.query) 42 | 43 | // Handle Verification 44 | if (req.query["hub.verify_token"]){ 45 | let hub:any = req.query 46 | console.log("Hub: ", hub) 47 | let verify_token = req.query["hub.verify_token"] 48 | 49 | if (verify_token == this.verifyToken){ 50 | return res.send(req.query["hub.challenge"]) 51 | } 52 | return res.send("Invalid verification token") 53 | } 54 | 55 | return res.send("This is a default response") 56 | } 57 | 58 | verifyWebhookToken = (query: Record): string => { 59 | // console.log("Request Data: ", query) 60 | 61 | // Handle Verification 62 | if (query["hub.verify_token"]){ 63 | 64 | console.log("Hub: ", query) 65 | let verify_token = query["hub.verify_token"] 66 | 67 | if (verify_token == this.verifyToken){ 68 | return query["hub.challenge"] 69 | } 70 | return "Invalid verification token" 71 | } 72 | 73 | return "This is a default response" 74 | } 75 | 76 | } 77 | 78 | export default Server -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # WhatsApp Cloud API Examples 2 | 3 | This directory contains examples demonstrating how to use the `@phoscoder/whatsapp-cloud-api` package in a Node.js project. 4 | 5 | ## Setup 6 | 7 | 1. **Install dependencies:** 8 | ```bash 9 | npm install 10 | ``` 11 | 12 | 2. **Configure environment variables:** 13 | - Copy `.env.example` to `.env` 14 | - Fill in your WhatsApp Cloud API credentials from [Facebook Developer Portal](https://developers.facebook.com/) 15 | 16 | ```bash 17 | cp .env.example .env 18 | ``` 19 | 20 | 3. **Get your credentials:** 21 | - Go to [Facebook Developer Portal](https://developers.facebook.com/apps) 22 | - Create a new app or select an existing one 23 | - Add WhatsApp Messenger product 24 | - Get your **TOKEN** and **PHONE_NUMBER_ID** 25 | - Verify your test recipient phone number 26 | 27 | ## Examples 28 | 29 | ### 1. Basic Usage (`basic-usage.js`) 30 | 31 | Demonstrates how to send different types of messages: 32 | - Text messages 33 | - Images 34 | - Videos 35 | - Documents 36 | - Location 37 | - Interactive buttons 38 | - Template messages 39 | 40 | **Run it:** 41 | ```bash 42 | node basic-usage.js 43 | ``` 44 | 45 | Make sure to: 46 | - Update the `TOKEN`, `PHONE_NUMBER_ID`, and `RECIPIENT` variables 47 | - Uncomment the function you want to test in the main execution block 48 | 49 | ### 2. Webhook Example (`webhook-example.js`) 50 | 51 | Demonstrates how to set up a webhook server to receive incoming messages and events. 52 | 53 | **Run it:** 54 | ```bash 55 | node webhook-example.js 56 | ``` 57 | 58 | Features: 59 | - Handles incoming text messages 60 | - Responds to different commands (hello, help, menu) 61 | - Handles media messages (images, videos, audio, documents) 62 | - Handles location messages 63 | - Handles interactive message responses 64 | - Tracks message delivery status 65 | 66 | **Setting up the webhook:** 67 | 1. Deploy your webhook to a public server (or use ngrok for local testing) 68 | 2. Go to Facebook Developer Portal 69 | 3. Configure webhook settings: 70 | - **Webhook URL:** `https://your-domain.com/webhook` 71 | - **Verify Token:** Use the same token from your `.env` file 72 | 4. Subscribe to webhook fields (messages, message_status, etc.) 73 | 74 | ## Using with Environment Variables 75 | 76 | Both examples support environment variables. Create a `.env` file: 77 | 78 | ```bash 79 | WHATSAPP_TOKEN=your_actual_token 80 | PHONE_NUMBER_ID=your_phone_number_id 81 | VERIFY_TOKEN=your_verify_token 82 | LISTEN_PORT=3000 83 | TEST_RECIPIENT=recipient_phone_number 84 | ``` 85 | 86 | Then modify the examples to use: 87 | 88 | ```javascript 89 | require('dotenv').config(); 90 | const TOKEN = process.env.WHATSAPP_TOKEN; 91 | const PHONE_NUMBER_ID = process.env.PHONE_NUMBER_ID; 92 | ``` 93 | 94 | ## Important Notes 95 | 96 | 1. **Media URLs:** When sending media, the URL must be publicly accessible and return the correct content-type header. 97 | 98 | 2. **Phone Number Format:** Use international format without '+' or '00' prefix (e.g., `255757902132` for a Tanzanian number). 99 | 100 | 3. **Template Messages:** Template messages must be pre-approved in the Facebook Business Manager before you can use them. 101 | 102 | 4. **Rate Limits:** Be aware of WhatsApp Cloud API rate limits. Check the [official documentation](https://developers.facebook.com/docs/whatsapp/cloud-api) for details. 103 | 104 | 5. **Media Download:** Media URLs from `getMedia()` are only valid for 5 minutes. Download and store them immediately if needed. 105 | 106 | ## Resources 107 | 108 | - [WhatsApp Cloud API Documentation](https://developers.facebook.com/docs/whatsapp/cloud-api) 109 | - [Facebook Developer Portal](https://developers.facebook.com/) 110 | - [Package Repository](https://github.com/phoscoder/wa-cloud-api) 111 | - [NPM Package](https://www.npmjs.com/package/@phoscoder/whatsapp-cloud-api) 112 | 113 | ## Support 114 | 115 | If you encounter any issues, please open an issue on the [GitHub repository](https://github.com/phoscoder/wa-cloud-api/issues). 116 | -------------------------------------------------------------------------------- /__tests__/whatsapp.test.ts: -------------------------------------------------------------------------------- 1 | import WhatsApp from "../src/classes/whatsapp"; 2 | import "dotenv/config"; 3 | 4 | let messenger = new WhatsApp(process.env.TOKEN, process.env.PHONE_NUMBER_ID); 5 | let test_number = process.env.TEST_NUMBER; 6 | 7 | describe("Test Messages", () => { 8 | test("Send a message to a whatsapp number", async () => { 9 | try { 10 | let resp = await messenger.sendMessage( 11 | "Hello man this is a test message", 12 | test_number as string, 13 | ); 14 | 15 | expect(resp.status).toBe(200); 16 | expect(resp.statusText).toBe("OK"); 17 | } catch (e) { 18 | // console.log(e) 19 | } 20 | }); 21 | 22 | test("Send a template to a whatsapp number", async () => { 23 | try { 24 | let resp = await messenger.sendTemplate("hello_world", test_number as string); 25 | 26 | expect(resp.status).toBe(200); 27 | expect(resp.statusText).toBe("OK"); 28 | } catch (e) { 29 | // console.log(e) 30 | } 31 | }); 32 | }); 33 | 34 | describe("Test Media", () => { 35 | test("Send a image to a whatsapp number", async () => { 36 | try { 37 | let resp = await messenger.sendImage( 38 | "https://i.imgur.com/Fh7XVYY.jpeg", 39 | test_number as string, 40 | ); 41 | 42 | expect(resp.status).toBe(200); 43 | expect(resp.statusText).toBe("OK"); 44 | } catch (e) { 45 | // console.log(e) 46 | } 47 | }); 48 | 49 | test("Send a video to a whatsapp number", async () => { 50 | try { 51 | let resp = await messenger.sendVideo( 52 | "https://www.youtube.com/watch?v=K4TOrB7at0Y", 53 | test_number, 54 | ); 55 | 56 | expect(resp.status).toBe(200); 57 | expect(resp.statusText).toBe("OK"); 58 | } catch (e) { 59 | // console.log(e) 60 | } 61 | }); 62 | 63 | test("Send a audio to a whatsapp number", async () => { 64 | try { 65 | let resp = await messenger.sendAudio( 66 | "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", 67 | test_number, 68 | ); 69 | 70 | expect(resp.status).toBe(200); 71 | expect(resp.statusText).toBe("OK"); 72 | } catch (e) { 73 | // console.log(e) 74 | } 75 | }); 76 | 77 | test("Send a document to a whatsapp number", async () => { 78 | try { 79 | let resp = await messenger.sendDocument( 80 | "http://www.africau.edu/images/default/sample.pdf", 81 | test_number, 82 | ); 83 | 84 | expect(resp.status).toBe(200); 85 | expect(resp.statusText).toBe("OK"); 86 | } catch (e) { 87 | // console.log(e) 88 | } 89 | }); 90 | }); 91 | 92 | describe("Test Location", () => { 93 | test("Send a location to a whatsapp number", async () => { 94 | try { 95 | let resp = await messenger.sendLocation( 96 | 1.29, 97 | 103.85, 98 | "Singapore", 99 | "Singapore", 100 | test_number as string, 101 | ); 102 | 103 | expect(resp.status).toBe(200); 104 | expect(resp.statusText).toBe("OK"); 105 | } catch (e) { 106 | // console.log(e) 107 | } 108 | }); 109 | }); 110 | 111 | describe("Test buttons", () => { 112 | test("Send an interative button to a whatsapp number", async () => { 113 | try { 114 | let resp = await messenger.sendButton({ 115 | header: "Header Testing", 116 | body: "Body Testing", 117 | footer: "Footer Testing", 118 | action: { 119 | button: "Button Testing", 120 | sections: [ 121 | { 122 | title: "iBank", 123 | rows: [ 124 | { id: "row 1", title: "Send Money", description: "" }, 125 | { 126 | id: "row 2", 127 | title: "Withdraw money", 128 | description: "", 129 | }, 130 | ], 131 | }, 132 | ], 133 | }, 134 | }, test_number as string); 135 | 136 | expect(resp.status).toBe(200); 137 | expect(resp.statusText).toBe("OK"); 138 | } catch (e) { 139 | // console.log(e) 140 | } 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | Trust, respect, collaboration and transparency are core values we believe should live and breathe within our projects. Our community welcomes participants from around the world with different experiences, unique perspectives, and great ideas to share. 4 | 5 | ## Our Pledge 6 | 7 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to creating a positive environment include: 12 | 13 | - Using welcoming and inclusive language 14 | - Being respectful of differing viewpoints and experiences 15 | - Gracefully accepting constructive criticism 16 | - Attempting collaboration before conflict 17 | - Focusing on what is best for the community 18 | - Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | - Violence, threats of violence, or inciting others to commit self-harm 23 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 24 | - Trolling, intentionally spreading misinformation, insulting/derogatory comments, and personal or political attacks 25 | - Public or private harassment 26 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 27 | - Abuse of the reporting process to intentionally harass or exclude others 28 | - Advocating for, or encouraging, any of the above behavior 29 | - Other conduct which could reasonably be considered inappropriate in a professional setting 30 | 31 | ## Our Responsibilities 32 | 33 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 34 | 35 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 40 | 41 | ## Enforcement 42 | 43 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting us. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. 44 | 45 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 46 | 47 | If you are unsure whether an incident is a violation, or whether the space where the incident took place is covered by our Code of Conduct, **we encourage you to still report it**. We would prefer to have a few extra reports where we decide to take no action, than to leave an incident go unnoticed and unresolved that may result in an individual or group to feel like they can no longer participate in the community. Reports deemed as not a violation will also allow us to improve our Code of Conduct and processes surrounding it. If you witness a dangerous situation or someone in distress, we encourage you to report even if you are only an observer. 48 | 49 | ## Attribution 50 | 51 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html -------------------------------------------------------------------------------- /src/types/event.ts: -------------------------------------------------------------------------------- 1 | export interface NotificationPayload { 2 | object:string 3 | entry: Entry[] 4 | } 5 | 6 | export interface Entry { 7 | id:string, 8 | changes: Change[] 9 | } 10 | 11 | export interface Change { 12 | value: Value 13 | field: string 14 | } 15 | 16 | export interface Value{ 17 | messaging_product: string 18 | metadata: { 19 | display_phone_number: any; 20 | phone_number_id: any 21 | } 22 | contacts?: Contact[] 23 | errors?: any[] 24 | statuses?: Status[] 25 | messages?: Message[] 26 | } 27 | 28 | export interface Message { 29 | from: any 30 | id: string 31 | timestamp: any 32 | context?: { 33 | from: any; 34 | id: string 35 | } 36 | button?: { 37 | text: string; 38 | payload: string 39 | } 40 | text?:{ 41 | body: string 42 | } 43 | image?: Image 44 | has_media?: boolean 45 | sticker?: Sticker 46 | document?: Document 47 | video?: Video 48 | audio?: Audio 49 | contacts?: ContactsData 50 | identity?: Identity 51 | location?: LocationData 52 | interactive?:Interactive 53 | referral: any // Handle this 54 | system: System 55 | errors?: UnknownMessageError[] 56 | type?: string 57 | } 58 | 59 | export interface Interactive{ 60 | type: { 61 | button_reply?: { 62 | id: string | number; 63 | title: string 64 | }; 65 | list_reply?: { 66 | id: string | number; 67 | title: string 68 | description: string 69 | } 70 | 71 | } 72 | } 73 | 74 | export interface System { 75 | body: string 76 | new_wa_id: string | number 77 | type: string 78 | } 79 | 80 | export interface Document{ 81 | caption: string 82 | filename: string 83 | sha256: string 84 | mime_type: string 85 | id: any 86 | } 87 | 88 | export interface Video{ 89 | caption: string 90 | filename: string 91 | sha256: string 92 | mime_type: string 93 | id: any 94 | } 95 | 96 | export interface Audio{ 97 | mime_type: string 98 | id: any 99 | } 100 | 101 | export interface Document{ 102 | mime_type: string 103 | id: any 104 | } 105 | 106 | export interface Identity { 107 | acknowledged: any 108 | created_timestamp: any 109 | hash: string 110 | } 111 | 112 | export interface Contact{ 113 | wa_id: any, 114 | profile: { 115 | name:string 116 | } 117 | } 118 | 119 | 120 | export interface Image{ 121 | caption: string 122 | mime_type: string 123 | sha256: string 124 | id: string 125 | } 126 | 127 | export interface Sticker{ 128 | mime_type: string 129 | sha256: string 130 | id: string 131 | } 132 | 133 | 134 | export interface ContactsMessage{ 135 | from: any 136 | id: string 137 | timestamp: any 138 | contacts: ContactsData 139 | type: string 140 | } 141 | 142 | export interface ContactsData { 143 | addresses: Address[], 144 | birthday: string, 145 | emails: Email[] 146 | name: Name 147 | org: Org 148 | phones: Phone[] 149 | urls: URLData[] 150 | } 151 | 152 | export interface Address{ 153 | city: string 154 | country: string 155 | country_code: string 156 | state: string 157 | street: string 158 | type: string 159 | zip: string 160 | } 161 | export interface Email{ 162 | email:string 163 | type: string 164 | } 165 | 166 | export interface Name{ 167 | formatted_name: string 168 | first_name: string 169 | last_name: string 170 | middle_name: string 171 | suffix: string 172 | prefix: string 173 | } 174 | 175 | export interface Org{ 176 | company: string 177 | department: string 178 | title: string 179 | } 180 | 181 | export interface Phone{ 182 | phone: string 183 | wa_id: string 184 | type: string 185 | } 186 | 187 | export interface URLData { 188 | url: string 189 | type: string 190 | } 191 | 192 | export interface UnknownMessageError{ 193 | code: number, 194 | details: string, 195 | title: string 196 | } 197 | 198 | export interface LocationData{ 199 | latitude: any 200 | longitude: any 201 | name: string, 202 | address: string 203 | } 204 | 205 | export interface Status{ 206 | id: string | number 207 | recipient_id: string 208 | status: string 209 | timestamp: number | string 210 | conversation: { 211 | id: string |number; 212 | origin: { 213 | type: string; 214 | }; 215 | expiration_timestamp: string 216 | } 217 | pricing: { 218 | category: string 219 | billable?: boolean 220 | pricing_model?: string | number 221 | } 222 | 223 | } 224 | 225 | 226 | 227 | 228 | -------------------------------------------------------------------------------- /examples/basic-usage.js: -------------------------------------------------------------------------------- 1 | // Import the WhatsApp Cloud API package 2 | const { WhatsApp, Server } = require("@phoscoder/whatsapp-cloud-api"); 3 | const { config } = require("dotenv"); 4 | config(); 5 | 6 | // Set up your credentials 7 | // Replace these with your actual values from Facebook Developer Portal 8 | // e.g., '255757902132' 9 | const TOKEN = process.env.TOKEN; 10 | const PHONE_NUMBER_ID = process.env.PHONE_NUMBER_ID; 11 | const WABA_ID = process.env.WABA_ID; 12 | const RECIPIENT = process.env.RECIPIENT; 13 | 14 | // Initialize the WhatsApp messenger 15 | const messenger = new WhatsApp(TOKEN, PHONE_NUMBER_ID, WABA_ID); 16 | 17 | // Example 1: Send a simple text message 18 | async function sendTextMessage() { 19 | try { 20 | const response = await messenger.sendMessage( 21 | "Hello! This is a test message from WhatsApp Cloud API", 22 | RECIPIENT, 23 | ); 24 | console.log("Message sent successfully:", response); 25 | } catch (error) { 26 | console.error("Error sending message:", error.message); 27 | } 28 | } 29 | 30 | // Example 2: Send an image 31 | async function sendImage() { 32 | try { 33 | const response = await messenger.sendImage( 34 | "https://hips.hearstapps.com/hmg-prod/images/dog-puppy-on-garden-royalty-free-image-1586966191.jpg?crop=0.752xw:1.00xh;0.175xw,0&resize=1200:*", 35 | RECIPIENT, 36 | undefined, 37 | "A cute dog", 38 | ); 39 | console.log("Image sent successfully:", response); 40 | } catch (error) { 41 | console.error("Error sending image:", error); 42 | } 43 | } 44 | 45 | // Example 3: Send a video 46 | async function sendVideo() { 47 | try { 48 | const response = await messenger.sendVideo( 49 | "https://www.youtube.com/watch?v=K4TOrB7at0Y", 50 | RECIPIENT, 51 | ); 52 | console.log("Video sent successfully:", response); 53 | } catch (error) { 54 | console.error("Error sending video:", error); 55 | } 56 | } 57 | 58 | // Example 4: Send a document 59 | async function sendDocument() { 60 | try { 61 | const response = await messenger.sendDocument( 62 | "https://ontheline.trincoll.edu/images/bookdown/sample-local-pdf.pdf", 63 | RECIPIENT, 64 | ); 65 | console.log("Document sent successfully:", response); 66 | } catch (error) { 67 | console.error("Error sending document:", error); 68 | } 69 | } 70 | 71 | // Example 5: Send location 72 | async function sendLocation() { 73 | try { 74 | const response = await messenger.sendLocation( 75 | 1.29, 76 | 103.85, 77 | "Singapore", 78 | "Singapore", 79 | RECIPIENT, 80 | ); 81 | console.log("Location sent successfully:", response); 82 | } catch (error) { 83 | console.error("Error sending location:", error); 84 | } 85 | } 86 | 87 | // Example 6: Send interactive buttons 88 | async function sendButton() { 89 | try { 90 | const response = await messenger.sendButton(RECIPIENT, { 91 | header: "Banking Services", 92 | body: "Please select an option below", 93 | footer: "Powered by Your Bank", 94 | action: { 95 | button: "Services", 96 | sections: [ 97 | { 98 | title: "iBank", 99 | rows: [ 100 | { 101 | id: "row 1", 102 | title: "Send Money", 103 | description: "Transfer funds to another account", 104 | }, 105 | { 106 | id: "row 2", 107 | title: "Withdraw money", 108 | description: "Withdraw from your account", 109 | }, 110 | ], 111 | }, 112 | ], 113 | }, 114 | }); 115 | console.log("Button sent successfully:", response); 116 | } catch (error) { 117 | console.error("Error sending button:", error); 118 | } 119 | } 120 | 121 | // Example 7: Send a template message 122 | async function sendTemplate() { 123 | try { 124 | const response = await messenger.sendTemplate("hello_world", RECIPIENT); 125 | console.log("Template sent successfully:", response); 126 | } catch (error) { 127 | console.error("Error sending template:", error); 128 | } 129 | } 130 | 131 | async function getTemplates() { 132 | try { 133 | const response = await messenger.getTemplates(); 134 | console.log("Templates retrieved successfully:", response); 135 | } catch (error) { 136 | console.error("Error retrieving templates:", error); 137 | } 138 | } 139 | 140 | // Run examples 141 | // Uncomment the function you want to test 142 | (async () => { 143 | console.log("WhatsApp Cloud API Examples\n"); 144 | 145 | await sendTextMessage(); 146 | // await sendImage(); 147 | // await sendVideo(); 148 | // await sendDocument(); 149 | // await sendLocation(); 150 | // await sendButton(); 151 | // await sendTemplate(); 152 | 153 | await getTemplates(); 154 | })(); 155 | 156 | let notificationServer = new Server( 157 | process.env.VERIFY_TOKEN, 158 | 6000 159 | ) 160 | 161 | let app = notificationServer.start(async (rawData ,processedPayload) => { 162 | // Do your stuff here 163 | // let messages = processedPayload.getMessages() 164 | // let metadata = processedPayload.getContacts() 165 | // let contacts = processedPayload.getContacts() 166 | // let status = processedPayload.getStatuses() 167 | 168 | console.log("processedPayload Type: ", processedPayload.type) 169 | console.log("processedPayload: ", JSON.stringify(rawData)) 170 | console.log("contacts ", processedPayload.getContacts()) 171 | 172 | 173 | console.log(processedPayload.type == "messages" ? processedPayload.getMessages() : processedPayload.getStatuses()) 174 | 175 | // Do other stuff here 176 | }) -------------------------------------------------------------------------------- /examples/webhook-example.js: -------------------------------------------------------------------------------- 1 | // Import required packages 2 | const { WhatsApp } = require("@phoscoder/whatsapp-cloud-api"); 3 | const Server = 4 | require("@phoscoder/whatsapp-cloud-api/dist/classes/server").default; 5 | 6 | // Load environment variables (create a .env file with these values) 7 | require("dotenv").config(); 8 | 9 | // Configuration 10 | const TOKEN = process.env.WHATSAPP_TOKEN || "YOUR_ACCESS_TOKEN_HERE"; 11 | const PHONE_NUMBER_ID = 12 | process.env.PHONE_NUMBER_ID || "YOUR_PHONE_NUMBER_ID_HERE"; 13 | const VERIFY_TOKEN = process.env.VERIFY_TOKEN || "your_verify_token"; 14 | const LISTEN_PORT = process.env.LISTEN_PORT || 3000; 15 | 16 | // Initialize WhatsApp messenger for sending responses 17 | const messenger = new WhatsApp(TOKEN, PHONE_NUMBER_ID); 18 | 19 | // Initialize notification server 20 | const notificationServer = new Server(LISTEN_PORT, VERIFY_TOKEN); 21 | 22 | // Handle incoming notifications 23 | async function handleNotifications(rawData, processedPayload) { 24 | console.log("Received notification"); 25 | 26 | try { 27 | // Get messages from the payload 28 | const messages = processedPayload.get_messages(); 29 | const contacts = processedPayload.get_contacts(); 30 | const statuses = processedPayload.get_statuses(); 31 | 32 | if (messages && messages.length > 0) { 33 | for (const message of messages) { 34 | console.log("Message received:", message); 35 | 36 | const senderId = message.from; 37 | const messageType = message.type; 38 | 39 | // Handle different message types 40 | switch (messageType) { 41 | case "text": 42 | await handleTextMessage(message, senderId); 43 | break; 44 | 45 | case "image": 46 | await handleImageMessage(message, senderId); 47 | break; 48 | 49 | case "video": 50 | await handleVideoMessage(message, senderId); 51 | break; 52 | 53 | case "audio": 54 | await handleAudioMessage(message, senderId); 55 | break; 56 | 57 | case "document": 58 | await handleDocumentMessage(message, senderId); 59 | break; 60 | 61 | case "location": 62 | await handleLocationMessage(message, senderId); 63 | break; 64 | 65 | case "interactive": 66 | await handleInteractiveMessage(message, senderId); 67 | break; 68 | 69 | default: 70 | console.log(`Unsupported message type: ${messageType}`); 71 | } 72 | } 73 | } 74 | 75 | // Handle message status updates (sent, delivered, read, etc.) 76 | if (statuses && statuses.length > 0) { 77 | for (const status of statuses) { 78 | console.log("Status update:", status); 79 | // You can track message delivery status here 80 | } 81 | } 82 | } catch (error) { 83 | console.error("Error handling notification:", error); 84 | } 85 | } 86 | 87 | // Handle text messages 88 | async function handleTextMessage(message, senderId) { 89 | const text = message.text.body.toLowerCase(); 90 | console.log(`Text message from ${senderId}: ${text}`); 91 | 92 | // Simple echo bot 93 | if (text.includes("hello") || text.includes("hi")) { 94 | await messenger.sendMessage("Hello! How can I help you today?", senderId); 95 | } else if (text.includes("help")) { 96 | await messenger.sendMessage( 97 | 'Available commands:\n- Say "hello" to get a greeting\n- Say "help" to see this message\n- Say "menu" to see options', 98 | senderId, 99 | ); 100 | } else if (text.includes("menu")) { 101 | await messenger.sendButton(senderId, { 102 | header: "Main Menu", 103 | body: "Please select an option:", 104 | footer: "Powered by WhatsApp Cloud API", 105 | action: { 106 | button: "Options", 107 | sections: [ 108 | { 109 | title: "Services", 110 | rows: [ 111 | { 112 | id: "service_1", 113 | title: "Service 1", 114 | description: "Description for service 1", 115 | }, 116 | { 117 | id: "service_2", 118 | title: "Service 2", 119 | description: "Description for service 2", 120 | }, 121 | ], 122 | }, 123 | ], 124 | }, 125 | }); 126 | } else { 127 | await messenger.sendMessage(`You said: "${message.text.body}"`, senderId); 128 | } 129 | } 130 | 131 | // Handle image messages 132 | async function handleImageMessage(message, senderId) { 133 | console.log(`Image received from ${senderId}`); 134 | 135 | // Get media details 136 | const mediaId = message.image.id; 137 | const mediaData = await messenger.getMedia(mediaId); 138 | 139 | console.log("Media data:", mediaData); 140 | 141 | await messenger.sendMessage("Thank you for the image!", senderId); 142 | } 143 | 144 | // Handle video messages 145 | async function handleVideoMessage(message, senderId) { 146 | console.log(`Video received from ${senderId}`); 147 | await messenger.sendMessage("Thank you for the video!", senderId); 148 | } 149 | 150 | // Handle audio messages 151 | async function handleAudioMessage(message, senderId) { 152 | console.log(`Audio received from ${senderId}`); 153 | await messenger.sendMessage("Thank you for the audio message!", senderId); 154 | } 155 | 156 | // Handle document messages 157 | async function handleDocumentMessage(message, senderId) { 158 | console.log(`Document received from ${senderId}`); 159 | await messenger.sendMessage("Thank you for the document!", senderId); 160 | } 161 | 162 | // Handle location messages 163 | async function handleLocationMessage(message, senderId) { 164 | const { latitude, longitude } = message.location; 165 | console.log(`Location received from ${senderId}: ${latitude}, ${longitude}`); 166 | await messenger.sendMessage( 167 | `Received your location: ${latitude}, ${longitude}`, 168 | senderId, 169 | ); 170 | } 171 | 172 | // Handle interactive messages (button responses, list responses) 173 | async function handleInteractiveMessage(message, senderId) { 174 | console.log(`Interactive message from ${senderId}:`, message.interactive); 175 | 176 | if (message.interactive.type === "button_reply") { 177 | const buttonId = message.interactive.button_reply.id; 178 | await messenger.sendMessage(`You selected button: ${buttonId}`, senderId); 179 | } else if (message.interactive.type === "list_reply") { 180 | const listId = message.interactive.list_reply.id; 181 | const listTitle = message.interactive.list_reply.title; 182 | await messenger.sendMessage(`You selected: ${listTitle}`, senderId); 183 | } 184 | } 185 | 186 | // Start the server 187 | const app = notificationServer.start(handleNotifications); 188 | 189 | console.log(`\nWebhook server started on port ${LISTEN_PORT}`); 190 | console.log(`Verify token: ${VERIFY_TOKEN}`); 191 | console.log( 192 | "\nMake sure to configure your webhook URL in Facebook Developer Portal:", 193 | ); 194 | console.log(`Webhook URL: https://your-domain.com/webhook`); 195 | console.log(`Verify Token: ${VERIFY_TOKEN}\n`); 196 | -------------------------------------------------------------------------------- /src/classes/whatsapp.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { AudioMessage, DocumentMessage, ImageMessage, VideoMessage } from "../types/general"; 3 | 4 | const VERSION = "v24.0"; 5 | 6 | 7 | 8 | enum httpMethod { 9 | POST = "post", 10 | GET = "get", 11 | PUT = "put", 12 | DELETE = "delete", 13 | } 14 | 15 | 16 | export default class WhatsApp { 17 | phone_number_id: string; 18 | wa_business_account_id: string; 19 | token: string; 20 | headers: { "Content-Type": string; Authorization: string }; 21 | url: string; 22 | debug: boolean; 23 | 24 | constructor(token: string = "", phone_number_id: string = "", wa_business_account_id: string = "", debug: boolean = false) { 25 | this.token = token; 26 | this.phone_number_id = phone_number_id; 27 | this.wa_business_account_id = wa_business_account_id; 28 | this.url = `https://graph.facebook.com/${VERSION}/${phone_number_id}/messages`; 29 | this.debug = debug; 30 | this.headers = { 31 | "Content-Type": "application/json", 32 | Authorization: `Bearer ${token}`, 33 | }; 34 | } 35 | 36 | buildUrl(path: string, useBusinessAccountId: boolean = false) { 37 | return `https://graph.facebook.com/${VERSION}/${useBusinessAccountId ? this.wa_business_account_id : this.phone_number_id}/${path}`; 38 | } 39 | 40 | async networkResponse(method: httpMethod, data: Record | undefined, customUrl: string | undefined=undefined){ 41 | try{ 42 | let r 43 | 44 | if (method == httpMethod.POST){ 45 | r = await axios.post(customUrl || this.url, data, { 46 | headers: this.headers, 47 | }); 48 | }else if (method == httpMethod.GET){ 49 | r = await axios.get(customUrl || this.url, { 50 | headers: this.headers, 51 | }); 52 | }else if (method == httpMethod.PUT){ 53 | r = await axios.put(customUrl || this.url, data, { 54 | headers: this.headers, 55 | }); 56 | }else if (method == httpMethod.DELETE){ 57 | r = await axios.delete(customUrl || this.url, { 58 | headers: this.headers, 59 | }); 60 | }else{ 61 | throw new Error(`Invalid method: ${method}`); 62 | } 63 | 64 | if (this.debug){ 65 | return r; 66 | } 67 | 68 | 69 | return r.data; 70 | }catch(error){ 71 | console.error(error); 72 | throw error; 73 | } 74 | } 75 | 76 | async sendMessage( 77 | message: string, 78 | recipient_id: string, 79 | recipient_type = "individual", 80 | preview_url = true, 81 | ) { 82 | let data = { 83 | messaging_product: "whatsapp", 84 | recipient_type: recipient_type, 85 | to: recipient_id, 86 | type: "text", 87 | text: { preview_url: preview_url, body: message }, 88 | }; 89 | 90 | return await this.networkResponse(httpMethod.POST, data); 91 | } 92 | 93 | async getTemplates() { 94 | const customURL = this.buildUrl("message_templates", true); 95 | let templates = await this.networkResponse(httpMethod.GET, undefined, customURL); 96 | return templates.data || templates 97 | } 98 | 99 | async sendTemplate( 100 | template: string, 101 | recipient_id: string, 102 | components: any[] = [], 103 | lang: string = "en_US", 104 | ) { 105 | let data = { 106 | messaging_product: "whatsapp", 107 | to: recipient_id, 108 | type: "template", 109 | template: { 110 | name: template, 111 | language: { code: lang }, 112 | components: [...components], 113 | }, 114 | }; 115 | return await this.networkResponse(httpMethod.POST, data); 116 | } 117 | 118 | async sendLocation( 119 | lat: number, 120 | long: number, 121 | name: string, 122 | address: string, 123 | recipient_id: string, 124 | ) { 125 | let data = { 126 | messaging_product: "whatsapp", 127 | to: recipient_id, 128 | type: "location", 129 | location: { 130 | latitude: lat, 131 | longitude: long, 132 | name: name, 133 | address: address, 134 | }, 135 | }; 136 | 137 | return await this.networkResponse(httpMethod.POST, data); 138 | } 139 | 140 | async sendImage( 141 | image: any, 142 | recipient_id: string, 143 | recipient_type = "individual", 144 | caption: string | null = null, 145 | link: boolean = true, 146 | ) { 147 | let data: ImageMessage; 148 | if (link) { 149 | data = { 150 | messaging_product: "whatsapp", 151 | recipient_type: recipient_type, 152 | to: recipient_id, 153 | type: "image", 154 | image: { link: image, caption: caption }, 155 | }; 156 | } else { 157 | data = { 158 | messaging_product: "whatsapp", 159 | recipient_type: recipient_type, 160 | to: recipient_id, 161 | type: "image", 162 | image: { id: image, caption: caption }, 163 | }; 164 | } 165 | 166 | return await this.networkResponse(httpMethod.POST, data); 167 | } 168 | 169 | async sendAudio(audio: any, recipient_id: any, link = true) { 170 | let data: AudioMessage; 171 | if (link) { 172 | data = { 173 | messaging_product: "whatsapp", 174 | to: recipient_id, 175 | type: "audio", 176 | audio: { link: audio }, 177 | }; 178 | } else { 179 | data = { 180 | messaging_product: "whatsapp", 181 | to: recipient_id, 182 | type: "audio", 183 | audio: { id: audio }, 184 | }; 185 | } 186 | 187 | return await this.networkResponse(httpMethod.POST, data); 188 | } 189 | 190 | async sendVideo(video: any, recipient_id: any, caption = null, link = true) { 191 | let data:VideoMessage; 192 | if (link) { 193 | data = { 194 | messaging_product: "whatsapp", 195 | to: recipient_id, 196 | type: "video", 197 | video: { link: video, caption: caption }, 198 | }; 199 | } else { 200 | data = { 201 | messaging_product: "whatsapp", 202 | to: recipient_id, 203 | type: "video", 204 | video: { id: video, caption: caption }, 205 | }; 206 | } 207 | return await this.networkResponse(httpMethod.POST, data); 208 | } 209 | 210 | async sendDocument( 211 | document: any, 212 | recipient_id: any, 213 | filename: string = "document", 214 | caption = null, 215 | link = true, 216 | ) { 217 | let data:DocumentMessage; 218 | if (link) { 219 | data = { 220 | messaging_product: "whatsapp", 221 | to: recipient_id, 222 | type: "document", 223 | document: { link: document, caption: caption, filename: filename }, 224 | }; 225 | } else { 226 | data = { 227 | messaging_product: "whatsapp", 228 | to: recipient_id, 229 | type: "document", 230 | document: { id: document, caption: caption, filename: filename }, 231 | }; 232 | } 233 | 234 | return await this.networkResponse(httpMethod.POST, data); 235 | } 236 | 237 | createButton(button: Record) { 238 | // TODO: Investigate 239 | return { 240 | type: "list", 241 | header: { type: "text", text: button["header"] }, 242 | body: { text: button["body"] }, 243 | footer: { text: button["footer"] }, 244 | action: button["action"], 245 | }; 246 | } 247 | 248 | async sendButton(button: Record, recipient_id: string) { 249 | let data = { 250 | messaging_product: "whatsapp", 251 | to: recipient_id, 252 | type: "interactive", 253 | interactive: this.createButton(button), 254 | }; 255 | return await this.networkResponse(httpMethod.POST, data); 256 | } 257 | 258 | async getMedia(id: string | number) { 259 | let mediaUrl = `https://graph.facebook.com/${VERSION}/${id}`; 260 | 261 | 262 | return await this.networkResponse(httpMethod.GET, undefined, mediaUrl); 263 | } 264 | 265 | async deleteMedia(id: string | number) { 266 | let mediaUrl = `https://graph.facebook.com/${VERSION}/${id}`; 267 | 268 | return await this.networkResponse(httpMethod.DELETE, undefined, mediaUrl); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # WhatsApp [Whatsapp Cloud API](https://npmjs.com/package/@phoscoder/whatsapp-cloud-api) 3 | 4 | [![Made in Zimbabwe🇿🇼](https://img.shields.io/badge/ported%20in-zimbabwe%20%F0%9F%87%BF%F0%9F%87%BC-blue)](https://github.com/phoscoder) 5 | ![NPM](https://img.shields.io/npm/l/@phoscoder/whatsapp-cloud-api) 6 | ![npm](https://img.shields.io/npm/v/@phoscoder/whatsapp-cloud-api) 7 | ![npm](https://img.shields.io/npm/dw/@phoscoder/whatsapp-cloud-api) 8 | 9 | 10 | Unofficial Javascript wrapper to [WhatsApp Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api). It was first ported from [heyoo](https://github.com/Neurotech-HQ/heyoo) but now I will be maintaining it and adding more features. 11 | 12 | ## Features supported 13 | 14 | ✅ Sending messages
15 | ✅ Sending Media (images, audio, video and documents)
16 | ✅ Sending location
17 | ✅ Sending interactive buttons
18 | ✅ Sending template messages
19 | ✅ Get templates
20 | 21 | ## Getting started 22 | 23 | To get started with **whatsapp-cloud-api**, you have to firstly install the libary either directly or using *npm*. 24 | 25 | ### Installing from npm 26 | 27 | ```bash 28 | # For Windows, Linux & Mac 29 | 30 | npm install @phoscoder/whatsapp-cloud-api 31 | ``` 32 | 33 | ## Setting up 34 | 35 | To get started using this package, you will need `**Access Token**`, `**Phone Number ID**` and `**Whatsapp Business Account ID**` which you can get by from [Facebook Developer Portal](https://developers.facebook.com/) 36 | 37 | Here are steps to follow for you to get started 38 | 39 | 1. [Go to your apps](https://developers.facebook.com/apps) 40 | 2. [create an app](https://developers.facebook.com/apps/create/) 41 | 3. Select Bussiness >> Bussiness 42 | 4. It will prompt you to enter basic app informations 43 | 5. It will ask you to add products to your app 44 | a. Add WhatsApp Messenger 45 | 6. Right there you will see a your **Access Token**, **Whatsapp Business Account ID** and **Phone Number ID** and its phone_number_id 46 | 7. Lastly verify the number you will be using for testing on the **To** field. 47 | 48 | Once you're follow the above procedures, now you're ready to start hacking with the Wrapper. 49 | 50 | ## Authentication 51 | 52 | Here how you authenticate your application, you need to specify two things the ``, `` and `` of your test number 53 | 54 | ```javascript 55 | import {WhatsApp} from '@phoscoder/whatsapp-cloud-api' 56 | let messenger = new WhatsApp( 57 | '', 58 | '', 59 | '' 60 | ) 61 | ``` 62 | 63 | Once you have authenticated your app, now you can start using the above mentioned feature as shown above; 64 | 65 | ## Sending Messanges 66 | 67 | Here how to send messages; 68 | 69 | ```javscript 70 | messenger.sendMessage('Your message ', 'Mobile eg: 263757xxxxx') 71 | ``` 72 | 73 | ### Example 74 | 75 | Here an example 76 | 77 | ```javascript 78 | messenger.sendMessage('Hi there just testiing', '263757902132') 79 | ``` 80 | 81 | ## Sending Images 82 | 83 | When sending media(image, video, audio, gif and document ), you can either specify a link containing the media or specify object id, you can do this using the same method. 84 | 85 | By default all media methods assume you're sending link containing media but you can change this by specifying the ```link=False```. 86 | 87 | Here an example; 88 | 89 | ```javascript 90 | messenger.sendImage( 91 | "https://i.imgur.com/Fh7XVYY.jpeg", 92 | "263757xxxxxx", 93 | ) 94 | ``` 95 | 96 | ## Sending Video 97 | 98 | Here an example; 99 | 100 | ```javascript 101 | 102 | messenger.sendVideo( 103 | "https://www.youtube.com/watch?v=K4TOrB7at0Y", 104 | "263757xxxxxx", 105 | ) 106 | ``` 107 | 108 | ## Sending Audio 109 | 110 | Here an example; 111 | 112 | ```javascript 113 | messenger.sendAudio( 114 | "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", 115 | "263757xxxxxx", 116 | ) 117 | ``` 118 | 119 | ## Sending Document 120 | 121 | Here an example; 122 | 123 | ```javascript 124 | messenger.sendDocument( 125 | "http://www.africau.edu/images/default/sample.pdf", 126 | "263757xxxxxx", // Recipient ID 127 | "sample.pdf", // File Name 128 | "Sample Document" // Caption 129 | ) 130 | ``` 131 | 132 | ## Sending Location 133 | 134 | Here an example; 135 | 136 | ```javascript 137 | messenger.sendLocation( 138 | lat=1.29, 139 | long=103.85, 140 | name="Singapore", 141 | address="Singapore", 142 | recipient_id="263757xxxxxx", 143 | ) 144 | ``` 145 | 146 | ## Sending Interactive buttons 147 | 148 | Here an example; 149 | 150 | ```javascript 151 | 152 | const button = { 153 | "header": "Header Testing", 154 | "body": "Body Testing", 155 | "footer": "Footer Testing", 156 | "action": { 157 | "button": "Button Testing", 158 | "sections": [ 159 | { 160 | "title": "iBank", 161 | "rows": [ 162 | {"id": "row 1", "title": "Send Money", "description": ""}, 163 | { 164 | "id": "row 2", 165 | "title": "Withdraw money", 166 | "description": "", 167 | }, 168 | ], 169 | } 170 | ], 171 | }, 172 | } 173 | 174 | messenger.sendButton( 175 | button, 176 | "263757xxxxxx" 177 | ) 178 | ``` 179 | 180 | ## Sending a Template Messages 181 | 182 | Here how to send a pre-approved template message; 183 | 184 | ```javascript 185 | messenger.sendTemplate("hello_world", "263757xxxxxx") 186 | ``` 187 | 188 | ## Sending a Template Messages with Components 189 | You can now specify components like this 190 | 191 | ```javascript 192 | let components = [ 193 | // Your components here 194 | ] 195 | 196 | messenger.sendTemplate("hello_world", "263757xxxxxx", components) 197 | ``` 198 | 199 | For moreabout components: https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-message-templates 200 | 201 | ## Webhook 202 | 203 | Webhooks are useful incase you're wondering how to respond to incoming message send by user, but I have created a [starter webhook](https://github.com/phoscoder/whatsapp-cloud-api/blob/main/src/hook.ts) which you can then customize it according to your own plans. 204 | 205 | To learn more about webhook and how to configure in your Facebook developer dashboard please [have a look here](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks). 206 | 207 | 208 | 209 | ### Notification Payload Structure 210 | 211 | This is the structure of the notifications that you will recieve from Whatsapp when a certain event is triggered 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 245 | 250 | 251 |
Example Notification Payload Nested Structure of the Payload
220 | 221 | ```bash 222 | { 223 | "object": "whatsapp_business_account", 224 | "entry": [{ 225 | "id": "WHATSAPP-BUSINESS-ACCOUNT-ID", 226 | "changes": [{ 227 | "value": { 228 | "messaging_product": "whatsapp", 229 | "metadata": { 230 | "display_phone_number": "PHONE-NUMBER", 231 | "phone_number_id": "PHONE-NUMBER-ID" 232 | }, 233 | # Additional arrays and objects 234 | "contacts": [{...}] 235 | "errors": [{...}] 236 | "messages": [{...}] 237 | "statuses": [{...}] 238 | }, 239 | "field": "messages" 240 | }] 241 | }] 242 | } 243 | ``` 244 | 246 | 247 | 248 | 249 |
252 | 253 | 254 | 255 | ### Recieving notifications (Method #1 > Inbuilt server) 256 | To receive notifications such as customer messages, alerts and other callbacks from WhatsApp you can start a server that listens and handles notifications from Whatsapp 257 | 258 | ```javascript 259 | import { Server } from '@phoscoder/whatsapp-cloud-api' 260 | import 'dotenv/config' 261 | 262 | let notificationsServer = new Server( 263 | process.env.VERIFY_TOKEN, 264 | process.env.LISTEN_PORT, 265 | ) 266 | 267 | let app = notificationsServer.start(async (rawData ,processedPayload) => { 268 | // Do your stuff here 269 | 270 | if (processedPayload.type == "messages") { 271 | let messages = processedPayload.getMessages() 272 | let metadata = processedPayload.getMetadata() 273 | let contacts = processedPayload.getContacts() 274 | 275 | if (processedPayload.type == "contacts") { 276 | let contacts = processedPayload.getContacts() 277 | 278 | if (processedPayload.type == "status") { 279 | let status = processedPayload.getStatuses() 280 | 281 | // Do other stuff here 282 | }) 283 | ``` 284 | 285 | `rawData` -> This is raw data straight from WhatsApp 286 | `processedPayload` -> This is an object of `ProcessPayload` it gives access to the raw_data plus helper methods 287 | 288 | > [!NOTE] 289 | > Beginners should work more with processed since it saves you time and minimizes errors 290 | 291 | **Tip:** You can refactor it to look more presentable: 292 | 293 | ```javascript 294 | import handleNotifications from 'path/to/file' 295 | 296 | let app = notificationServer.start(handleNotifications) 297 | ``` 298 | ### Recieving notifications (Method #2 > Existing server) 299 | To receive notifications such as customer messages, alerts and other callbacks from WhatsApp on an existing server, take the following steps 300 | 301 | ```javascript 302 | import { 303 | Server, 304 | NotificationPayload, 305 | ProcessPayload, 306 | VerifyWebhookToken } from '@phoscoder/whatsapp-cloud-api' 307 | 308 | import dotenv from 'dotenv' 309 | dotenv.config() 310 | 311 | const VERIFY_TOKEN = process.env.VERIFY_TOKEN 312 | 313 | // For webhook verification 314 | app.get("/", async (req: Request, res:Response) => { 315 | const server = new Server(VERIFY_TOKEN) 316 | let res = server.verifyWebhookToken(req.query as Record) 317 | res.send(res) 318 | }) 319 | 320 | // For incoming notifications 321 | app.post("/", async (req: Request, res:Response) => { 322 | let data: NotificationPayload = req.body 323 | 324 | let processedPayload = new ProcessPayload(data) 325 | 326 | // Do your stuff here 327 | if (processedPayload.type == "messages") { 328 | let messages = processedPayload.getMessages() 329 | let metadata = processedPayload.getMetadata() 330 | let contacts = processedPayload.getContacts() 331 | 332 | if (processedPayload.type == "contacts") { 333 | let contacts = processedPayload.getContacts() 334 | 335 | if (processedPayload.type == "status") { 336 | let status = processedPayload.getStatuses() 337 | 338 | return res.json("Notification recieved!") 339 | }) 340 | ``` 341 | 342 | 343 | ### Getting media links 344 | 345 | To retrive actual media link 346 | 347 | ```javascript 348 | let message = processedPayload.getMessages()[0] 349 | let mediaData = await messenger.getMedia(message.image.id) 350 | ``` 351 | 352 | > [!NOTE] 353 | > The URL you get is only available for a 5 minutes, so you may need to download it and store it somewhere, or use it as quick as possible 354 | 355 | 356 | For more info check [Notification Payload Reference](https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/components) and [Notification Payload Examples](https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/payload-examples) 357 | 358 | ## Debugging 359 | 360 | In cases where you want to see the full response and all its data set debug to true like this 361 | 362 | ```js 363 | import { WhatsApp } from '@phoscoder/whatsapp_cloud_api' 364 | ... 365 | ... 366 | ... 367 | const client = new WhatsApp("", "", true) 368 | ``` 369 | 370 | OR this 371 | 372 | ```js 373 | client.debug = true 374 | ``` 375 | 376 | 377 | ## Issues 378 | 379 | If you will face any issue with the usage of this package please raise one so as we can quickly fix it as soon as possible; 380 | 381 | ## Contributing 382 | 383 | This is an opensource project under ```MIT License``` so any one is welcome to contribute from typo, to source code to documentation, ```JUST FORK IT```. 384 | 385 | ## All the credit 386 | 387 | 1. [kalebu](https://github.com/Kalebu) 388 | 2. [takunda](https://github.com/takumade) 389 | 3. Contribute to get added here 390 | -------------------------------------------------------------------------------- /__tests__/notifications.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import express from 'express'; 3 | import supertest from 'supertest' 4 | import app from '../src/hook' 5 | 6 | 7 | describe("Test Message Notifications", () => { 8 | 9 | test("Responds to Text Message Notification", async () => { 10 | await supertest(app) 11 | .post("/") 12 | .send({ 13 | "object": "whatsapp_business_account", 14 | "entry": [{ 15 | "id": "101404072591160", 16 | "changes": [{ 17 | "value": { 18 | "messaging_product": "whatsapp", 19 | "metadata": { 20 | "display_phone_number": "26377851561", 21 | "phone_number_id": "101404072591160" 22 | }, 23 | "contacts": [{ 24 | "profile": { 25 | "name": "John Lee" 26 | }, 27 | "wa_id": "101404072591160" 28 | }], 29 | "messages": [{ 30 | "from": "10290191920", 31 | "id": "wamid.ID", 32 | "timestamp": "timestamp", 33 | "text": { 34 | "body": "Hello lets take some drinks" 35 | }, 36 | "type": "text" 37 | }] 38 | }, 39 | "field": "messages" 40 | }] 41 | }] 42 | }) 43 | .expect(200) 44 | .then(async (res: { body: { from: any; type: any; text: any; }; }) => { 45 | expect(res.body.from).toEqual('10290191920') 46 | expect(res.body.type).toEqual('text') 47 | expect(res.body.text.body).toEqual('Hello lets take some drinks') 48 | 49 | }) 50 | }) 51 | 52 | 53 | test("Responds to Image Message Notification", async () => { 54 | await supertest(app) 55 | .post("/") 56 | .send({ 57 | "object": "whatsapp_business_account", 58 | "entry": [{ 59 | "id": "101404072591160", 60 | "changes": [{ 61 | "value": { 62 | "messaging_product": "whatsapp", 63 | "metadata": { 64 | "display_phone_number": "26377851561", 65 | "phone_number_id": "101404072591160" 66 | }, 67 | "contacts": [{ 68 | "profile": { 69 | "name": "John Lee" 70 | }, 71 | "wa_id": "101404072591160" 72 | }], 73 | "messages": [{ 74 | "from": "10290191920", 75 | "id": "wamid.ID", 76 | "timestamp": "timestamp", 77 | "type": "image", 78 | "image": { 79 | "caption": "Cat in the fridge", 80 | "mime_type": "image/jpeg", 81 | "sha256": "IMAGE_HASH", 82 | "id": "ID" 83 | } 84 | }] 85 | }, 86 | "field": "messages" 87 | }] 88 | }] 89 | }) 90 | .expect(200) 91 | .then(async (res: { body: { from: any; type: any; image: any; }; }) => { 92 | expect(res.body.from).toEqual('10290191920') 93 | expect(res.body.type).toEqual('image') 94 | expect(res.body.image.caption).toEqual('Cat in the fridge') 95 | 96 | }) 97 | }) 98 | 99 | test("Responds to Image Message Notification", async () => { 100 | await supertest(app) 101 | .post("/") 102 | .send({ 103 | "object": "whatsapp_business_account", 104 | "entry": [{ 105 | "id": "101404072591160", 106 | "changes": [{ 107 | "value": { 108 | "messaging_product": "whatsapp", 109 | "metadata": { 110 | "display_phone_number": "26377851561", 111 | "phone_number_id": "101404072591160" 112 | }, 113 | "contacts": [{ 114 | "profile": { 115 | "name": "John Lee" 116 | }, 117 | "wa_id": "101404072591160" 118 | }], 119 | "messages": [{ 120 | "from": "10290191920", 121 | "id": "wamid.ID", 122 | "timestamp": "timestamp", 123 | "type": "sticker", 124 | "sticker": { 125 | "mime_type": "image/webp", 126 | "sha256": "HASH", 127 | "id": "ID" 128 | } 129 | }] 130 | }, 131 | "field": "messages" 132 | }] 133 | }] 134 | }) 135 | .expect(200) 136 | .then(async (res: { body: { from: any; type: any; sticker: any; }; }) => { 137 | expect(res.body.from).toEqual('10290191920') 138 | expect(res.body.type).toEqual('sticker') 139 | expect(res.body.sticker.sha256).toEqual('HASH') 140 | 141 | }) 142 | }) 143 | test("Responds to Location Message Notification", async () => { 144 | await supertest(app) 145 | .post("/") 146 | .send({ 147 | "object": "whatsapp_business_account", 148 | "entry": [{ 149 | "id": "101404072591160", 150 | "changes": [{ 151 | "value": { 152 | "messaging_product": "whatsapp", 153 | "metadata": { 154 | "display_phone_number": "26377851561", 155 | "phone_number_id": "101404072591160" 156 | }, 157 | "contacts": [{ 158 | "profile": { 159 | "name": "John Lee" 160 | }, 161 | "wa_id": "101404072591160" 162 | }], 163 | "messages": [{ 164 | "from": "10290191920", 165 | "id": "wamid.ID", 166 | "timestamp": "timestamp", 167 | "type": "image", 168 | "location": { 169 | "latitude": 1.29, 170 | "longitude":103.85, 171 | "name":"Singapore", 172 | "address":"Singapore" 173 | } 174 | }] 175 | }, 176 | "field": "messages" 177 | }] 178 | }] 179 | }) 180 | .expect(200) 181 | .then(async (res: { body: { from: any; type: any; location: any; }; }) => { 182 | expect(res.body.from).toEqual('10290191920') 183 | expect(res.body.type).toEqual('location') 184 | expect(res.body.location.name).toEqual('Singapore') 185 | expect(res.body.location.longitude).toEqual(103.85) 186 | 187 | }) 188 | }) 189 | 190 | test("Responds to Contacts Message Notification", async () => { 191 | await supertest(app) 192 | .post("/") 193 | .send({ 194 | "object": "whatsapp_business_account", 195 | "entry": [{ 196 | "id": "101404072591160", 197 | "changes": [{ 198 | "value": { 199 | "messaging_product": "whatsapp", 200 | "metadata": { 201 | "display_phone_number": "26377851561", 202 | "phone_number_id": "101404072591160" 203 | }, 204 | "contacts": [{ 205 | "profile": { 206 | "name": "John Lee" 207 | }, 208 | "wa_id": "101404072591160" 209 | }], 210 | "messages": [{ 211 | "from": "10290191920", 212 | "id": "wamid.ID", 213 | "timestamp": "timestamp", 214 | "type": "image", 215 | "contacts":[{ 216 | "addresses":[{ 217 | "city":"CONTACT_CITY", 218 | "country":"CONTACT_COUNTRY", 219 | "country_code":"CONTACT_COUNTRY_CODE", 220 | "state":"CONTACT_STATE", 221 | "street":"CONTACT_STREET", 222 | "type":"HOME or WORK", 223 | "zip":"CONTACT_ZIP" 224 | }], 225 | "birthday":"CONTACT_BIRTHDAY", 226 | "emails":[{ 227 | "email":"CONTACT_EMAIL", 228 | "type":"WORK or HOME" 229 | }], 230 | "name":{ 231 | "formatted_name":"CONTACT_FORMATTED_NAME", 232 | "first_name":"CONTACT_FIRST_NAME", 233 | "last_name":"CONTACT_LAST_NAME", 234 | "middle_name":"CONTACT_MIDDLE_NAME", 235 | "suffix":"CONTACT_SUFFIX", 236 | "prefix":"CONTACT_PREFIX" 237 | }, 238 | "org":{ 239 | "company":"CONTACT_ORG_COMPANY", 240 | "department":"CONTACT_ORG_DEPARTMENT", 241 | "title":"CONTACT_ORG_TITLE" 242 | }, 243 | "phones":[{ 244 | "phone":"CONTACT_PHONE", 245 | "wa_id":"CONTACT_WA_ID", 246 | "type":"HOME or WORK>" 247 | }], 248 | "urls":[{ 249 | "url":"CONTACT_URL", 250 | "type":"HOME or WORK" 251 | }] 252 | }] 253 | }] 254 | }, 255 | "field": "messages" 256 | }] 257 | }] 258 | }) 259 | .expect(200) 260 | .then(async (res: { body: { from: any; type: any; contacts: any; }; }) => { 261 | expect(res.body.from).toEqual('10290191920') 262 | expect(res.body.type).toEqual('contacts') 263 | expect(Object.keys(res.body.contacts[0]).includes("phones")).toBe(true) 264 | expect(Object.keys(res.body.contacts[0]).includes("addresses")).toBe(true) 265 | expect(Object.keys(res.body.contacts[0]).includes("urls")).toBe(true) 266 | 267 | }) 268 | }) 269 | }) 270 | 271 | 272 | -------------------------------------------------------------------------------- /examples/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatsapp-cloud-api-examples", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "whatsapp-cloud-api-examples", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@phoscoder/whatsapp-cloud-api": "^1.4.5", 13 | "dotenv": "^16.4.7" 14 | } 15 | }, 16 | "node_modules/@phoscoder/whatsapp-cloud-api": { 17 | "version": "1.4.5", 18 | "resolved": "https://registry.npmjs.org/@phoscoder/whatsapp-cloud-api/-/whatsapp-cloud-api-1.4.5.tgz", 19 | "integrity": "sha512-aG6q46YsYSFHt/sbjPkN4knxjOtfKXFp+455Rq+kj2HVXMkeD0O+w4e7B24avrRIbjLiPRZxNyW7o0XQnMQyDA==", 20 | "license": "MIT", 21 | "dependencies": { 22 | "axios": "^1.13.2", 23 | "body-parser": "^1.20.0", 24 | "dotenv": "^16.0.1", 25 | "express": "^4.18.1" 26 | } 27 | }, 28 | "node_modules/accepts": { 29 | "version": "1.3.8", 30 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 31 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 32 | "license": "MIT", 33 | "dependencies": { 34 | "mime-types": "~2.1.34", 35 | "negotiator": "0.6.3" 36 | }, 37 | "engines": { 38 | "node": ">= 0.6" 39 | } 40 | }, 41 | "node_modules/array-flatten": { 42 | "version": "1.1.1", 43 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 44 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", 45 | "license": "MIT" 46 | }, 47 | "node_modules/asynckit": { 48 | "version": "0.4.0", 49 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 50 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 51 | "license": "MIT" 52 | }, 53 | "node_modules/axios": { 54 | "version": "1.13.2", 55 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", 56 | "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", 57 | "license": "MIT", 58 | "dependencies": { 59 | "follow-redirects": "^1.15.6", 60 | "form-data": "^4.0.4", 61 | "proxy-from-env": "^1.1.0" 62 | } 63 | }, 64 | "node_modules/body-parser": { 65 | "version": "1.20.4", 66 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", 67 | "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", 68 | "license": "MIT", 69 | "dependencies": { 70 | "bytes": "~3.1.2", 71 | "content-type": "~1.0.5", 72 | "debug": "2.6.9", 73 | "depd": "2.0.0", 74 | "destroy": "~1.2.0", 75 | "http-errors": "~2.0.1", 76 | "iconv-lite": "~0.4.24", 77 | "on-finished": "~2.4.1", 78 | "qs": "~6.14.0", 79 | "raw-body": "~2.5.3", 80 | "type-is": "~1.6.18", 81 | "unpipe": "~1.0.0" 82 | }, 83 | "engines": { 84 | "node": ">= 0.8", 85 | "npm": "1.2.8000 || >= 1.4.16" 86 | } 87 | }, 88 | "node_modules/bytes": { 89 | "version": "3.1.2", 90 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 91 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 92 | "license": "MIT", 93 | "engines": { 94 | "node": ">= 0.8" 95 | } 96 | }, 97 | "node_modules/call-bind-apply-helpers": { 98 | "version": "1.0.2", 99 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 100 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 101 | "license": "MIT", 102 | "dependencies": { 103 | "es-errors": "^1.3.0", 104 | "function-bind": "^1.1.2" 105 | }, 106 | "engines": { 107 | "node": ">= 0.4" 108 | } 109 | }, 110 | "node_modules/call-bound": { 111 | "version": "1.0.4", 112 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 113 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 114 | "license": "MIT", 115 | "dependencies": { 116 | "call-bind-apply-helpers": "^1.0.2", 117 | "get-intrinsic": "^1.3.0" 118 | }, 119 | "engines": { 120 | "node": ">= 0.4" 121 | }, 122 | "funding": { 123 | "url": "https://github.com/sponsors/ljharb" 124 | } 125 | }, 126 | "node_modules/combined-stream": { 127 | "version": "1.0.8", 128 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 129 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 130 | "license": "MIT", 131 | "dependencies": { 132 | "delayed-stream": "~1.0.0" 133 | }, 134 | "engines": { 135 | "node": ">= 0.8" 136 | } 137 | }, 138 | "node_modules/content-disposition": { 139 | "version": "0.5.4", 140 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 141 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 142 | "license": "MIT", 143 | "dependencies": { 144 | "safe-buffer": "5.2.1" 145 | }, 146 | "engines": { 147 | "node": ">= 0.6" 148 | } 149 | }, 150 | "node_modules/content-type": { 151 | "version": "1.0.5", 152 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 153 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 154 | "license": "MIT", 155 | "engines": { 156 | "node": ">= 0.6" 157 | } 158 | }, 159 | "node_modules/cookie": { 160 | "version": "0.7.2", 161 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 162 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 163 | "license": "MIT", 164 | "engines": { 165 | "node": ">= 0.6" 166 | } 167 | }, 168 | "node_modules/cookie-signature": { 169 | "version": "1.0.7", 170 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", 171 | "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", 172 | "license": "MIT" 173 | }, 174 | "node_modules/debug": { 175 | "version": "2.6.9", 176 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 177 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 178 | "license": "MIT", 179 | "dependencies": { 180 | "ms": "2.0.0" 181 | } 182 | }, 183 | "node_modules/delayed-stream": { 184 | "version": "1.0.0", 185 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 186 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 187 | "license": "MIT", 188 | "engines": { 189 | "node": ">=0.4.0" 190 | } 191 | }, 192 | "node_modules/depd": { 193 | "version": "2.0.0", 194 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 195 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 196 | "license": "MIT", 197 | "engines": { 198 | "node": ">= 0.8" 199 | } 200 | }, 201 | "node_modules/destroy": { 202 | "version": "1.2.0", 203 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 204 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 205 | "license": "MIT", 206 | "engines": { 207 | "node": ">= 0.8", 208 | "npm": "1.2.8000 || >= 1.4.16" 209 | } 210 | }, 211 | "node_modules/dotenv": { 212 | "version": "16.6.1", 213 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", 214 | "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", 215 | "license": "BSD-2-Clause", 216 | "engines": { 217 | "node": ">=12" 218 | }, 219 | "funding": { 220 | "url": "https://dotenvx.com" 221 | } 222 | }, 223 | "node_modules/dunder-proto": { 224 | "version": "1.0.1", 225 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 226 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 227 | "license": "MIT", 228 | "dependencies": { 229 | "call-bind-apply-helpers": "^1.0.1", 230 | "es-errors": "^1.3.0", 231 | "gopd": "^1.2.0" 232 | }, 233 | "engines": { 234 | "node": ">= 0.4" 235 | } 236 | }, 237 | "node_modules/ee-first": { 238 | "version": "1.1.1", 239 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 240 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 241 | "license": "MIT" 242 | }, 243 | "node_modules/encodeurl": { 244 | "version": "2.0.0", 245 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 246 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 247 | "license": "MIT", 248 | "engines": { 249 | "node": ">= 0.8" 250 | } 251 | }, 252 | "node_modules/es-define-property": { 253 | "version": "1.0.1", 254 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 255 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 256 | "license": "MIT", 257 | "engines": { 258 | "node": ">= 0.4" 259 | } 260 | }, 261 | "node_modules/es-errors": { 262 | "version": "1.3.0", 263 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 264 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 265 | "license": "MIT", 266 | "engines": { 267 | "node": ">= 0.4" 268 | } 269 | }, 270 | "node_modules/es-object-atoms": { 271 | "version": "1.1.1", 272 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 273 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 274 | "license": "MIT", 275 | "dependencies": { 276 | "es-errors": "^1.3.0" 277 | }, 278 | "engines": { 279 | "node": ">= 0.4" 280 | } 281 | }, 282 | "node_modules/es-set-tostringtag": { 283 | "version": "2.1.0", 284 | "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 285 | "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 286 | "license": "MIT", 287 | "dependencies": { 288 | "es-errors": "^1.3.0", 289 | "get-intrinsic": "^1.2.6", 290 | "has-tostringtag": "^1.0.2", 291 | "hasown": "^2.0.2" 292 | }, 293 | "engines": { 294 | "node": ">= 0.4" 295 | } 296 | }, 297 | "node_modules/escape-html": { 298 | "version": "1.0.3", 299 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 300 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 301 | "license": "MIT" 302 | }, 303 | "node_modules/etag": { 304 | "version": "1.8.1", 305 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 306 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 307 | "license": "MIT", 308 | "engines": { 309 | "node": ">= 0.6" 310 | } 311 | }, 312 | "node_modules/express": { 313 | "version": "4.22.1", 314 | "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", 315 | "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", 316 | "license": "MIT", 317 | "dependencies": { 318 | "accepts": "~1.3.8", 319 | "array-flatten": "1.1.1", 320 | "body-parser": "~1.20.3", 321 | "content-disposition": "~0.5.4", 322 | "content-type": "~1.0.4", 323 | "cookie": "~0.7.1", 324 | "cookie-signature": "~1.0.6", 325 | "debug": "2.6.9", 326 | "depd": "2.0.0", 327 | "encodeurl": "~2.0.0", 328 | "escape-html": "~1.0.3", 329 | "etag": "~1.8.1", 330 | "finalhandler": "~1.3.1", 331 | "fresh": "~0.5.2", 332 | "http-errors": "~2.0.0", 333 | "merge-descriptors": "1.0.3", 334 | "methods": "~1.1.2", 335 | "on-finished": "~2.4.1", 336 | "parseurl": "~1.3.3", 337 | "path-to-regexp": "~0.1.12", 338 | "proxy-addr": "~2.0.7", 339 | "qs": "~6.14.0", 340 | "range-parser": "~1.2.1", 341 | "safe-buffer": "5.2.1", 342 | "send": "~0.19.0", 343 | "serve-static": "~1.16.2", 344 | "setprototypeof": "1.2.0", 345 | "statuses": "~2.0.1", 346 | "type-is": "~1.6.18", 347 | "utils-merge": "1.0.1", 348 | "vary": "~1.1.2" 349 | }, 350 | "engines": { 351 | "node": ">= 0.10.0" 352 | }, 353 | "funding": { 354 | "type": "opencollective", 355 | "url": "https://opencollective.com/express" 356 | } 357 | }, 358 | "node_modules/finalhandler": { 359 | "version": "1.3.2", 360 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", 361 | "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", 362 | "license": "MIT", 363 | "dependencies": { 364 | "debug": "2.6.9", 365 | "encodeurl": "~2.0.0", 366 | "escape-html": "~1.0.3", 367 | "on-finished": "~2.4.1", 368 | "parseurl": "~1.3.3", 369 | "statuses": "~2.0.2", 370 | "unpipe": "~1.0.0" 371 | }, 372 | "engines": { 373 | "node": ">= 0.8" 374 | } 375 | }, 376 | "node_modules/follow-redirects": { 377 | "version": "1.15.11", 378 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", 379 | "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", 380 | "funding": [ 381 | { 382 | "type": "individual", 383 | "url": "https://github.com/sponsors/RubenVerborgh" 384 | } 385 | ], 386 | "license": "MIT", 387 | "engines": { 388 | "node": ">=4.0" 389 | }, 390 | "peerDependenciesMeta": { 391 | "debug": { 392 | "optional": true 393 | } 394 | } 395 | }, 396 | "node_modules/form-data": { 397 | "version": "4.0.5", 398 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", 399 | "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", 400 | "license": "MIT", 401 | "dependencies": { 402 | "asynckit": "^0.4.0", 403 | "combined-stream": "^1.0.8", 404 | "es-set-tostringtag": "^2.1.0", 405 | "hasown": "^2.0.2", 406 | "mime-types": "^2.1.12" 407 | }, 408 | "engines": { 409 | "node": ">= 6" 410 | } 411 | }, 412 | "node_modules/forwarded": { 413 | "version": "0.2.0", 414 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 415 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 416 | "license": "MIT", 417 | "engines": { 418 | "node": ">= 0.6" 419 | } 420 | }, 421 | "node_modules/fresh": { 422 | "version": "0.5.2", 423 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 424 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 425 | "license": "MIT", 426 | "engines": { 427 | "node": ">= 0.6" 428 | } 429 | }, 430 | "node_modules/function-bind": { 431 | "version": "1.1.2", 432 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 433 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 434 | "license": "MIT", 435 | "funding": { 436 | "url": "https://github.com/sponsors/ljharb" 437 | } 438 | }, 439 | "node_modules/get-intrinsic": { 440 | "version": "1.3.0", 441 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 442 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 443 | "license": "MIT", 444 | "dependencies": { 445 | "call-bind-apply-helpers": "^1.0.2", 446 | "es-define-property": "^1.0.1", 447 | "es-errors": "^1.3.0", 448 | "es-object-atoms": "^1.1.1", 449 | "function-bind": "^1.1.2", 450 | "get-proto": "^1.0.1", 451 | "gopd": "^1.2.0", 452 | "has-symbols": "^1.1.0", 453 | "hasown": "^2.0.2", 454 | "math-intrinsics": "^1.1.0" 455 | }, 456 | "engines": { 457 | "node": ">= 0.4" 458 | }, 459 | "funding": { 460 | "url": "https://github.com/sponsors/ljharb" 461 | } 462 | }, 463 | "node_modules/get-proto": { 464 | "version": "1.0.1", 465 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 466 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 467 | "license": "MIT", 468 | "dependencies": { 469 | "dunder-proto": "^1.0.1", 470 | "es-object-atoms": "^1.0.0" 471 | }, 472 | "engines": { 473 | "node": ">= 0.4" 474 | } 475 | }, 476 | "node_modules/gopd": { 477 | "version": "1.2.0", 478 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 479 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 480 | "license": "MIT", 481 | "engines": { 482 | "node": ">= 0.4" 483 | }, 484 | "funding": { 485 | "url": "https://github.com/sponsors/ljharb" 486 | } 487 | }, 488 | "node_modules/has-symbols": { 489 | "version": "1.1.0", 490 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 491 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 492 | "license": "MIT", 493 | "engines": { 494 | "node": ">= 0.4" 495 | }, 496 | "funding": { 497 | "url": "https://github.com/sponsors/ljharb" 498 | } 499 | }, 500 | "node_modules/has-tostringtag": { 501 | "version": "1.0.2", 502 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 503 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 504 | "license": "MIT", 505 | "dependencies": { 506 | "has-symbols": "^1.0.3" 507 | }, 508 | "engines": { 509 | "node": ">= 0.4" 510 | }, 511 | "funding": { 512 | "url": "https://github.com/sponsors/ljharb" 513 | } 514 | }, 515 | "node_modules/hasown": { 516 | "version": "2.0.2", 517 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 518 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 519 | "license": "MIT", 520 | "dependencies": { 521 | "function-bind": "^1.1.2" 522 | }, 523 | "engines": { 524 | "node": ">= 0.4" 525 | } 526 | }, 527 | "node_modules/http-errors": { 528 | "version": "2.0.1", 529 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", 530 | "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", 531 | "license": "MIT", 532 | "dependencies": { 533 | "depd": "~2.0.0", 534 | "inherits": "~2.0.4", 535 | "setprototypeof": "~1.2.0", 536 | "statuses": "~2.0.2", 537 | "toidentifier": "~1.0.1" 538 | }, 539 | "engines": { 540 | "node": ">= 0.8" 541 | }, 542 | "funding": { 543 | "type": "opencollective", 544 | "url": "https://opencollective.com/express" 545 | } 546 | }, 547 | "node_modules/iconv-lite": { 548 | "version": "0.4.24", 549 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 550 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 551 | "license": "MIT", 552 | "dependencies": { 553 | "safer-buffer": ">= 2.1.2 < 3" 554 | }, 555 | "engines": { 556 | "node": ">=0.10.0" 557 | } 558 | }, 559 | "node_modules/inherits": { 560 | "version": "2.0.4", 561 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 562 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 563 | "license": "ISC" 564 | }, 565 | "node_modules/ipaddr.js": { 566 | "version": "1.9.1", 567 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 568 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 569 | "license": "MIT", 570 | "engines": { 571 | "node": ">= 0.10" 572 | } 573 | }, 574 | "node_modules/math-intrinsics": { 575 | "version": "1.1.0", 576 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 577 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 578 | "license": "MIT", 579 | "engines": { 580 | "node": ">= 0.4" 581 | } 582 | }, 583 | "node_modules/media-typer": { 584 | "version": "0.3.0", 585 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 586 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 587 | "license": "MIT", 588 | "engines": { 589 | "node": ">= 0.6" 590 | } 591 | }, 592 | "node_modules/merge-descriptors": { 593 | "version": "1.0.3", 594 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 595 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 596 | "license": "MIT", 597 | "funding": { 598 | "url": "https://github.com/sponsors/sindresorhus" 599 | } 600 | }, 601 | "node_modules/methods": { 602 | "version": "1.1.2", 603 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 604 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 605 | "license": "MIT", 606 | "engines": { 607 | "node": ">= 0.6" 608 | } 609 | }, 610 | "node_modules/mime": { 611 | "version": "1.6.0", 612 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 613 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 614 | "license": "MIT", 615 | "bin": { 616 | "mime": "cli.js" 617 | }, 618 | "engines": { 619 | "node": ">=4" 620 | } 621 | }, 622 | "node_modules/mime-db": { 623 | "version": "1.52.0", 624 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 625 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 626 | "license": "MIT", 627 | "engines": { 628 | "node": ">= 0.6" 629 | } 630 | }, 631 | "node_modules/mime-types": { 632 | "version": "2.1.35", 633 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 634 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 635 | "license": "MIT", 636 | "dependencies": { 637 | "mime-db": "1.52.0" 638 | }, 639 | "engines": { 640 | "node": ">= 0.6" 641 | } 642 | }, 643 | "node_modules/ms": { 644 | "version": "2.0.0", 645 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 646 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 647 | "license": "MIT" 648 | }, 649 | "node_modules/negotiator": { 650 | "version": "0.6.3", 651 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 652 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 653 | "license": "MIT", 654 | "engines": { 655 | "node": ">= 0.6" 656 | } 657 | }, 658 | "node_modules/object-inspect": { 659 | "version": "1.13.4", 660 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 661 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 662 | "license": "MIT", 663 | "engines": { 664 | "node": ">= 0.4" 665 | }, 666 | "funding": { 667 | "url": "https://github.com/sponsors/ljharb" 668 | } 669 | }, 670 | "node_modules/on-finished": { 671 | "version": "2.4.1", 672 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 673 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 674 | "license": "MIT", 675 | "dependencies": { 676 | "ee-first": "1.1.1" 677 | }, 678 | "engines": { 679 | "node": ">= 0.8" 680 | } 681 | }, 682 | "node_modules/parseurl": { 683 | "version": "1.3.3", 684 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 685 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 686 | "license": "MIT", 687 | "engines": { 688 | "node": ">= 0.8" 689 | } 690 | }, 691 | "node_modules/path-to-regexp": { 692 | "version": "0.1.12", 693 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", 694 | "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", 695 | "license": "MIT" 696 | }, 697 | "node_modules/proxy-addr": { 698 | "version": "2.0.7", 699 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 700 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 701 | "license": "MIT", 702 | "dependencies": { 703 | "forwarded": "0.2.0", 704 | "ipaddr.js": "1.9.1" 705 | }, 706 | "engines": { 707 | "node": ">= 0.10" 708 | } 709 | }, 710 | "node_modules/proxy-from-env": { 711 | "version": "1.1.0", 712 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 713 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", 714 | "license": "MIT" 715 | }, 716 | "node_modules/qs": { 717 | "version": "6.14.0", 718 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 719 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 720 | "license": "BSD-3-Clause", 721 | "dependencies": { 722 | "side-channel": "^1.1.0" 723 | }, 724 | "engines": { 725 | "node": ">=0.6" 726 | }, 727 | "funding": { 728 | "url": "https://github.com/sponsors/ljharb" 729 | } 730 | }, 731 | "node_modules/range-parser": { 732 | "version": "1.2.1", 733 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 734 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 735 | "license": "MIT", 736 | "engines": { 737 | "node": ">= 0.6" 738 | } 739 | }, 740 | "node_modules/raw-body": { 741 | "version": "2.5.3", 742 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", 743 | "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", 744 | "license": "MIT", 745 | "dependencies": { 746 | "bytes": "~3.1.2", 747 | "http-errors": "~2.0.1", 748 | "iconv-lite": "~0.4.24", 749 | "unpipe": "~1.0.0" 750 | }, 751 | "engines": { 752 | "node": ">= 0.8" 753 | } 754 | }, 755 | "node_modules/safe-buffer": { 756 | "version": "5.2.1", 757 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 758 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 759 | "funding": [ 760 | { 761 | "type": "github", 762 | "url": "https://github.com/sponsors/feross" 763 | }, 764 | { 765 | "type": "patreon", 766 | "url": "https://www.patreon.com/feross" 767 | }, 768 | { 769 | "type": "consulting", 770 | "url": "https://feross.org/support" 771 | } 772 | ], 773 | "license": "MIT" 774 | }, 775 | "node_modules/safer-buffer": { 776 | "version": "2.1.2", 777 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 778 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 779 | "license": "MIT" 780 | }, 781 | "node_modules/send": { 782 | "version": "0.19.1", 783 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", 784 | "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", 785 | "license": "MIT", 786 | "dependencies": { 787 | "debug": "2.6.9", 788 | "depd": "2.0.0", 789 | "destroy": "1.2.0", 790 | "encodeurl": "~2.0.0", 791 | "escape-html": "~1.0.3", 792 | "etag": "~1.8.1", 793 | "fresh": "0.5.2", 794 | "http-errors": "2.0.0", 795 | "mime": "1.6.0", 796 | "ms": "2.1.3", 797 | "on-finished": "2.4.1", 798 | "range-parser": "~1.2.1", 799 | "statuses": "2.0.1" 800 | }, 801 | "engines": { 802 | "node": ">= 0.8.0" 803 | } 804 | }, 805 | "node_modules/send/node_modules/http-errors": { 806 | "version": "2.0.0", 807 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 808 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 809 | "license": "MIT", 810 | "dependencies": { 811 | "depd": "2.0.0", 812 | "inherits": "2.0.4", 813 | "setprototypeof": "1.2.0", 814 | "statuses": "2.0.1", 815 | "toidentifier": "1.0.1" 816 | }, 817 | "engines": { 818 | "node": ">= 0.8" 819 | } 820 | }, 821 | "node_modules/send/node_modules/ms": { 822 | "version": "2.1.3", 823 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 824 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 825 | "license": "MIT" 826 | }, 827 | "node_modules/send/node_modules/statuses": { 828 | "version": "2.0.1", 829 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 830 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 831 | "license": "MIT", 832 | "engines": { 833 | "node": ">= 0.8" 834 | } 835 | }, 836 | "node_modules/serve-static": { 837 | "version": "1.16.2", 838 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 839 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 840 | "license": "MIT", 841 | "dependencies": { 842 | "encodeurl": "~2.0.0", 843 | "escape-html": "~1.0.3", 844 | "parseurl": "~1.3.3", 845 | "send": "0.19.0" 846 | }, 847 | "engines": { 848 | "node": ">= 0.8.0" 849 | } 850 | }, 851 | "node_modules/serve-static/node_modules/http-errors": { 852 | "version": "2.0.0", 853 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 854 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 855 | "license": "MIT", 856 | "dependencies": { 857 | "depd": "2.0.0", 858 | "inherits": "2.0.4", 859 | "setprototypeof": "1.2.0", 860 | "statuses": "2.0.1", 861 | "toidentifier": "1.0.1" 862 | }, 863 | "engines": { 864 | "node": ">= 0.8" 865 | } 866 | }, 867 | "node_modules/serve-static/node_modules/ms": { 868 | "version": "2.1.3", 869 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 870 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 871 | "license": "MIT" 872 | }, 873 | "node_modules/serve-static/node_modules/send": { 874 | "version": "0.19.0", 875 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 876 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 877 | "license": "MIT", 878 | "dependencies": { 879 | "debug": "2.6.9", 880 | "depd": "2.0.0", 881 | "destroy": "1.2.0", 882 | "encodeurl": "~1.0.2", 883 | "escape-html": "~1.0.3", 884 | "etag": "~1.8.1", 885 | "fresh": "0.5.2", 886 | "http-errors": "2.0.0", 887 | "mime": "1.6.0", 888 | "ms": "2.1.3", 889 | "on-finished": "2.4.1", 890 | "range-parser": "~1.2.1", 891 | "statuses": "2.0.1" 892 | }, 893 | "engines": { 894 | "node": ">= 0.8.0" 895 | } 896 | }, 897 | "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { 898 | "version": "1.0.2", 899 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 900 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 901 | "license": "MIT", 902 | "engines": { 903 | "node": ">= 0.8" 904 | } 905 | }, 906 | "node_modules/serve-static/node_modules/statuses": { 907 | "version": "2.0.1", 908 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 909 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 910 | "license": "MIT", 911 | "engines": { 912 | "node": ">= 0.8" 913 | } 914 | }, 915 | "node_modules/setprototypeof": { 916 | "version": "1.2.0", 917 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 918 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 919 | "license": "ISC" 920 | }, 921 | "node_modules/side-channel": { 922 | "version": "1.1.0", 923 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 924 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 925 | "license": "MIT", 926 | "dependencies": { 927 | "es-errors": "^1.3.0", 928 | "object-inspect": "^1.13.3", 929 | "side-channel-list": "^1.0.0", 930 | "side-channel-map": "^1.0.1", 931 | "side-channel-weakmap": "^1.0.2" 932 | }, 933 | "engines": { 934 | "node": ">= 0.4" 935 | }, 936 | "funding": { 937 | "url": "https://github.com/sponsors/ljharb" 938 | } 939 | }, 940 | "node_modules/side-channel-list": { 941 | "version": "1.0.0", 942 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 943 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 944 | "license": "MIT", 945 | "dependencies": { 946 | "es-errors": "^1.3.0", 947 | "object-inspect": "^1.13.3" 948 | }, 949 | "engines": { 950 | "node": ">= 0.4" 951 | }, 952 | "funding": { 953 | "url": "https://github.com/sponsors/ljharb" 954 | } 955 | }, 956 | "node_modules/side-channel-map": { 957 | "version": "1.0.1", 958 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 959 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 960 | "license": "MIT", 961 | "dependencies": { 962 | "call-bound": "^1.0.2", 963 | "es-errors": "^1.3.0", 964 | "get-intrinsic": "^1.2.5", 965 | "object-inspect": "^1.13.3" 966 | }, 967 | "engines": { 968 | "node": ">= 0.4" 969 | }, 970 | "funding": { 971 | "url": "https://github.com/sponsors/ljharb" 972 | } 973 | }, 974 | "node_modules/side-channel-weakmap": { 975 | "version": "1.0.2", 976 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 977 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 978 | "license": "MIT", 979 | "dependencies": { 980 | "call-bound": "^1.0.2", 981 | "es-errors": "^1.3.0", 982 | "get-intrinsic": "^1.2.5", 983 | "object-inspect": "^1.13.3", 984 | "side-channel-map": "^1.0.1" 985 | }, 986 | "engines": { 987 | "node": ">= 0.4" 988 | }, 989 | "funding": { 990 | "url": "https://github.com/sponsors/ljharb" 991 | } 992 | }, 993 | "node_modules/statuses": { 994 | "version": "2.0.2", 995 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", 996 | "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", 997 | "license": "MIT", 998 | "engines": { 999 | "node": ">= 0.8" 1000 | } 1001 | }, 1002 | "node_modules/toidentifier": { 1003 | "version": "1.0.1", 1004 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1005 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1006 | "license": "MIT", 1007 | "engines": { 1008 | "node": ">=0.6" 1009 | } 1010 | }, 1011 | "node_modules/type-is": { 1012 | "version": "1.6.18", 1013 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1014 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1015 | "license": "MIT", 1016 | "dependencies": { 1017 | "media-typer": "0.3.0", 1018 | "mime-types": "~2.1.24" 1019 | }, 1020 | "engines": { 1021 | "node": ">= 0.6" 1022 | } 1023 | }, 1024 | "node_modules/unpipe": { 1025 | "version": "1.0.0", 1026 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1027 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1028 | "license": "MIT", 1029 | "engines": { 1030 | "node": ">= 0.8" 1031 | } 1032 | }, 1033 | "node_modules/utils-merge": { 1034 | "version": "1.0.1", 1035 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1036 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1037 | "license": "MIT", 1038 | "engines": { 1039 | "node": ">= 0.4.0" 1040 | } 1041 | }, 1042 | "node_modules/vary": { 1043 | "version": "1.1.2", 1044 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1045 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1046 | "license": "MIT", 1047 | "engines": { 1048 | "node": ">= 0.8" 1049 | } 1050 | } 1051 | } 1052 | } 1053 | --------------------------------------------------------------------------------