@sjawhar/whatsapp-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/__tests__/connection.test.d.ts +2 -0
- package/dist/__tests__/connection.test.d.ts.map +1 -0
- package/dist/__tests__/connection.test.js +105 -0
- package/dist/__tests__/connection.test.js.map +1 -0
- package/dist/__tests__/disconnect.test.d.ts +2 -0
- package/dist/__tests__/disconnect.test.d.ts.map +1 -0
- package/dist/__tests__/disconnect.test.js +166 -0
- package/dist/__tests__/disconnect.test.js.map +1 -0
- package/dist/__tests__/download-media.test.d.ts +2 -0
- package/dist/__tests__/download-media.test.d.ts.map +1 -0
- package/dist/__tests__/download-media.test.js +110 -0
- package/dist/__tests__/download-media.test.js.map +1 -0
- package/dist/__tests__/failures/connection-failures.test.d.ts +2 -0
- package/dist/__tests__/failures/connection-failures.test.d.ts.map +1 -0
- package/dist/__tests__/failures/connection-failures.test.js +146 -0
- package/dist/__tests__/failures/connection-failures.test.js.map +1 -0
- package/dist/__tests__/failures/edge-cases.test.d.ts +2 -0
- package/dist/__tests__/failures/edge-cases.test.d.ts.map +1 -0
- package/dist/__tests__/failures/edge-cases.test.js +121 -0
- package/dist/__tests__/failures/edge-cases.test.js.map +1 -0
- package/dist/__tests__/failures/resource-failures.test.d.ts +2 -0
- package/dist/__tests__/failures/resource-failures.test.d.ts.map +1 -0
- package/dist/__tests__/failures/resource-failures.test.js +136 -0
- package/dist/__tests__/failures/resource-failures.test.js.map +1 -0
- package/dist/__tests__/failures/security-failures.test.d.ts +2 -0
- package/dist/__tests__/failures/security-failures.test.d.ts.map +1 -0
- package/dist/__tests__/failures/security-failures.test.js +0 -0
- package/dist/__tests__/failures/security-failures.test.js.map +1 -0
- package/dist/__tests__/helpers/fake-baileys.d.ts +52 -0
- package/dist/__tests__/helpers/fake-baileys.d.ts.map +1 -0
- package/dist/__tests__/helpers/fake-baileys.js +60 -0
- package/dist/__tests__/helpers/fake-baileys.js.map +1 -0
- package/dist/__tests__/helpers/mcp-test-client.d.ts +9 -0
- package/dist/__tests__/helpers/mcp-test-client.d.ts.map +1 -0
- package/dist/__tests__/helpers/mcp-test-client.js +40 -0
- package/dist/__tests__/helpers/mcp-test-client.js.map +1 -0
- package/dist/__tests__/helpers/test-db.d.ts +4 -0
- package/dist/__tests__/helpers/test-db.d.ts.map +1 -0
- package/dist/__tests__/helpers/test-db.js +32 -0
- package/dist/__tests__/helpers/test-db.js.map +1 -0
- package/dist/__tests__/integration/chat-navigation.test.d.ts +2 -0
- package/dist/__tests__/integration/chat-navigation.test.d.ts.map +1 -0
- package/dist/__tests__/integration/chat-navigation.test.js +171 -0
- package/dist/__tests__/integration/chat-navigation.test.js.map +1 -0
- package/dist/__tests__/integration/contacts-flow.test.d.ts +2 -0
- package/dist/__tests__/integration/contacts-flow.test.d.ts.map +1 -0
- package/dist/__tests__/integration/contacts-flow.test.js +144 -0
- package/dist/__tests__/integration/contacts-flow.test.js.map +1 -0
- package/dist/__tests__/integration/media-flow.test.d.ts +2 -0
- package/dist/__tests__/integration/media-flow.test.d.ts.map +1 -0
- package/dist/__tests__/integration/media-flow.test.js +225 -0
- package/dist/__tests__/integration/media-flow.test.js.map +1 -0
- package/dist/__tests__/integration/search-flow.test.d.ts +2 -0
- package/dist/__tests__/integration/search-flow.test.d.ts.map +1 -0
- package/dist/__tests__/integration/search-flow.test.js +44 -0
- package/dist/__tests__/integration/search-flow.test.js.map +1 -0
- package/dist/__tests__/integration/send-message.test.d.ts +2 -0
- package/dist/__tests__/integration/send-message.test.d.ts.map +1 -0
- package/dist/__tests__/integration/send-message.test.js +160 -0
- package/dist/__tests__/integration/send-message.test.js.map +1 -0
- package/dist/__tests__/lock-file.test.d.ts +2 -0
- package/dist/__tests__/lock-file.test.d.ts.map +1 -0
- package/dist/__tests__/lock-file.test.js +63 -0
- package/dist/__tests__/lock-file.test.js.map +1 -0
- package/dist/__tests__/medium-fixes.test.d.ts +2 -0
- package/dist/__tests__/medium-fixes.test.d.ts.map +1 -0
- package/dist/__tests__/medium-fixes.test.js +141 -0
- package/dist/__tests__/medium-fixes.test.js.map +1 -0
- package/dist/__tests__/rate-limit.test.d.ts +2 -0
- package/dist/__tests__/rate-limit.test.d.ts.map +1 -0
- package/dist/__tests__/rate-limit.test.js +193 -0
- package/dist/__tests__/rate-limit.test.js.map +1 -0
- package/dist/__tests__/send-file.test.d.ts +2 -0
- package/dist/__tests__/send-file.test.d.ts.map +1 -0
- package/dist/__tests__/send-file.test.js +237 -0
- package/dist/__tests__/send-file.test.js.map +1 -0
- package/dist/__tests__/smoke.test.d.ts +2 -0
- package/dist/__tests__/smoke.test.d.ts.map +1 -0
- package/dist/__tests__/smoke.test.js +28 -0
- package/dist/__tests__/smoke.test.js.map +1 -0
- package/dist/__tests__/transcribe.test.d.ts +2 -0
- package/dist/__tests__/transcribe.test.d.ts.map +1 -0
- package/dist/__tests__/transcribe.test.js +71 -0
- package/dist/__tests__/transcribe.test.js.map +1 -0
- package/dist/__tests__/zombie.test.d.ts +2 -0
- package/dist/__tests__/zombie.test.d.ts.map +1 -0
- package/dist/__tests__/zombie.test.js +145 -0
- package/dist/__tests__/zombie.test.js.map +1 -0
- package/dist/db.d.ts +53 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +509 -0
- package/dist/db.js.map +1 -0
- package/dist/import-contacts.d.ts +37 -0
- package/dist/import-contacts.d.ts.map +1 -0
- package/dist/import-contacts.js +242 -0
- package/dist/import-contacts.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/lock.d.ts +16 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +65 -0
- package/dist/lock.js.map +1 -0
- package/dist/tools.d.ts +6 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +339 -0
- package/dist/tools.js.map +1 -0
- package/dist/transcribe.d.ts +8 -0
- package/dist/transcribe.d.ts.map +1 -0
- package/dist/transcribe.js +63 -0
- package/dist/transcribe.js.map +1 -0
- package/dist/utils.d.ts +51 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +156 -0
- package/dist/utils.js.map +1 -0
- package/dist/whatsapp.d.ts +50 -0
- package/dist/whatsapp.d.ts.map +1 -0
- package/dist/whatsapp.js +896 -0
- package/dist/whatsapp.js.map +1 -0
- package/package.json +52 -0
- package/patches/@whiskeysockets+baileys+6.7.21.patch +46 -0
- package/patches/libsignal+2.0.1.patch +84 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getChats, getChat, getMessages, searchMessages, searchContacts, getMessageContext, getRecipientInfo, sendTextMessage, sendFileMessage, downloadMessageMedia, transcribeVoiceNote, deleteMessage, deleteChat, updateContact, getMyInfo, } from "./whatsapp.js";
|
|
5
|
+
import { getDb } from "./db.js";
|
|
6
|
+
import { importContactsFromVcf } from "./import-contacts.js";
|
|
7
|
+
import { validateFilePath } from "./utils.js";
|
|
8
|
+
/**
|
|
9
|
+
* Register all WhatsApp MCP tools on the server.
|
|
10
|
+
*/
|
|
11
|
+
export function registerTools(server) {
|
|
12
|
+
// ─── Reading Tools ──────────────────────────────────────────
|
|
13
|
+
server.tool("list_chats", "List all WhatsApp chats sorted by last activity. Optionally filter by name.", {
|
|
14
|
+
nameFilter: z.string().optional().describe("Filter chats by name (case-insensitive substring match)"),
|
|
15
|
+
limit: z.number().min(1).max(100).default(20).describe("Max number of chats to return"),
|
|
16
|
+
}, async ({ nameFilter, limit }) => {
|
|
17
|
+
try {
|
|
18
|
+
const chats = await getChats(nameFilter, limit);
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: JSON.stringify(chats, null, 2),
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
server.tool("get_chat", "Get details for a specific chat including recent messages.", {
|
|
33
|
+
jid: z.string().describe("Chat JID (phone@s.whatsapp.net) or phone number"),
|
|
34
|
+
}, async ({ jid }) => {
|
|
35
|
+
try {
|
|
36
|
+
const chat = await getChat(jid);
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: JSON.stringify(chat, null, 2) }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
server.tool("list_messages", "Get messages from a specific chat. Returns most recent messages first.", {
|
|
46
|
+
jid: z.string().describe("Chat JID or phone number"),
|
|
47
|
+
limit: z.number().min(1).max(100).default(50).describe("Number of messages to return"),
|
|
48
|
+
}, async ({ jid, limit }) => {
|
|
49
|
+
try {
|
|
50
|
+
const messages = await getMessages(jid, limit);
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: JSON.stringify(messages, null, 2) }],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
server.tool("search_messages", "Substring search across messages. Can search in a specific chat or across all chats.", {
|
|
60
|
+
query: z.string().describe("Text to search for (case-insensitive)"),
|
|
61
|
+
jid: z.string().optional().describe("Optional: limit search to a specific chat JID"),
|
|
62
|
+
}, async ({ query, jid }) => {
|
|
63
|
+
try {
|
|
64
|
+
if (!query.trim()) {
|
|
65
|
+
throw new Error("Search query cannot be empty");
|
|
66
|
+
}
|
|
67
|
+
const results = await searchMessages(query, jid);
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: results.length
|
|
73
|
+
? JSON.stringify(results, null, 2)
|
|
74
|
+
: `No messages found matching "${query}"`,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
server.tool("search_contacts", "Find contacts by name or phone number.", {
|
|
84
|
+
query: z.string().describe("Name or phone number to search for"),
|
|
85
|
+
}, async ({ query }) => {
|
|
86
|
+
try {
|
|
87
|
+
const results = await searchContacts(query);
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: results.length
|
|
93
|
+
? JSON.stringify(results, null, 2)
|
|
94
|
+
: `No contacts found matching "${query}"`,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
server.tool("get_message_context", "Get messages surrounding a specific message for context.", {
|
|
104
|
+
jid: z.string().describe("Chat JID or phone number"),
|
|
105
|
+
messageId: z.string().describe("ID of the target message"),
|
|
106
|
+
count: z.number().min(1).max(20).default(5).describe("Number of messages before and after"),
|
|
107
|
+
}, async ({ jid, messageId, count }) => {
|
|
108
|
+
try {
|
|
109
|
+
const context = await getMessageContext(jid, messageId, count);
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: JSON.stringify(context, null, 2) }],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// ─── Identity Tools ─────────────────────────────────────────
|
|
119
|
+
server.tool("get_my_profile", "Get the authenticated WhatsApp user's own phone number, JID, LID JID, and display name. Use this to identify 'me' for sending messages to yourself.", {}, async () => {
|
|
120
|
+
try {
|
|
121
|
+
const info = getMyInfo();
|
|
122
|
+
if (!info.jid) {
|
|
123
|
+
return {
|
|
124
|
+
content: [{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: "Not connected yet — WhatsApp identity not available. Try again in a moment.",
|
|
127
|
+
}],
|
|
128
|
+
isError: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: JSON.stringify(info, null, 2) }],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
// ─── Contact Management Tools ───────────────────────────────
|
|
140
|
+
server.tool("update_contact", "Update or set the display name for a contact. Updates both the contact record and chat listing so the name appears immediately everywhere.", {
|
|
141
|
+
jid: z.string().describe("Contact JID (phone@s.whatsapp.net) or phone number"),
|
|
142
|
+
name: z.string().describe("The display name to set for this contact"),
|
|
143
|
+
}, async ({ jid, name }) => {
|
|
144
|
+
try {
|
|
145
|
+
const result = await updateContact(jid, name);
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
server.tool("sync_contacts", "Import phone contacts from the contacts/contacts.vcf file into the database. " +
|
|
155
|
+
"Matches phone numbers from the VCF to existing WhatsApp JIDs and updates their display names. " +
|
|
156
|
+
"Use this after a fresh QR code scan to populate contact names from your address book.", {}, async () => {
|
|
157
|
+
try {
|
|
158
|
+
const result = importContactsFromVcf(getDb());
|
|
159
|
+
return {
|
|
160
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
// ─── Writing Tools ──────────────────────────────────────────
|
|
168
|
+
server.tool("send_message", "Send a text message to a WhatsApp contact or group. " +
|
|
169
|
+
"First call without confirmed=true returns a preview for user approval. " +
|
|
170
|
+
"Call again with confirmed=true to actually send.", {
|
|
171
|
+
jid: z.string().describe("Recipient JID (phone@s.whatsapp.net), group JID, or phone number"),
|
|
172
|
+
text: z.string().describe("Message text to send"),
|
|
173
|
+
confirmed: z.boolean().default(false).describe("Set to true to confirm and send the message. When false, returns a preview for user approval."),
|
|
174
|
+
}, async ({ jid, text, confirmed }) => {
|
|
175
|
+
try {
|
|
176
|
+
const recipient = getRecipientInfo(jid);
|
|
177
|
+
if (!confirmed) {
|
|
178
|
+
return {
|
|
179
|
+
content: [{
|
|
180
|
+
type: "text",
|
|
181
|
+
text: JSON.stringify({
|
|
182
|
+
status: "confirmation_required",
|
|
183
|
+
to: recipient.name || "Unknown contact",
|
|
184
|
+
phone: recipient.phone,
|
|
185
|
+
jid: recipient.jid,
|
|
186
|
+
message: text,
|
|
187
|
+
instruction: "Show the user who this message will be sent to, their number, and the message content. Ask them to confirm before calling send_message again with confirmed=true.",
|
|
188
|
+
}, null, 2),
|
|
189
|
+
}],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const result = await sendTextMessage(jid, text);
|
|
193
|
+
return {
|
|
194
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
server.tool("send_file", "Send an image, video, audio, or document file to a WhatsApp contact or group. " +
|
|
202
|
+
"First call without confirmed=true returns a preview for user approval. " +
|
|
203
|
+
"Call again with confirmed=true to actually send.", {
|
|
204
|
+
jid: z.string().describe("Recipient JID or phone number"),
|
|
205
|
+
filePath: z.string().describe("Absolute path to the file to send"),
|
|
206
|
+
caption: z.string().optional().describe("Optional caption for the file"),
|
|
207
|
+
confirmed: z.boolean().default(false).describe("Set to true to confirm and send the file. When false, returns a preview for user approval."),
|
|
208
|
+
}, async ({ jid, filePath, caption, confirmed }) => {
|
|
209
|
+
try {
|
|
210
|
+
const recipient = getRecipientInfo(jid);
|
|
211
|
+
const allowedDir = process.env.ALLOWED_SEND_DIR || "./uploads/";
|
|
212
|
+
const maxFileSize = Number(process.env.MAX_SEND_FILE_SIZE || "67108864");
|
|
213
|
+
const validation = validateFilePath(filePath, allowedDir, maxFileSize);
|
|
214
|
+
if (!validation.valid) {
|
|
215
|
+
throw new Error(validation.error);
|
|
216
|
+
}
|
|
217
|
+
if (!confirmed) {
|
|
218
|
+
const stat = fs.statSync(validation.absolutePath);
|
|
219
|
+
return {
|
|
220
|
+
content: [{
|
|
221
|
+
type: "text",
|
|
222
|
+
text: JSON.stringify({
|
|
223
|
+
preview: true,
|
|
224
|
+
fileName: path.basename(validation.absolutePath),
|
|
225
|
+
fileSize: stat.size,
|
|
226
|
+
recipient,
|
|
227
|
+
}, null, 2),
|
|
228
|
+
}],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const result = await sendFileMessage(jid, validation.absolutePath, caption);
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
server.tool("delete_message", "Delete a message for everyone in the chat. Removes it from WhatsApp servers and the local database. For messages you didn't send, this only works in groups where you are an admin. " +
|
|
241
|
+
"First call without confirmed=true returns a preview for user approval. " +
|
|
242
|
+
"Call again with confirmed=true to actually delete.", {
|
|
243
|
+
jid: z.string().describe("Chat JID or phone number where the message is"),
|
|
244
|
+
messageId: z.string().describe("ID of the message to delete"),
|
|
245
|
+
confirmed: z.boolean().default(false).describe("Set to true to confirm and delete the message. When false, returns a preview for user approval."),
|
|
246
|
+
}, async ({ jid, messageId, confirmed }) => {
|
|
247
|
+
try {
|
|
248
|
+
if (!confirmed) {
|
|
249
|
+
const recipient = getRecipientInfo(jid);
|
|
250
|
+
return {
|
|
251
|
+
content: [{
|
|
252
|
+
type: "text",
|
|
253
|
+
text: JSON.stringify({
|
|
254
|
+
status: "confirmation_required",
|
|
255
|
+
chat: recipient.name || "Unknown contact",
|
|
256
|
+
phone: recipient.phone,
|
|
257
|
+
jid: recipient.jid,
|
|
258
|
+
messageId,
|
|
259
|
+
warning: "This will delete the message for everyone.",
|
|
260
|
+
instruction: "Show the user the chat name, number, and message ID. Ask them to confirm before calling delete_message again with confirmed=true.",
|
|
261
|
+
}, null, 2),
|
|
262
|
+
}],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const result = await deleteMessage(jid, messageId);
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
server.tool("delete_chat", "Delete an entire chat from WhatsApp. Removes the chat from your chat list and deletes all messages locally. " +
|
|
275
|
+
"First call without confirmed=true returns a preview for user approval. " +
|
|
276
|
+
"Call again with confirmed=true to actually delete.", {
|
|
277
|
+
jid: z.string().describe("Chat JID or phone number of the chat to delete"),
|
|
278
|
+
confirmed: z.boolean().default(false).describe("Set to true to confirm and delete the chat. When false, returns a preview for user approval."),
|
|
279
|
+
}, async ({ jid, confirmed }) => {
|
|
280
|
+
try {
|
|
281
|
+
if (!confirmed) {
|
|
282
|
+
const recipient = getRecipientInfo(jid);
|
|
283
|
+
return {
|
|
284
|
+
content: [{
|
|
285
|
+
type: "text",
|
|
286
|
+
text: JSON.stringify({
|
|
287
|
+
status: "confirmation_required",
|
|
288
|
+
chat: recipient.name || "Unknown contact",
|
|
289
|
+
phone: recipient.phone,
|
|
290
|
+
jid: recipient.jid,
|
|
291
|
+
warning: "This will permanently delete the entire chat from your WhatsApp, including all messages.",
|
|
292
|
+
instruction: "Show the user the chat name and number. Ask them to confirm before calling delete_chat again with confirmed=true.",
|
|
293
|
+
}, null, 2),
|
|
294
|
+
}],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const result = await deleteChat(jid);
|
|
298
|
+
return {
|
|
299
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
// ─── Media Tools ────────────────────────────────────────────
|
|
307
|
+
server.tool("download_media", "Download media (voice notes, images, videos, documents) from a received message to local disk. Returns the file path so you can read/process the content directly.", {
|
|
308
|
+
jid: z.string().describe("Chat JID or phone number where the message is"),
|
|
309
|
+
messageId: z.string().describe("ID of the message containing media"),
|
|
310
|
+
}, async ({ jid, messageId }) => {
|
|
311
|
+
try {
|
|
312
|
+
const result = await downloadMessageMedia(jid, messageId);
|
|
313
|
+
return {
|
|
314
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
server.tool("transcribe_voice_note", "Transcribe a voice note to text. Downloads the audio from WhatsApp, " +
|
|
322
|
+
"sends it to a speech-to-text API, and returns the transcription. " +
|
|
323
|
+
"Requires WHISPER_API_KEY and WHISPER_API_URL environment variables. " +
|
|
324
|
+
"Results are cached so repeated calls for the same message are instant.", {
|
|
325
|
+
jid: z.string().describe("Chat JID or phone number where the voice note is"),
|
|
326
|
+
messageId: z.string().describe("ID of the voice note message"),
|
|
327
|
+
}, async ({ jid, messageId }) => {
|
|
328
|
+
try {
|
|
329
|
+
const result = await transcribeVoiceNote(jid, messageId);
|
|
330
|
+
return {
|
|
331
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcribe.d.ts","sourceRoot":"","sources":["../src/transcribe.ts"],"names":[],"mappings":"AAEA,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA0BD,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,mBAAmB,CAAC,CAgC9B"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { validateApiUrl } from "./utils.js";
|
|
2
|
+
function getConfig() {
|
|
3
|
+
const apiKey = process.env.WHISPER_API_KEY;
|
|
4
|
+
const apiUrl = process.env.WHISPER_API_URL;
|
|
5
|
+
if (!apiKey || !apiUrl) {
|
|
6
|
+
throw new Error("Voice note transcription requires WHISPER_API_KEY and WHISPER_API_URL environment variables. " +
|
|
7
|
+
"Set them in your Claude Desktop MCP config under the \"env\" block for the WhatsApp server.\n" +
|
|
8
|
+
"Example for Groq (free): WHISPER_API_URL=https://api.groq.com/openai/v1/audio/transcriptions, " +
|
|
9
|
+
"WHISPER_API_KEY=gsk_...\n" +
|
|
10
|
+
"Example for OpenAI: WHISPER_API_URL=https://api.openai.com/v1/audio/transcriptions, " +
|
|
11
|
+
"WHISPER_API_KEY=sk-...");
|
|
12
|
+
}
|
|
13
|
+
const validatedUrl = validateApiUrl(apiUrl);
|
|
14
|
+
return {
|
|
15
|
+
apiUrl: validatedUrl.href,
|
|
16
|
+
apiKey,
|
|
17
|
+
model: process.env.WHISPER_MODEL || "whisper-large-v3-turbo",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function transcribeAudio(buffer, fileName) {
|
|
21
|
+
const config = getConfig();
|
|
22
|
+
const formData = new FormData();
|
|
23
|
+
formData.append("file", new Blob([new Uint8Array(buffer)]), fileName);
|
|
24
|
+
formData.append("model", config.model);
|
|
25
|
+
formData.append("response_format", "verbose_json");
|
|
26
|
+
const response = await fetch(config.apiUrl, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
30
|
+
},
|
|
31
|
+
body: formData,
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
const errorText = await response.text();
|
|
35
|
+
throw new Error(`Transcription API error (${response.status}): ${errorText}`);
|
|
36
|
+
}
|
|
37
|
+
const result = (await response.json());
|
|
38
|
+
return {
|
|
39
|
+
text: deduplicateText(result.text),
|
|
40
|
+
language: result.language,
|
|
41
|
+
duration: result.duration,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whisper models sometimes hallucinate by repeating sentences.
|
|
46
|
+
* This detects and removes consecutive duplicate sentences.
|
|
47
|
+
*/
|
|
48
|
+
function deduplicateText(text) {
|
|
49
|
+
const sentences = text.match(/[^.!?]+[.!?]+/g);
|
|
50
|
+
if (!sentences)
|
|
51
|
+
return text;
|
|
52
|
+
const trimmed = sentences.map((s) => s.trim());
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const deduped = [];
|
|
55
|
+
for (const sentence of trimmed) {
|
|
56
|
+
if (!seen.has(sentence)) {
|
|
57
|
+
seen.add(sentence);
|
|
58
|
+
deduped.push(sentence);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return deduped.join(" ");
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=transcribe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcribe.js","sourceRoot":"","sources":["../src/transcribe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAQ5C,SAAS,SAAS;IAChB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IAC3C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IAE3C,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,+FAA+F;YAC/F,+FAA+F;YAC/F,gGAAgG;YAChG,2BAA2B;YAC3B,sFAAsF;YACtF,wBAAwB,CACzB,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IAE5C,OAAO;QACL,MAAM,EAAE,YAAY,CAAC,IAAI;QACzB,MAAM;QACN,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,wBAAwB;KAC7D,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,QAAgB;IAEhB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAE3B,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACtE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;IAEnD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE;QAC1C,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,MAAM,CAAC,MAAM,EAAE;SACzC;QACD,IAAI,EAAE,QAAQ;KACf,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,CAAC,MAAM,MAAM,SAAS,EAAE,CAAC,CAAC;IAChF,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAIpC,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC;QAClC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;KAC1B,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAC/C,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAE5B,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,QAAQ,IAAI,OAAO,EAAE,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC"}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a phone number or JID into proper WhatsApp JID format.
|
|
3
|
+
* Accepts: "1234567890", "+1234567890", "1234567890@s.whatsapp.net"
|
|
4
|
+
* Groups are passed through as-is if they end with @g.us
|
|
5
|
+
*/
|
|
6
|
+
export declare function toJid(input: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Extract the readable phone number or group ID from a JID.
|
|
9
|
+
*/
|
|
10
|
+
export declare function fromJid(jid: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Format a Unix timestamp (seconds) to a readable date string.
|
|
13
|
+
*/
|
|
14
|
+
export declare function formatTimestamp(ts: number | null | undefined): string;
|
|
15
|
+
/**
|
|
16
|
+
* Format a SQLite message row for MCP tool responses.
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatMessageRow(row: {
|
|
19
|
+
id: string;
|
|
20
|
+
chat_jid: string;
|
|
21
|
+
from_me: number;
|
|
22
|
+
sender_jid: string | null;
|
|
23
|
+
sender_name: string | null;
|
|
24
|
+
type: string;
|
|
25
|
+
text: string | null;
|
|
26
|
+
timestamp: number;
|
|
27
|
+
has_media: number;
|
|
28
|
+
}): Record<string, unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Detect media MIME type from a file extension.
|
|
31
|
+
*/
|
|
32
|
+
export declare function mimeFromExtension(filePath: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Detect the Baileys media type key from MIME type for sending files.
|
|
35
|
+
*/
|
|
36
|
+
export declare function mediaCategoryFromMime(mime: string): "image" | "video" | "audio" | "document";
|
|
37
|
+
export declare function validateFilePath(filePath: string, allowedDir: string, maxSizeBytes: number): {
|
|
38
|
+
valid: true;
|
|
39
|
+
absolutePath: string;
|
|
40
|
+
} | {
|
|
41
|
+
valid: false;
|
|
42
|
+
error: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function sanitizeFilename(name: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Validate a Whisper API URL to prevent SSRF attacks.
|
|
47
|
+
* Rejects non-http(s) protocols, localhost, and private IP ranges.
|
|
48
|
+
* Returns the parsed URL on success; throws on violation.
|
|
49
|
+
*/
|
|
50
|
+
export declare function validateApiUrl(url: string): URL;
|
|
51
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAe3C;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAIrE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAU1B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAsB1D;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,CAK5F;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,GACnB;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAgCzE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAeD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAsB/C"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a phone number or JID into proper WhatsApp JID format.
|
|
5
|
+
* Accepts: "1234567890", "+1234567890", "1234567890@s.whatsapp.net"
|
|
6
|
+
* Groups are passed through as-is if they end with @g.us
|
|
7
|
+
*/
|
|
8
|
+
export function toJid(input) {
|
|
9
|
+
const trimmed = input.trim();
|
|
10
|
+
// Already a valid JID
|
|
11
|
+
if (trimmed.endsWith("@s.whatsapp.net") || trimmed.endsWith("@g.us") || trimmed.endsWith("@lid")) {
|
|
12
|
+
return trimmed.replace(/:\d+@/, "@");
|
|
13
|
+
}
|
|
14
|
+
// Strip non-numeric characters (like +, spaces, dashes)
|
|
15
|
+
const digits = trimmed.replace(/\D/g, "");
|
|
16
|
+
if (!digits) {
|
|
17
|
+
throw new Error(`Invalid phone number or JID: "${input}"`);
|
|
18
|
+
}
|
|
19
|
+
return `${digits}@s.whatsapp.net`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Extract the readable phone number or group ID from a JID.
|
|
23
|
+
*/
|
|
24
|
+
export function fromJid(jid) {
|
|
25
|
+
return jid.replace(/@s\.whatsapp\.net$/, "").replace(/@g\.us$/, " (group)").replace(/@lid$/, "");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Format a Unix timestamp (seconds) to a readable date string.
|
|
29
|
+
*/
|
|
30
|
+
export function formatTimestamp(ts) {
|
|
31
|
+
if (!ts)
|
|
32
|
+
return "unknown";
|
|
33
|
+
const n = typeof ts === "number" ? ts : Number(ts);
|
|
34
|
+
return new Date(n * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Format a SQLite message row for MCP tool responses.
|
|
38
|
+
*/
|
|
39
|
+
export function formatMessageRow(row) {
|
|
40
|
+
return {
|
|
41
|
+
id: row.id,
|
|
42
|
+
from: row.from_me ? "me" : (row.sender_name || row.sender_jid || row.chat_jid),
|
|
43
|
+
fromMe: !!row.from_me,
|
|
44
|
+
type: row.type,
|
|
45
|
+
text: row.text || undefined,
|
|
46
|
+
timestamp: formatTimestamp(row.timestamp),
|
|
47
|
+
hasMedia: !!row.has_media,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Detect media MIME type from a file extension.
|
|
52
|
+
*/
|
|
53
|
+
export function mimeFromExtension(filePath) {
|
|
54
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
55
|
+
const map = {
|
|
56
|
+
jpg: "image/jpeg",
|
|
57
|
+
jpeg: "image/jpeg",
|
|
58
|
+
png: "image/png",
|
|
59
|
+
gif: "image/gif",
|
|
60
|
+
webp: "image/webp",
|
|
61
|
+
mp4: "video/mp4",
|
|
62
|
+
mov: "video/quicktime",
|
|
63
|
+
avi: "video/x-msvideo",
|
|
64
|
+
mp3: "audio/mpeg",
|
|
65
|
+
ogg: "audio/ogg",
|
|
66
|
+
wav: "audio/wav",
|
|
67
|
+
pdf: "application/pdf",
|
|
68
|
+
doc: "application/msword",
|
|
69
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
70
|
+
xls: "application/vnd.ms-excel",
|
|
71
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
72
|
+
zip: "application/zip",
|
|
73
|
+
};
|
|
74
|
+
return map[ext || ""] || "application/octet-stream";
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Detect the Baileys media type key from MIME type for sending files.
|
|
78
|
+
*/
|
|
79
|
+
export function mediaCategoryFromMime(mime) {
|
|
80
|
+
if (mime.startsWith("image/"))
|
|
81
|
+
return "image";
|
|
82
|
+
if (mime.startsWith("video/"))
|
|
83
|
+
return "video";
|
|
84
|
+
if (mime.startsWith("audio/"))
|
|
85
|
+
return "audio";
|
|
86
|
+
return "document";
|
|
87
|
+
}
|
|
88
|
+
export function validateFilePath(filePath, allowedDir, maxSizeBytes) {
|
|
89
|
+
const absolutePath = path.resolve(filePath);
|
|
90
|
+
const allowedDirPath = path.resolve(allowedDir);
|
|
91
|
+
const allowedPrefix = `${allowedDirPath}${path.sep}`;
|
|
92
|
+
if (!absolutePath.startsWith(allowedPrefix)) {
|
|
93
|
+
return { valid: false, error: "Path not allowed" };
|
|
94
|
+
}
|
|
95
|
+
if (!fs.existsSync(absolutePath)) {
|
|
96
|
+
return { valid: false, error: `File not found: ${absolutePath}` };
|
|
97
|
+
}
|
|
98
|
+
let stat;
|
|
99
|
+
try {
|
|
100
|
+
stat = fs.statSync(absolutePath);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return { valid: false, error: err?.message || "Failed to stat file" };
|
|
104
|
+
}
|
|
105
|
+
if (!stat.isFile()) {
|
|
106
|
+
return { valid: false, error: `Not a file: ${absolutePath}` };
|
|
107
|
+
}
|
|
108
|
+
if (stat.size > maxSizeBytes) {
|
|
109
|
+
return {
|
|
110
|
+
valid: false,
|
|
111
|
+
error: `File too large: ${stat.size} bytes exceeds ${maxSizeBytes} bytes`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { valid: true, absolutePath };
|
|
115
|
+
}
|
|
116
|
+
export function sanitizeFilename(name) {
|
|
117
|
+
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
118
|
+
}
|
|
119
|
+
function isPrivateIp(hostname) {
|
|
120
|
+
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
121
|
+
const match = hostname.match(ipv4Regex);
|
|
122
|
+
if (!match)
|
|
123
|
+
return false;
|
|
124
|
+
const [, a, b] = match.map(Number);
|
|
125
|
+
return (a === 10 ||
|
|
126
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
127
|
+
(a === 192 && b === 168) ||
|
|
128
|
+
(a === 169 && b === 254) // link-local / cloud metadata
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Validate a Whisper API URL to prevent SSRF attacks.
|
|
133
|
+
* Rejects non-http(s) protocols, localhost, and private IP ranges.
|
|
134
|
+
* Returns the parsed URL on success; throws on violation.
|
|
135
|
+
*/
|
|
136
|
+
export function validateApiUrl(url) {
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = new URL(url);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
throw new Error(`Invalid URL: ${url}`);
|
|
143
|
+
}
|
|
144
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
145
|
+
throw new Error(`Only http/https URLs allowed for WHISPER_API_URL (got ${parsed.protocol})`);
|
|
146
|
+
}
|
|
147
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, ''); // strip IPv6 brackets
|
|
148
|
+
if (['localhost', '127.0.0.1', '::1'].includes(hostname)) {
|
|
149
|
+
throw new Error(`SSRF protection: localhost not allowed for WHISPER_API_URL`);
|
|
150
|
+
}
|
|
151
|
+
if (isPrivateIp(hostname)) {
|
|
152
|
+
throw new Error(`SSRF protection: Private/internal IP not allowed for WHISPER_API_URL (${hostname})`);
|
|
153
|
+
}
|
|
154
|
+
return parsed;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=utils.js.map
|