@copilotkit/channels-whatsapp 0.0.1

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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +236 -0
  3. package/dist/adapter.d.ts +57 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +242 -0
  6. package/dist/adapter.test.d.ts +2 -0
  7. package/dist/adapter.test.d.ts.map +1 -0
  8. package/dist/adapter.test.js +137 -0
  9. package/dist/built-in-context.d.ts +7 -0
  10. package/dist/built-in-context.d.ts.map +1 -0
  11. package/dist/built-in-context.js +19 -0
  12. package/dist/built-in-context.test.d.ts +2 -0
  13. package/dist/built-in-context.test.d.ts.map +1 -0
  14. package/dist/built-in-context.test.js +17 -0
  15. package/dist/built-in-tools.d.ts +8 -0
  16. package/dist/built-in-tools.d.ts.map +1 -0
  17. package/dist/built-in-tools.js +6 -0
  18. package/dist/client.d.ts +40 -0
  19. package/dist/client.d.ts.map +1 -0
  20. package/dist/client.js +108 -0
  21. package/dist/client.test.d.ts +2 -0
  22. package/dist/client.test.d.ts.map +1 -0
  23. package/dist/client.test.js +108 -0
  24. package/dist/conversation-store.d.ts +23 -0
  25. package/dist/conversation-store.d.ts.map +1 -0
  26. package/dist/conversation-store.js +41 -0
  27. package/dist/conversation-store.test.d.ts +2 -0
  28. package/dist/conversation-store.test.d.ts.map +1 -0
  29. package/dist/conversation-store.test.js +40 -0
  30. package/dist/download-files.d.ts +44 -0
  31. package/dist/download-files.d.ts.map +1 -0
  32. package/dist/download-files.js +66 -0
  33. package/dist/download-files.test.d.ts +2 -0
  34. package/dist/download-files.test.d.ts.map +1 -0
  35. package/dist/download-files.test.js +34 -0
  36. package/dist/event-renderer.d.ts +18 -0
  37. package/dist/event-renderer.d.ts.map +1 -0
  38. package/dist/event-renderer.js +110 -0
  39. package/dist/event-renderer.test.d.ts +2 -0
  40. package/dist/event-renderer.test.d.ts.map +1 -0
  41. package/dist/event-renderer.test.js +68 -0
  42. package/dist/history-store.d.ts +36 -0
  43. package/dist/history-store.d.ts.map +1 -0
  44. package/dist/history-store.js +17 -0
  45. package/dist/history-store.test.d.ts +2 -0
  46. package/dist/history-store.test.d.ts.map +1 -0
  47. package/dist/history-store.test.js +24 -0
  48. package/dist/index.d.ts +18 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +12 -0
  51. package/dist/interaction.d.ts +19 -0
  52. package/dist/interaction.d.ts.map +1 -0
  53. package/dist/interaction.js +56 -0
  54. package/dist/interaction.test.d.ts +2 -0
  55. package/dist/interaction.test.d.ts.map +1 -0
  56. package/dist/interaction.test.js +67 -0
  57. package/dist/markdown-to-wa.d.ts +15 -0
  58. package/dist/markdown-to-wa.d.ts.map +1 -0
  59. package/dist/markdown-to-wa.js +56 -0
  60. package/dist/markdown-to-wa.test.d.ts +2 -0
  61. package/dist/markdown-to-wa.test.d.ts.map +1 -0
  62. package/dist/markdown-to-wa.test.js +29 -0
  63. package/dist/render/budget.d.ts +34 -0
  64. package/dist/render/budget.d.ts.map +1 -0
  65. package/dist/render/budget.js +40 -0
  66. package/dist/render/budget.test.d.ts +2 -0
  67. package/dist/render/budget.test.d.ts.map +1 -0
  68. package/dist/render/budget.test.js +24 -0
  69. package/dist/render/message.d.ts +74 -0
  70. package/dist/render/message.d.ts.map +1 -0
  71. package/dist/render/message.js +247 -0
  72. package/dist/render/message.test.d.ts +2 -0
  73. package/dist/render/message.test.d.ts.map +1 -0
  74. package/dist/render/message.test.js +184 -0
  75. package/dist/types.d.ts +107 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +1 -0
  78. package/dist/webhook-listener.d.ts +16 -0
  79. package/dist/webhook-listener.d.ts.map +1 -0
  80. package/dist/webhook-listener.js +128 -0
  81. package/dist/webhook-listener.test.d.ts +2 -0
  82. package/dist/webhook-listener.test.d.ts.map +1 -0
  83. package/dist/webhook-listener.test.js +204 -0
  84. package/dist/webhook-server.d.ts +23 -0
  85. package/dist/webhook-server.d.ts.map +1 -0
  86. package/dist/webhook-server.js +99 -0
  87. package/dist/webhook-server.test.d.ts +2 -0
  88. package/dist/webhook-server.test.d.ts.map +1 -0
  89. package/dist/webhook-server.test.js +150 -0
  90. package/package.json +55 -0
@@ -0,0 +1,23 @@
1
+ import type { AddressInfo } from "node:net";
2
+ import type { WebhookBody } from "./types.js";
3
+ export interface WebhookServerArgs {
4
+ path: string;
5
+ verifyToken: string;
6
+ appSecret: string;
7
+ /** Called (async, after the 200 ack) with the parsed webhook body. */
8
+ onEvent: (body: WebhookBody) => Promise<void>;
9
+ }
10
+ /** Owns the inbound webhook HTTP server: GET verify + signed POST intake. */
11
+ export declare class WebhookServer {
12
+ private readonly args;
13
+ private server;
14
+ constructor(args: WebhookServerArgs);
15
+ address(): AddressInfo | null;
16
+ start(port: number): Promise<void>;
17
+ stop(): Promise<void>;
18
+ private handle;
19
+ private handleVerify;
20
+ private handlePost;
21
+ private verifySignature;
22
+ }
23
+ //# sourceMappingURL=webhook-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-server.d.ts","sourceRoot":"","sources":["../src/webhook-server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,OAAO,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,6EAA6E;AAC7E,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoB;IACzC,OAAO,CAAC,MAAM,CAAqB;gBAEvB,IAAI,EAAE,iBAAiB;IAInC,OAAO,IAAI,WAAW,GAAG,IAAI;IAKvB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ3B,OAAO,CAAC,MAAM;IA2Bd,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,UAAU;IA6BlB,OAAO,CAAC,eAAe;CASxB"}
@@ -0,0 +1,99 @@
1
+ import { createServer } from "node:http";
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
3
+ /** Owns the inbound webhook HTTP server: GET verify + signed POST intake. */
4
+ export class WebhookServer {
5
+ args;
6
+ server;
7
+ constructor(args) {
8
+ this.args = args;
9
+ }
10
+ address() {
11
+ const a = this.server?.address();
12
+ return a && typeof a === "object" ? a : null;
13
+ }
14
+ async start(port) {
15
+ this.server = createServer((req, res) => this.handle(req, res));
16
+ await new Promise((resolve) => this.server.listen(port, resolve));
17
+ }
18
+ async stop() {
19
+ if (!this.server)
20
+ return;
21
+ await new Promise((resolve, reject) => this.server.close((err) => (err ? reject(err) : resolve())));
22
+ this.server = undefined;
23
+ }
24
+ handle(req, res) {
25
+ const url = new URL(req.url ?? "/", "http://localhost");
26
+ // Health check — gives the Railway public domain a 200 to hit. The webhook
27
+ // itself lives at `this.args.path`; everything else 404s as before. Guard
28
+ // with `path !== "/"` so that if an operator configures the webhook AT the
29
+ // root, the Meta verify handshake (GET /?hub...) is not shadowed by health.
30
+ if (req.method === "GET" &&
31
+ url.pathname === "/" &&
32
+ this.args.path !== "/") {
33
+ res.statusCode = 200;
34
+ res.setHeader("content-type", "text/plain");
35
+ res.end("ok");
36
+ return;
37
+ }
38
+ if (url.pathname !== this.args.path) {
39
+ res.statusCode = 404;
40
+ res.end();
41
+ return;
42
+ }
43
+ if (req.method === "GET")
44
+ return this.handleVerify(url, res);
45
+ if (req.method === "POST")
46
+ return this.handlePost(req, res);
47
+ res.statusCode = 405;
48
+ res.end();
49
+ }
50
+ handleVerify(url, res) {
51
+ const mode = url.searchParams.get("hub.mode");
52
+ const token = url.searchParams.get("hub.verify_token");
53
+ const challenge = url.searchParams.get("hub.challenge") ?? "";
54
+ if (mode === "subscribe" && token === this.args.verifyToken) {
55
+ res.statusCode = 200;
56
+ res.end(challenge);
57
+ }
58
+ else {
59
+ res.statusCode = 403;
60
+ res.end();
61
+ }
62
+ }
63
+ handlePost(req, res) {
64
+ const chunks = [];
65
+ req.on("data", (c) => chunks.push(c));
66
+ req.on("end", () => {
67
+ const rawBuf = Buffer.concat(chunks);
68
+ const raw = rawBuf.toString("utf8");
69
+ const sig = req.headers["x-hub-signature-256"];
70
+ if (!this.verifySignature(rawBuf, typeof sig === "string" ? sig : undefined)) {
71
+ res.statusCode = 401;
72
+ res.end();
73
+ return;
74
+ }
75
+ // Ack immediately; Meta retries non-200 and can disable a flapping webhook.
76
+ res.statusCode = 200;
77
+ res.end();
78
+ let body;
79
+ try {
80
+ body = JSON.parse(raw);
81
+ }
82
+ catch {
83
+ return;
84
+ }
85
+ void this.args.onEvent(body).catch((err) => {
86
+ console.error("[whatsapp] onEvent failed:", err);
87
+ });
88
+ });
89
+ }
90
+ verifySignature(raw, signature) {
91
+ if (!signature || !signature.startsWith("sha256="))
92
+ return false;
93
+ const expected = "sha256=" +
94
+ createHmac("sha256", this.args.appSecret).update(raw).digest("hex");
95
+ const a = Buffer.from(signature);
96
+ const b = Buffer.from(expected);
97
+ return a.length === b.length && timingSafeEqual(a, b);
98
+ }
99
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=webhook-server.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-server.test.d.ts","sourceRoot":"","sources":["../src/webhook-server.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,150 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { createHmac } from "node:crypto";
3
+ import { WebhookServer } from "./webhook-server.js";
4
+ const sign = (secret, body) => "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
5
+ async function get(server, path) {
6
+ const port = server.address().port;
7
+ return fetch(`http://127.0.0.1:${port}${path}`);
8
+ }
9
+ async function post(server, path, body, sig) {
10
+ const port = server.address().port;
11
+ return fetch(`http://127.0.0.1:${port}${path}`, {
12
+ method: "POST",
13
+ headers: {
14
+ "Content-Type": "application/json",
15
+ ...(sig ? { "X-Hub-Signature-256": sig } : {}),
16
+ },
17
+ body,
18
+ });
19
+ }
20
+ describe("WebhookServer", () => {
21
+ it("echoes hub.challenge when verify token matches", async () => {
22
+ const server = new WebhookServer({
23
+ path: "/webhook",
24
+ verifyToken: "VTOK",
25
+ appSecret: "SECRET",
26
+ onEvent: async () => { },
27
+ });
28
+ await server.start(0);
29
+ try {
30
+ const res = await get(server, "/webhook?hub.mode=subscribe&hub.verify_token=VTOK&hub.challenge=12345");
31
+ expect(res.status).toBe(200);
32
+ expect(await res.text()).toBe("12345");
33
+ }
34
+ finally {
35
+ await server.stop();
36
+ }
37
+ });
38
+ it("rejects a verify with a wrong token", async () => {
39
+ const server = new WebhookServer({
40
+ path: "/webhook",
41
+ verifyToken: "VTOK",
42
+ appSecret: "S",
43
+ onEvent: async () => { },
44
+ });
45
+ await server.start(0);
46
+ try {
47
+ const res = await get(server, "/webhook?hub.mode=subscribe&hub.verify_token=WRONG&hub.challenge=1");
48
+ expect(res.status).toBe(403);
49
+ }
50
+ finally {
51
+ await server.stop();
52
+ }
53
+ });
54
+ it("accepts a correctly-signed POST and invokes onEvent", async () => {
55
+ const onEvent = vi.fn(async () => { });
56
+ const server = new WebhookServer({
57
+ path: "/webhook",
58
+ verifyToken: "V",
59
+ appSecret: "SECRET",
60
+ onEvent,
61
+ });
62
+ await server.start(0);
63
+ try {
64
+ const body = JSON.stringify({
65
+ object: "whatsapp_business_account",
66
+ entry: [],
67
+ });
68
+ const res = await post(server, "/webhook", body, sign("SECRET", body));
69
+ expect(res.status).toBe(200);
70
+ await new Promise((r) => setTimeout(r, 10));
71
+ expect(onEvent).toHaveBeenCalledOnce();
72
+ }
73
+ finally {
74
+ await server.stop();
75
+ }
76
+ });
77
+ it("rejects a POST with a bad signature", async () => {
78
+ const onEvent = vi.fn(async () => { });
79
+ const server = new WebhookServer({
80
+ path: "/webhook",
81
+ verifyToken: "V",
82
+ appSecret: "SECRET",
83
+ onEvent,
84
+ });
85
+ await server.start(0);
86
+ try {
87
+ const res = await post(server, "/webhook", "{}", "sha256=deadbeef");
88
+ expect(res.status).toBe(401);
89
+ expect(onEvent).not.toHaveBeenCalled();
90
+ }
91
+ finally {
92
+ await server.stop();
93
+ }
94
+ });
95
+ it("rejects a POST with a correctly-formatted but wrong signature", async () => {
96
+ const onEvent = vi.fn(async () => { });
97
+ const server = new WebhookServer({
98
+ path: "/webhook",
99
+ verifyToken: "V",
100
+ appSecret: "SECRET",
101
+ onEvent,
102
+ });
103
+ await server.start(0);
104
+ try {
105
+ const body = "{}";
106
+ // valid length (64 hex) but computed with the WRONG secret
107
+ const wrong = "sha256=" +
108
+ createHmac("sha256", "NOT_THE_SECRET").update(body).digest("hex");
109
+ const res = await post(server, "/webhook", body, wrong);
110
+ expect(res.status).toBe(401);
111
+ expect(onEvent).not.toHaveBeenCalled();
112
+ }
113
+ finally {
114
+ await server.stop();
115
+ }
116
+ });
117
+ it("responds 200 ok to GET / for health checks", async () => {
118
+ const server = new WebhookServer({
119
+ path: "/webhook",
120
+ verifyToken: "V",
121
+ appSecret: "S",
122
+ onEvent: async () => { },
123
+ });
124
+ await server.start(0);
125
+ try {
126
+ const res = await get(server, "/");
127
+ expect(res.status).toBe(200);
128
+ expect(await res.text()).toBe("ok");
129
+ }
130
+ finally {
131
+ await server.stop();
132
+ }
133
+ });
134
+ it("still 404s an unknown path (health route doesn't broaden 200)", async () => {
135
+ const server = new WebhookServer({
136
+ path: "/webhook",
137
+ verifyToken: "V",
138
+ appSecret: "S",
139
+ onEvent: async () => { },
140
+ });
141
+ await server.start(0);
142
+ try {
143
+ const res = await get(server, "/nope");
144
+ expect(res.status).toBe(404);
145
+ }
146
+ finally {
147
+ await server.stop();
148
+ }
149
+ });
150
+ });
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@copilotkit/channels-whatsapp",
3
+ "version": "0.0.1",
4
+ "description": "WhatsApp Cloud API platform adapter for CopilotKit JSX channels (@copilotkit/channels).",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/CopilotKit/CopilotKit.git",
9
+ "directory": "packages/channels-whatsapp"
10
+ },
11
+ "homepage": "https://github.com/CopilotKit/CopilotKit",
12
+ "keywords": [
13
+ "ai",
14
+ "agent",
15
+ "bot",
16
+ "whatsapp",
17
+ "cloud-api",
18
+ "copilotkit",
19
+ "ag-ui"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "type": "module",
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ }
35
+ },
36
+ "dependencies": {
37
+ "@ag-ui/client": "0.0.57",
38
+ "@copilotkit/channels-ui": "~0.1.0",
39
+ "@copilotkit/channels": "~0.1.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.10.0",
43
+ "typescript": "^5.6.3",
44
+ "vitest": "^4.1.3",
45
+ "@copilotkit/typescript-config": "^1.55.0-next.8"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc -p tsconfig.json",
49
+ "check-types": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.check.json",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "publint": "publint .",
53
+ "attw": "attw --pack . --profile esm-only"
54
+ }
55
+ }