@operor/provider-wati 0.1.1 → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # @operor/provider-wati
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a3c2adc: Upgrade WATI provider to v3 API with improved webhook URL setup, add WATI WhatsApp skill to catalog, add back navigation and Esc-to-exit to CLI setup wizard, and persist business info in .env as a required setup step.
8
+
9
+ ### Patch Changes
10
+
11
+ - @operor/core@0.2.0
package/dist/index.d.ts CHANGED
@@ -5,13 +5,13 @@ import { MessageProvider, OutgoingMessage } from "@operor/core";
5
5
  interface WatiProviderConfig {
6
6
  /** WATI API bearer token */
7
7
  apiToken: string;
8
- /** WATI tenant ID (appears in the API base URL) */
9
- tenantId: string;
8
+ /** WATI tenant ID — optional in v3 (used for Channel:PhoneNumber targeting in multi-channel setups) */
9
+ tenantId?: string;
10
10
  /** Port for the webhook HTTP server (default: 3001) */
11
11
  webhookPort?: number;
12
12
  /** Path the webhook listens on (default: /webhook/wati) */
13
13
  webhookPath?: string;
14
- /** Override the WATI API base URL (default: https://live-mt-server.wati.io/{tenantId}/api) */
14
+ /** Override the WATI API base URL (default: https://live-mt-server.wati.io) */
15
15
  apiBaseUrl?: string;
16
16
  }
17
17
  interface WatiTemplateParams {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/WatiProvider.ts"],"mappings":";;;;UAAiB,kBAAA;;EAEf,QAAA;;EAEA,QAAA;EAJiC;EAMjC,WAAA;EANiC;EAQjC,WAAA;EAJA;EAMA,UAAA;AAAA;AAAA,UAce,kBAAA;EACf,YAAA;EACA,UAAA,EAAY,KAAA;IAAQ,IAAA;IAAc,KAAA;EAAA;AAAA;;;cCrBvB,YAAA,SAAqB,YAAA,YAAwB,eAAA;EAAA,SACxC,IAAA;EAAA,QACR,MAAA;EAAA,QACA,MAAA;EAAA,QACA,SAAA;EAAA,QACA,OAAA;EAAA,QACA,WAAA;EAAA,QACA,WAAA;cAEI,MAAA,EAAQ,kBAAA;EAQd,OAAA,CAAA,GAAW,OAAA;EA2BX,UAAA,CAAA,GAAc,OAAA;EAWd,WAAA,CAAY,EAAA,UAAY,OAAA,EAAS,eAAA,GAAkB,OAAA;EAanD,mBAAA,CAAoB,EAAA,UAAY,QAAA,EAAU,kBAAA,GAAqB,OAAA;EAgBrE,QAAA,CAAA;EAAA,QAMQ,kBAAA;EAAA,QAUA,oBAAA;EAAA,QAoCM,UAAA;AAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/WatiProvider.ts"],"mappings":";;;;UAAiB,kBAAA;;EAEf,QAAA;;EAEA,QAAA;EAJiC;EAMjC,WAAA;EANiC;EAQjC,WAAA;EAJA;EAMA,UAAA;AAAA;AAAA,UAGe,kBAAA;EACf,YAAA;EACA,UAAA,EAAY,KAAA;IAAQ,IAAA;IAAc,KAAA;EAAA;AAAA;;;cCVvB,YAAA,SAAqB,YAAA,YAAwB,eAAA;EAAA,SACxC,IAAA;EAAA,QACR,MAAA;EAAA,QACA,MAAA;EAAA,QACA,SAAA;EAAA,QACA,OAAA;EAAA,QACA,WAAA;EAAA,QACA,WAAA;cAEI,MAAA,EAAQ,kBAAA;EASd,OAAA,CAAA,GAAW,OAAA;EA2BX,UAAA,CAAA,GAAc,OAAA;EAWd,WAAA,CAAY,EAAA,UAAY,OAAA,EAAS,eAAA,GAAkB,OAAA;EAenD,mBAAA,CAAoB,EAAA,UAAY,QAAA,EAAU,kBAAA,GAAqB,OAAA;EAkBrE,QAAA,CAAA;EAAA,QAMQ,kBAAA;EAAA,QAUA,oBAAA;EAAA,QAsDM,UAAA;AAAA"}
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ var WatiProvider = class extends EventEmitter {
13
13
  constructor(config) {
14
14
  super();
15
15
  this.config = config;
16
- this.baseUrl = config.apiBaseUrl || `https://live-mt-server.wati.io/${config.tenantId}/api`;
16
+ this.baseUrl = (config.apiBaseUrl || "https://live-mt-server.wati.io").replace(/\/+$/, "");
17
17
  this.webhookPort = config.webhookPort || 3001;
18
18
  this.webhookPath = config.webhookPath || "/webhook/wati";
19
19
  }
@@ -46,17 +46,21 @@ var WatiProvider = class extends EventEmitter {
46
46
  }
47
47
  async sendMessage(to, message) {
48
48
  if (!this.connected) throw new Error("WATI provider not connected");
49
- await this.apiRequest(`/v1/sendSessionMessage/${to}`, {
49
+ await this.apiRequest("/api/ext/v3/conversations/messages/text", {
50
50
  method: "POST",
51
- body: JSON.stringify({ messageText: message.text })
51
+ body: JSON.stringify({
52
+ target: to,
53
+ text: message.text
54
+ })
52
55
  });
53
56
  console.log(`šŸ“¤ [WATI] Sent reply to ${to}`);
54
57
  }
55
58
  async sendTemplateMessage(to, template) {
56
59
  if (!this.connected) throw new Error("WATI provider not connected");
57
- await this.apiRequest(`/v2/sendTemplateMessage?whatsappNumber=${encodeURIComponent(to)}`, {
60
+ await this.apiRequest("/api/ext/v3/conversations/messages/template", {
58
61
  method: "POST",
59
62
  body: JSON.stringify({
63
+ target: to,
60
64
  template_name: template.templateName,
61
65
  parameters: template.parameters
62
66
  })
@@ -80,24 +84,37 @@ var WatiProvider = class extends EventEmitter {
80
84
  res.writeHead(200).end("ok");
81
85
  try {
82
86
  const payload = JSON.parse(body);
83
- if (payload.eventType !== "messageReceived" || payload.fromMe) return;
87
+ console.log(`šŸ“© [WATI] Webhook received:`, JSON.stringify(payload, null, 2));
88
+ const text = payload.text || payload.message?.text || payload.message?.body || "";
89
+ const waId = payload.waId || payload.wa_id || payload.from || "";
90
+ const messageId = payload.whatsappMessageId || payload.message_id || payload.id || "";
91
+ const senderName = payload.senderName || payload.sender_name || payload.pushName || "";
92
+ const timestamp = payload.timestamp || payload.created || "";
93
+ const fromMe = payload.fromMe ?? payload.from_me ?? payload.owner ?? false;
94
+ const messageType = payload.type || payload.messageType || "text";
95
+ if (!text || fromMe) {
96
+ console.log(`ā­ļø [WATI] Skipped: ${!text ? "no text" : "fromMe"} (type: ${messageType})`);
97
+ return;
98
+ }
99
+ const parsedTs = timestamp ? new Date(/^\d+$/.test(String(timestamp)) ? Number(timestamp) * (/^\d{10}$/.test(String(timestamp)) ? 1e3 : 1) : timestamp).getTime() : NaN;
84
100
  const msg = {
85
- id: payload.whatsappMessageId,
86
- from: payload.waId,
87
- text: payload.text,
88
- timestamp: new Date(payload.timestamp).getTime(),
101
+ id: messageId,
102
+ from: waId,
103
+ text,
104
+ timestamp: Number.isFinite(parsedTs) ? parsedTs : Date.now(),
89
105
  channel: "whatsapp",
90
106
  provider: this.name,
91
107
  metadata: {
92
- senderName: payload.senderName,
93
- messageType: payload.type
108
+ senderName,
109
+ messageType
94
110
  }
95
111
  };
96
- console.log(`\nšŸ“„ [WATI] New message from ${payload.senderName || payload.waId}`);
112
+ console.log(`\nšŸ“„ [WATI] New message from ${senderName || waId}`);
97
113
  console.log(` Message: "${msg.text}"`);
98
114
  this.emit("message", msg);
99
115
  } catch (err) {
100
116
  console.error("āŒ [WATI] Failed to parse webhook payload:", err);
117
+ console.error(" Raw body:", body);
101
118
  }
102
119
  });
103
120
  }
@@ -113,8 +130,10 @@ var WatiProvider = class extends EventEmitter {
113
130
  });
114
131
  if (!res.ok) {
115
132
  const text = await res.text().catch(() => "");
133
+ console.error(`āŒ [WATI] API ${res.status} ${init.method || "GET"} ${path}: ${text}`);
116
134
  throw new Error(`WATI API error ${res.status}: ${text}`);
117
135
  }
136
+ console.log(`āœ… [WATI] API ${res.status} ${init.method || "GET"} ${path}`);
118
137
  return res;
119
138
  }
120
139
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/WatiProvider.ts"],"sourcesContent":["import EventEmitter from 'eventemitter3';\nimport * as http from 'node:http';\nimport type { MessageProvider, IncomingMessage, OutgoingMessage } from '@operor/core';\nimport type { WatiProviderConfig, WatiWebhookPayload, WatiTemplateParams } from './types.js';\n\nexport class WatiProvider extends EventEmitter implements MessageProvider {\n public readonly name = 'wati';\n private config: WatiProviderConfig;\n private server: http.Server | null = null;\n private connected = false;\n private baseUrl: string;\n private webhookPort: number;\n private webhookPath: string;\n\n constructor(config: WatiProviderConfig) {\n super();\n this.config = config;\n this.baseUrl = config.apiBaseUrl || `https://live-mt-server.wati.io/${config.tenantId}/api`;\n this.webhookPort = config.webhookPort || 3001;\n this.webhookPath = config.webhookPath || '/webhook/wati';\n }\n\n async connect(): Promise<void> {\n this.server = http.createServer((req, res) => {\n if (req.url?.startsWith(this.webhookPath)) {\n if (req.method === 'GET') {\n this.handleVerification(req, res);\n } else if (req.method === 'POST') {\n this.handleWebhookRequest(req, res);\n } else {\n res.writeHead(405).end();\n }\n } else {\n res.writeHead(404).end();\n }\n });\n\n await new Promise<void>((resolve) => {\n this.server!.listen(this.webhookPort, () => {\n this.connected = true;\n console.log(`āœ… WATI webhook server listening on port ${this.webhookPort}`);\n console.log(` Webhook path: ${this.webhookPath}`);\n resolve();\n });\n });\n\n this.emit('ready');\n }\n\n async disconnect(): Promise<void> {\n if (this.server) {\n await new Promise<void>((resolve, reject) => {\n this.server!.close((err) => (err ? reject(err) : resolve()));\n });\n this.server = null;\n }\n this.connected = false;\n console.log('WATI provider disconnected');\n }\n\n async sendMessage(to: string, message: OutgoingMessage): Promise<void> {\n if (!this.connected) {\n throw new Error('WATI provider not connected');\n }\n\n await this.apiRequest(`/v1/sendSessionMessage/${to}`, {\n method: 'POST',\n body: JSON.stringify({ messageText: message.text }),\n });\n\n console.log(`šŸ“¤ [WATI] Sent reply to ${to}`);\n }\n\n async sendTemplateMessage(to: string, template: WatiTemplateParams): Promise<void> {\n if (!this.connected) {\n throw new Error('WATI provider not connected');\n }\n\n await this.apiRequest(`/v2/sendTemplateMessage?whatsappNumber=${encodeURIComponent(to)}`, {\n method: 'POST',\n body: JSON.stringify({\n template_name: template.templateName,\n parameters: template.parameters,\n }),\n });\n\n console.log(`šŸ“¤ [WATI] Sent template \"${template.templateName}\" to ${to}`);\n }\n\n isActive(): boolean {\n return this.connected;\n }\n\n // --- Private helpers ---\n\n private handleVerification(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url!, `http://localhost:${this.webhookPort}`);\n const challenge = url.searchParams.get('hub.challenge');\n if (challenge) {\n res.writeHead(200, { 'Content-Type': 'text/plain' }).end(challenge);\n } else {\n res.writeHead(200).end('ok');\n }\n }\n\n private handleWebhookRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n let body = '';\n req.on('data', (chunk: Buffer) => { body += chunk.toString(); });\n req.on('end', () => {\n res.writeHead(200).end('ok');\n\n try {\n const payload: WatiWebhookPayload = JSON.parse(body);\n\n if (payload.eventType !== 'messageReceived' || payload.fromMe) {\n return;\n }\n\n const msg: IncomingMessage = {\n id: payload.whatsappMessageId,\n from: payload.waId,\n text: payload.text,\n timestamp: new Date(payload.timestamp).getTime(),\n channel: 'whatsapp',\n provider: this.name,\n metadata: {\n senderName: payload.senderName,\n messageType: payload.type,\n },\n };\n\n console.log(`\\nšŸ“„ [WATI] New message from ${payload.senderName || payload.waId}`);\n console.log(` Message: \"${msg.text}\"`);\n\n this.emit('message', msg);\n } catch (err) {\n console.error('āŒ [WATI] Failed to parse webhook payload:', err);\n }\n });\n }\n\n private async apiRequest(path: string, init: RequestInit): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n const res = await fetch(url, {\n ...init,\n headers: {\n 'Authorization': `Bearer ${this.config.apiToken}`,\n 'Content-Type': 'application/json',\n ...init.headers,\n },\n });\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new Error(`WATI API error ${res.status}: ${text}`);\n }\n\n return res;\n }\n}\n"],"mappings":";;;;AAKA,IAAa,eAAb,cAAkC,aAAwC;CACxE,AAAgB,OAAO;CACvB,AAAQ;CACR,AAAQ,SAA6B;CACrC,AAAQ,YAAY;CACpB,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,YAAY,QAA4B;AACtC,SAAO;AACP,OAAK,SAAS;AACd,OAAK,UAAU,OAAO,cAAc,kCAAkC,OAAO,SAAS;AACtF,OAAK,cAAc,OAAO,eAAe;AACzC,OAAK,cAAc,OAAO,eAAe;;CAG3C,MAAM,UAAyB;AAC7B,OAAK,SAAS,KAAK,cAAc,KAAK,QAAQ;AAC5C,OAAI,IAAI,KAAK,WAAW,KAAK,YAAY,CACvC,KAAI,IAAI,WAAW,MACjB,MAAK,mBAAmB,KAAK,IAAI;YACxB,IAAI,WAAW,OACxB,MAAK,qBAAqB,KAAK,IAAI;OAEnC,KAAI,UAAU,IAAI,CAAC,KAAK;OAG1B,KAAI,UAAU,IAAI,CAAC,KAAK;IAE1B;AAEF,QAAM,IAAI,SAAe,YAAY;AACnC,QAAK,OAAQ,OAAO,KAAK,mBAAmB;AAC1C,SAAK,YAAY;AACjB,YAAQ,IAAI,2CAA2C,KAAK,cAAc;AAC1E,YAAQ,IAAI,oBAAoB,KAAK,cAAc;AACnD,aAAS;KACT;IACF;AAEF,OAAK,KAAK,QAAQ;;CAGpB,MAAM,aAA4B;AAChC,MAAI,KAAK,QAAQ;AACf,SAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,SAAK,OAAQ,OAAO,QAAS,MAAM,OAAO,IAAI,GAAG,SAAS,CAAE;KAC5D;AACF,QAAK,SAAS;;AAEhB,OAAK,YAAY;AACjB,UAAQ,IAAI,6BAA6B;;CAG3C,MAAM,YAAY,IAAY,SAAyC;AACrE,MAAI,CAAC,KAAK,UACR,OAAM,IAAI,MAAM,8BAA8B;AAGhD,QAAM,KAAK,WAAW,0BAA0B,MAAM;GACpD,QAAQ;GACR,MAAM,KAAK,UAAU,EAAE,aAAa,QAAQ,MAAM,CAAC;GACpD,CAAC;AAEF,UAAQ,IAAI,2BAA2B,KAAK;;CAG9C,MAAM,oBAAoB,IAAY,UAA6C;AACjF,MAAI,CAAC,KAAK,UACR,OAAM,IAAI,MAAM,8BAA8B;AAGhD,QAAM,KAAK,WAAW,0CAA0C,mBAAmB,GAAG,IAAI;GACxF,QAAQ;GACR,MAAM,KAAK,UAAU;IACnB,eAAe,SAAS;IACxB,YAAY,SAAS;IACtB,CAAC;GACH,CAAC;AAEF,UAAQ,IAAI,4BAA4B,SAAS,aAAa,OAAO,KAAK;;CAG5E,WAAoB;AAClB,SAAO,KAAK;;CAKd,AAAQ,mBAAmB,KAA2B,KAAgC;EAEpF,MAAM,YADM,IAAI,IAAI,IAAI,KAAM,oBAAoB,KAAK,cAAc,CAC/C,aAAa,IAAI,gBAAgB;AACvD,MAAI,UACF,KAAI,UAAU,KAAK,EAAE,gBAAgB,cAAc,CAAC,CAAC,IAAI,UAAU;MAEnE,KAAI,UAAU,IAAI,CAAC,IAAI,KAAK;;CAIhC,AAAQ,qBAAqB,KAA2B,KAAgC;EACtF,IAAI,OAAO;AACX,MAAI,GAAG,SAAS,UAAkB;AAAE,WAAQ,MAAM,UAAU;IAAI;AAChE,MAAI,GAAG,aAAa;AAClB,OAAI,UAAU,IAAI,CAAC,IAAI,KAAK;AAE5B,OAAI;IACF,MAAM,UAA8B,KAAK,MAAM,KAAK;AAEpD,QAAI,QAAQ,cAAc,qBAAqB,QAAQ,OACrD;IAGF,MAAM,MAAuB;KAC3B,IAAI,QAAQ;KACZ,MAAM,QAAQ;KACd,MAAM,QAAQ;KACd,WAAW,IAAI,KAAK,QAAQ,UAAU,CAAC,SAAS;KAChD,SAAS;KACT,UAAU,KAAK;KACf,UAAU;MACR,YAAY,QAAQ;MACpB,aAAa,QAAQ;MACtB;KACF;AAED,YAAQ,IAAI,gCAAgC,QAAQ,cAAc,QAAQ,OAAO;AACjF,YAAQ,IAAI,gBAAgB,IAAI,KAAK,GAAG;AAExC,SAAK,KAAK,WAAW,IAAI;YAClB,KAAK;AACZ,YAAQ,MAAM,6CAA6C,IAAI;;IAEjE;;CAGJ,MAAc,WAAW,MAAc,MAAsC;EAC3E,MAAM,MAAM,GAAG,KAAK,UAAU;EAC9B,MAAM,MAAM,MAAM,MAAM,KAAK;GAC3B,GAAG;GACH,SAAS;IACP,iBAAiB,UAAU,KAAK,OAAO;IACvC,gBAAgB;IAChB,GAAG,KAAK;IACT;GACF,CAAC;AAEF,MAAI,CAAC,IAAI,IAAI;GACX,MAAM,OAAO,MAAM,IAAI,MAAM,CAAC,YAAY,GAAG;AAC7C,SAAM,IAAI,MAAM,kBAAkB,IAAI,OAAO,IAAI,OAAO;;AAG1D,SAAO"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/WatiProvider.ts"],"sourcesContent":["import EventEmitter from 'eventemitter3';\nimport * as http from 'node:http';\nimport type { MessageProvider, IncomingMessage, OutgoingMessage } from '@operor/core';\nimport type { WatiProviderConfig, WatiTemplateParams } from './types.js';\n\nexport class WatiProvider extends EventEmitter implements MessageProvider {\n public readonly name = 'wati';\n private config: WatiProviderConfig;\n private server: http.Server | null = null;\n private connected = false;\n private baseUrl: string;\n private webhookPort: number;\n private webhookPath: string;\n\n constructor(config: WatiProviderConfig) {\n super();\n this.config = config;\n // v3 API: base URL without tenant ID — token resolves tenant\n this.baseUrl = (config.apiBaseUrl || 'https://live-mt-server.wati.io').replace(/\\/+$/, '');\n this.webhookPort = config.webhookPort || 3001;\n this.webhookPath = config.webhookPath || '/webhook/wati';\n }\n\n async connect(): Promise<void> {\n this.server = http.createServer((req, res) => {\n if (req.url?.startsWith(this.webhookPath)) {\n if (req.method === 'GET') {\n this.handleVerification(req, res);\n } else if (req.method === 'POST') {\n this.handleWebhookRequest(req, res);\n } else {\n res.writeHead(405).end();\n }\n } else {\n res.writeHead(404).end();\n }\n });\n\n await new Promise<void>((resolve) => {\n this.server!.listen(this.webhookPort, () => {\n this.connected = true;\n console.log(`āœ… WATI webhook server listening on port ${this.webhookPort}`);\n console.log(` Webhook path: ${this.webhookPath}`);\n resolve();\n });\n });\n\n this.emit('ready');\n }\n\n async disconnect(): Promise<void> {\n if (this.server) {\n await new Promise<void>((resolve, reject) => {\n this.server!.close((err) => (err ? reject(err) : resolve()));\n });\n this.server = null;\n }\n this.connected = false;\n console.log('WATI provider disconnected');\n }\n\n async sendMessage(to: string, message: OutgoingMessage): Promise<void> {\n if (!this.connected) {\n throw new Error('WATI provider not connected');\n }\n\n // v3 API: POST /api/ext/v3/conversations/messages/text\n // target is just the phone number; use Channel:Phone only if channelId is explicitly set\n await this.apiRequest('/api/ext/v3/conversations/messages/text', {\n method: 'POST',\n body: JSON.stringify({ target: to, text: message.text }),\n });\n\n console.log(`šŸ“¤ [WATI] Sent reply to ${to}`);\n }\n\n async sendTemplateMessage(to: string, template: WatiTemplateParams): Promise<void> {\n if (!this.connected) {\n throw new Error('WATI provider not connected');\n }\n\n // v3 API: POST /api/ext/v3/conversations/messages/template\n await this.apiRequest('/api/ext/v3/conversations/messages/template', {\n method: 'POST',\n body: JSON.stringify({\n target: to,\n template_name: template.templateName,\n parameters: template.parameters,\n }),\n });\n\n console.log(`šŸ“¤ [WATI] Sent template \"${template.templateName}\" to ${to}`);\n }\n\n isActive(): boolean {\n return this.connected;\n }\n\n // --- Private helpers ---\n\n private handleVerification(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url!, `http://localhost:${this.webhookPort}`);\n const challenge = url.searchParams.get('hub.challenge');\n if (challenge) {\n res.writeHead(200, { 'Content-Type': 'text/plain' }).end(challenge);\n } else {\n res.writeHead(200).end('ok');\n }\n }\n\n private handleWebhookRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n let body = '';\n req.on('data', (chunk: Buffer) => { body += chunk.toString(); });\n req.on('end', () => {\n res.writeHead(200).end('ok');\n\n try {\n const payload = JSON.parse(body);\n\n console.log(`šŸ“© [WATI] Webhook received:`, JSON.stringify(payload, null, 2));\n\n // WATI sends different payload structures — normalize\n const text = payload.text || payload.message?.text || payload.message?.body || '';\n const waId = payload.waId || payload.wa_id || payload.from || '';\n const messageId = payload.whatsappMessageId || payload.message_id || payload.id || '';\n const senderName = payload.senderName || payload.sender_name || payload.pushName || '';\n const timestamp = payload.timestamp || payload.created || '';\n const fromMe = payload.fromMe ?? payload.from_me ?? payload.owner ?? false;\n const messageType = payload.type || payload.messageType || 'text';\n\n if (!text || fromMe) {\n console.log(`ā­ļø [WATI] Skipped: ${!text ? 'no text' : 'fromMe'} (type: ${messageType})`);\n return;\n }\n\n const parsedTs = timestamp ? new Date(\n // If it's a pure number (unix epoch), parse as integer\n /^\\d+$/.test(String(timestamp)) ? Number(timestamp) * (/^\\d{10}$/.test(String(timestamp)) ? 1000 : 1) : timestamp\n ).getTime() : NaN;\n\n const msg: IncomingMessage = {\n id: messageId,\n from: waId,\n text,\n timestamp: Number.isFinite(parsedTs) ? parsedTs : Date.now(),\n channel: 'whatsapp',\n provider: this.name,\n metadata: {\n senderName,\n messageType,\n },\n };\n\n console.log(`\\nšŸ“„ [WATI] New message from ${senderName || waId}`);\n console.log(` Message: \"${msg.text}\"`);\n\n this.emit('message', msg);\n } catch (err) {\n console.error('āŒ [WATI] Failed to parse webhook payload:', err);\n console.error(' Raw body:', body);\n }\n });\n }\n\n private async apiRequest(path: string, init: RequestInit): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n const res = await fetch(url, {\n ...init,\n headers: {\n 'Authorization': `Bearer ${this.config.apiToken}`,\n 'Content-Type': 'application/json',\n ...init.headers,\n },\n });\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n console.error(`āŒ [WATI] API ${res.status} ${init.method || 'GET'} ${path}: ${text}`);\n throw new Error(`WATI API error ${res.status}: ${text}`);\n }\n\n console.log(`āœ… [WATI] API ${res.status} ${init.method || 'GET'} ${path}`);\n return res;\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@operor/provider-wati",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "WATI WhatsApp Business API provider for Agent OS",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "dependencies": {
9
9
  "eventemitter3": "^5.0.1",
10
- "@operor/core": "0.1.1"
10
+ "@operor/core": "0.2.0"
11
11
  },
12
12
  "devDependencies": {
13
13
  "tsdown": "^0.20.3",
@@ -1,7 +1,7 @@
1
1
  import EventEmitter from 'eventemitter3';
2
2
  import * as http from 'node:http';
3
3
  import type { MessageProvider, IncomingMessage, OutgoingMessage } from '@operor/core';
4
- import type { WatiProviderConfig, WatiWebhookPayload, WatiTemplateParams } from './types.js';
4
+ import type { WatiProviderConfig, WatiTemplateParams } from './types.js';
5
5
 
6
6
  export class WatiProvider extends EventEmitter implements MessageProvider {
7
7
  public readonly name = 'wati';
@@ -15,7 +15,8 @@ export class WatiProvider extends EventEmitter implements MessageProvider {
15
15
  constructor(config: WatiProviderConfig) {
16
16
  super();
17
17
  this.config = config;
18
- this.baseUrl = config.apiBaseUrl || `https://live-mt-server.wati.io/${config.tenantId}/api`;
18
+ // v3 API: base URL without tenant ID — token resolves tenant
19
+ this.baseUrl = (config.apiBaseUrl || 'https://live-mt-server.wati.io').replace(/\/+$/, '');
19
20
  this.webhookPort = config.webhookPort || 3001;
20
21
  this.webhookPath = config.webhookPath || '/webhook/wati';
21
22
  }
@@ -63,9 +64,11 @@ export class WatiProvider extends EventEmitter implements MessageProvider {
63
64
  throw new Error('WATI provider not connected');
64
65
  }
65
66
 
66
- await this.apiRequest(`/v1/sendSessionMessage/${to}`, {
67
+ // v3 API: POST /api/ext/v3/conversations/messages/text
68
+ // target is just the phone number; use Channel:Phone only if channelId is explicitly set
69
+ await this.apiRequest('/api/ext/v3/conversations/messages/text', {
67
70
  method: 'POST',
68
- body: JSON.stringify({ messageText: message.text }),
71
+ body: JSON.stringify({ target: to, text: message.text }),
69
72
  });
70
73
 
71
74
  console.log(`šŸ“¤ [WATI] Sent reply to ${to}`);
@@ -76,9 +79,11 @@ export class WatiProvider extends EventEmitter implements MessageProvider {
76
79
  throw new Error('WATI provider not connected');
77
80
  }
78
81
 
79
- await this.apiRequest(`/v2/sendTemplateMessage?whatsappNumber=${encodeURIComponent(to)}`, {
82
+ // v3 API: POST /api/ext/v3/conversations/messages/template
83
+ await this.apiRequest('/api/ext/v3/conversations/messages/template', {
80
84
  method: 'POST',
81
85
  body: JSON.stringify({
86
+ target: to,
82
87
  template_name: template.templateName,
83
88
  parameters: template.parameters,
84
89
  }),
@@ -110,31 +115,49 @@ export class WatiProvider extends EventEmitter implements MessageProvider {
110
115
  res.writeHead(200).end('ok');
111
116
 
112
117
  try {
113
- const payload: WatiWebhookPayload = JSON.parse(body);
118
+ const payload = JSON.parse(body);
114
119
 
115
- if (payload.eventType !== 'messageReceived' || payload.fromMe) {
120
+ console.log(`šŸ“© [WATI] Webhook received:`, JSON.stringify(payload, null, 2));
121
+
122
+ // WATI sends different payload structures — normalize
123
+ const text = payload.text || payload.message?.text || payload.message?.body || '';
124
+ const waId = payload.waId || payload.wa_id || payload.from || '';
125
+ const messageId = payload.whatsappMessageId || payload.message_id || payload.id || '';
126
+ const senderName = payload.senderName || payload.sender_name || payload.pushName || '';
127
+ const timestamp = payload.timestamp || payload.created || '';
128
+ const fromMe = payload.fromMe ?? payload.from_me ?? payload.owner ?? false;
129
+ const messageType = payload.type || payload.messageType || 'text';
130
+
131
+ if (!text || fromMe) {
132
+ console.log(`ā­ļø [WATI] Skipped: ${!text ? 'no text' : 'fromMe'} (type: ${messageType})`);
116
133
  return;
117
134
  }
118
135
 
136
+ const parsedTs = timestamp ? new Date(
137
+ // If it's a pure number (unix epoch), parse as integer
138
+ /^\d+$/.test(String(timestamp)) ? Number(timestamp) * (/^\d{10}$/.test(String(timestamp)) ? 1000 : 1) : timestamp
139
+ ).getTime() : NaN;
140
+
119
141
  const msg: IncomingMessage = {
120
- id: payload.whatsappMessageId,
121
- from: payload.waId,
122
- text: payload.text,
123
- timestamp: new Date(payload.timestamp).getTime(),
142
+ id: messageId,
143
+ from: waId,
144
+ text,
145
+ timestamp: Number.isFinite(parsedTs) ? parsedTs : Date.now(),
124
146
  channel: 'whatsapp',
125
147
  provider: this.name,
126
148
  metadata: {
127
- senderName: payload.senderName,
128
- messageType: payload.type,
149
+ senderName,
150
+ messageType,
129
151
  },
130
152
  };
131
153
 
132
- console.log(`\nšŸ“„ [WATI] New message from ${payload.senderName || payload.waId}`);
154
+ console.log(`\nšŸ“„ [WATI] New message from ${senderName || waId}`);
133
155
  console.log(` Message: "${msg.text}"`);
134
156
 
135
157
  this.emit('message', msg);
136
158
  } catch (err) {
137
159
  console.error('āŒ [WATI] Failed to parse webhook payload:', err);
160
+ console.error(' Raw body:', body);
138
161
  }
139
162
  });
140
163
  }
@@ -152,9 +175,11 @@ export class WatiProvider extends EventEmitter implements MessageProvider {
152
175
 
153
176
  if (!res.ok) {
154
177
  const text = await res.text().catch(() => '');
178
+ console.error(`āŒ [WATI] API ${res.status} ${init.method || 'GET'} ${path}: ${text}`);
155
179
  throw new Error(`WATI API error ${res.status}: ${text}`);
156
180
  }
157
181
 
182
+ console.log(`āœ… [WATI] API ${res.status} ${init.method || 'GET'} ${path}`);
158
183
  return res;
159
184
  }
160
185
  }
package/src/types.ts CHANGED
@@ -1,27 +1,16 @@
1
1
  export interface WatiProviderConfig {
2
2
  /** WATI API bearer token */
3
3
  apiToken: string;
4
- /** WATI tenant ID (appears in the API base URL) */
5
- tenantId: string;
4
+ /** WATI tenant ID — optional in v3 (used for Channel:PhoneNumber targeting in multi-channel setups) */
5
+ tenantId?: string;
6
6
  /** Port for the webhook HTTP server (default: 3001) */
7
7
  webhookPort?: number;
8
8
  /** Path the webhook listens on (default: /webhook/wati) */
9
9
  webhookPath?: string;
10
- /** Override the WATI API base URL (default: https://live-mt-server.wati.io/{tenantId}/api) */
10
+ /** Override the WATI API base URL (default: https://live-mt-server.wati.io) */
11
11
  apiBaseUrl?: string;
12
12
  }
13
13
 
14
- export interface WatiWebhookPayload {
15
- eventType: string;
16
- waId: string;
17
- text: string;
18
- whatsappMessageId: string;
19
- timestamp: string;
20
- senderName: string;
21
- type: string;
22
- fromMe: boolean;
23
- }
24
-
25
14
  export interface WatiTemplateParams {
26
15
  templateName: string;
27
16
  parameters: Array<{ name: string; value: string }>;