@runfusion/fusion 0.23.0 → 0.24.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.
Files changed (204) hide show
  1. package/dist/bin.js +26610 -20597
  2. package/dist/client/assets/AgentDetailView-BwJaLqZh.css +1 -0
  3. package/dist/client/assets/AgentDetailView-gy_5SUj2.js +18 -0
  4. package/dist/client/assets/AgentsView-BkB9FiMT.js +29 -0
  5. package/dist/client/assets/{AgentsView-DSGQWObq.css → AgentsView-CV3vm7Qk.css} +1 -1
  6. package/dist/client/assets/ChatView-B_-B8fqu.js +1 -0
  7. package/dist/client/assets/ChatView-DwJAd5G1.css +1 -0
  8. package/dist/client/assets/{DevServerView-C9lzHrcT.js → DevServerView-BkvtjZBa.js} +1 -1
  9. package/dist/client/assets/{DirectoryPicker-aVdFaV37.js → DirectoryPicker-BK-KbnhP.js} +1 -1
  10. package/dist/client/assets/{DocumentsView-DIpg3NSP.js → DocumentsView-BEg1CQAk.js} +1 -1
  11. package/dist/client/assets/{DocumentsView-BrhyOdeE.css → DocumentsView-gv4zG3aT.css} +1 -1
  12. package/dist/client/assets/EvalsView-Berf9bQm.js +1 -0
  13. package/dist/client/assets/EvalsView-CUNJ1TLc.css +1 -0
  14. package/dist/client/assets/{agentSkills-DDHJnrkn.css → ExperimentalAgentOnboardingModal-B-APN_lM.css} +1 -1
  15. package/dist/client/assets/ExperimentalAgentOnboardingModal-jcInE50G.js +499 -0
  16. package/dist/client/assets/InsightsView-B0J4mhzV.css +1 -0
  17. package/dist/client/assets/InsightsView-BX5bSF1J.js +11 -0
  18. package/dist/client/assets/{MemoryView-nXlTqebk.js → MemoryView-CKElJY_3.js} +2 -2
  19. package/dist/client/assets/NodesView-DLUOBLf6.js +14 -0
  20. package/dist/client/assets/NodesView-DT4pXowv.css +1 -0
  21. package/dist/client/assets/{PiExtensionsManager-Buopv-jb.js → PiExtensionsManager-COlJf0Kx.js} +2 -2
  22. package/dist/client/assets/PluginManager-CfW55BF4.js +1 -0
  23. package/dist/client/assets/PluginManager-DtRQXia5.css +1 -0
  24. package/dist/client/assets/{ResearchView-_BHXUv2j.js → ResearchView-B256Lr8I.js} +1 -1
  25. package/dist/client/assets/SettingsModal-BeA_nQtW.js +31 -0
  26. package/dist/client/assets/SettingsModal-DzsLquBu.css +1 -0
  27. package/dist/client/assets/{SettingsModal-C89Ikhfm.js → SettingsModal-yRqM4DV8.js} +1 -1
  28. package/dist/client/assets/SetupWizardModal-uUZk3TKT.js +1 -0
  29. package/dist/client/assets/{SkillsView-hDpTBdFT.js → SkillsView-CP8JX0P_.js} +1 -1
  30. package/dist/client/assets/TodoView-Cx9cVhq7.css +1 -0
  31. package/dist/client/assets/TodoView-DCRIkDZ-.js +6 -0
  32. package/dist/client/assets/createLucideIcon-BazL2hk5.js +21 -0
  33. package/dist/client/assets/dashboard-view-BkTMSZYn.css +1 -0
  34. package/dist/client/assets/dashboard-view-CyWN-d02.js +63 -0
  35. package/dist/client/assets/dashboard-view-lR7YYmSC.js +21 -0
  36. package/dist/client/assets/{folder-open-usZkXdq2.js → folder-open-DHjELt8-.js} +1 -1
  37. package/dist/client/assets/index-CQyVRLOb.js +692 -0
  38. package/dist/client/assets/index-CxA2Nn0_.css +1 -0
  39. package/dist/client/assets/projectDetection-G3XuxD2X.js +1 -0
  40. package/dist/client/assets/{star-BAT_ObKE.js → star-DYesq1AV.js} +1 -1
  41. package/dist/client/assets/{upload-BC2YKNEV.js → upload-DTWF3Db5.js} +1 -1
  42. package/dist/client/assets/{users-Dkd4rtrN.js → users--syrel4l.js} +1 -1
  43. package/dist/client/index.html +12 -20
  44. package/dist/client/theme-data.css +106 -0
  45. package/dist/client/version.json +1 -1
  46. package/dist/droid-cli/package.json +1 -1
  47. package/dist/extension.js +14287 -9568
  48. package/dist/pi-claude-cli/package.json +1 -1
  49. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +218 -0
  50. package/dist/plugins/fusion-plugin-cursor-runtime/manifest.json +6 -0
  51. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +11 -0
  52. package/dist/plugins/fusion-plugin-dependency-graph/manifest.json +1 -1
  53. package/dist/plugins/fusion-plugin-dependency-graph/package.json +6 -4
  54. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.css +58 -0
  55. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.tsx +301 -0
  56. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphHighlight.css +27 -0
  57. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.css +157 -0
  58. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx +126 -0
  59. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.css +35 -0
  60. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.tsx +36 -0
  61. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx +112 -0
  62. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.persistence.test.tsx +115 -0
  63. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.test.tsx +128 -0
  64. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.drag.test.tsx +82 -0
  65. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.test.tsx +307 -0
  66. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphToolbar.test.tsx +60 -0
  67. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx +75 -0
  68. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filtering.test.tsx +62 -0
  69. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filters.test.ts +78 -0
  70. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/graphPositionStorage.test.ts +95 -0
  71. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/host-integration.test.ts +74 -0
  72. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/index.test.ts +58 -0
  73. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/interactions.test.tsx +121 -0
  74. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/layout.test.ts +70 -0
  75. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/persistence.test.tsx +89 -0
  76. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphData.test.ts +86 -0
  77. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphInteraction.test.ts +167 -0
  78. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphPositions.test.ts +66 -0
  79. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useNodeDrag.test.ts +81 -0
  80. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-interop.d.ts +35 -0
  81. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx +19 -0
  82. package/dist/plugins/fusion-plugin-dependency-graph/src/edges.tsx +70 -0
  83. package/dist/plugins/fusion-plugin-dependency-graph/src/filters.ts +8 -0
  84. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/__tests__/useDependencyChain.test.ts +53 -0
  85. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useDependencyChain.ts +60 -0
  86. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useGraphPositions.ts +45 -0
  87. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useNodeDrag.ts +114 -0
  88. package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +1 -2
  89. package/dist/plugins/fusion-plugin-dependency-graph/src/layout.ts +91 -0
  90. package/dist/plugins/fusion-plugin-dependency-graph/src/styles/drag.css +15 -0
  91. package/dist/plugins/fusion-plugin-dependency-graph/src/types.ts +21 -0
  92. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphData.ts +17 -0
  93. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphInteraction.ts +292 -0
  94. package/dist/plugins/fusion-plugin-dependency-graph/src/utils/graphPositionStorage.ts +65 -0
  95. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +136680 -0
  96. package/dist/plugins/fusion-plugin-droid-runtime/manifest.json +13 -0
  97. package/dist/plugins/fusion-plugin-droid-runtime/mcp-schema-server.cjs +49 -0
  98. package/dist/plugins/fusion-plugin-droid-runtime/package.json +11 -0
  99. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  100. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +93 -6
  101. package/dist/plugins/fusion-plugin-openclaw-runtime/mcp-schema-server.cjs +59 -0
  102. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  103. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  104. package/dist/plugins/fusion-plugin-reports/manifest.json +33 -0
  105. package/dist/plugins/fusion-plugin-reports/package.json +26 -0
  106. package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +51 -0
  107. package/dist/plugins/fusion-plugin-reports/src/__tests__/review-panel.test.ts +166 -0
  108. package/dist/plugins/fusion-plugin-reports/src/__tests__/settings.test.ts +157 -0
  109. package/dist/plugins/fusion-plugin-reports/src/index.ts +41 -0
  110. package/dist/plugins/fusion-plugin-reports/src/review-panel.ts +294 -0
  111. package/dist/plugins/fusion-plugin-reports/src/review-types.ts +75 -0
  112. package/dist/plugins/fusion-plugin-reports/src/settings.ts +105 -0
  113. package/dist/plugins/fusion-plugin-roadmap/manifest.json +16 -0
  114. package/dist/plugins/fusion-plugin-roadmap/package.json +48 -0
  115. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +101 -0
  116. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +92 -0
  117. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +48 -0
  118. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +31 -0
  119. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +1299 -0
  120. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +2559 -0
  121. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +1144 -0
  122. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +1756 -0
  123. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +70 -0
  124. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +7 -0
  125. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +1 -0
  126. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +8 -0
  127. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +1188 -0
  128. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +20 -0
  129. package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +6 -0
  130. package/dist/plugins/fusion-plugin-roadmap/src/index.ts +74 -0
  131. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +1 -0
  132. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +41 -0
  133. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +15 -0
  134. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +15 -0
  135. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +283 -0
  136. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +1 -0
  137. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +21 -0
  138. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +1 -0
  139. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +310 -0
  140. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +5 -0
  141. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +1 -0
  142. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +361 -0
  143. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +1 -0
  144. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +408 -0
  145. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +68 -0
  146. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +1 -0
  147. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +300 -0
  148. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +1 -0
  149. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +381 -0
  150. package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +3 -0
  151. package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +1 -0
  152. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +445 -0
  153. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +334 -0
  154. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +1318 -0
  155. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +163 -0
  156. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +37 -0
  157. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +1 -0
  158. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +188 -0
  159. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +1 -0
  160. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +311 -0
  161. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +299 -0
  162. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +1 -0
  163. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +765 -0
  164. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +1 -0
  165. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +1001 -0
  166. package/dist/plugins/fusion-plugin-whatsapp-chat/manifest.json +8 -0
  167. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +34 -0
  168. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/auth-state.test.ts +99 -0
  169. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/connection.test.ts +145 -0
  170. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/index.test.ts +216 -0
  171. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/reply.test.ts +52 -0
  172. package/dist/plugins/fusion-plugin-whatsapp-chat/src/auth-state.ts +89 -0
  173. package/dist/plugins/fusion-plugin-whatsapp-chat/src/connection.ts +253 -0
  174. package/dist/plugins/fusion-plugin-whatsapp-chat/src/index.ts +262 -0
  175. package/dist/plugins/fusion-plugin-whatsapp-chat/src/qrcode.d.ts +1 -0
  176. package/dist/plugins/fusion-plugin-whatsapp-chat/src/reply.ts +37 -0
  177. package/package.json +2 -2
  178. package/skill/fusion/SKILL.md +2 -2
  179. package/skill/fusion/references/engine-tools.md +3 -0
  180. package/skill/fusion/references/extension-tools.md +39 -0
  181. package/skill/fusion/references/fusion-capabilities.md +3 -0
  182. package/dist/client/assets/AgentDetailView-C1XceMgi.js +0 -18
  183. package/dist/client/assets/AgentDetailView-CeO_1MK7.css +0 -1
  184. package/dist/client/assets/AgentsView-Deh125ss.js +0 -527
  185. package/dist/client/assets/ChatView-7D_RQDqT.js +0 -1
  186. package/dist/client/assets/InsightsView-AWo5o_81.css +0 -1
  187. package/dist/client/assets/InsightsView-jKjEFAx_.js +0 -11
  188. package/dist/client/assets/NodesView-Di2SvOhg.js +0 -14
  189. package/dist/client/assets/NodesView-fXqDk9ur.css +0 -1
  190. package/dist/client/assets/PluginManager-B9-NbQ8f.js +0 -1
  191. package/dist/client/assets/PluginManager-C1DbPaar.css +0 -1
  192. package/dist/client/assets/RoadmapsView-DHWjUoc8.js +0 -6
  193. package/dist/client/assets/SettingsModal-DHitIpsa.css +0 -1
  194. package/dist/client/assets/SettingsModal-DR_yirvK.js +0 -31
  195. package/dist/client/assets/SetupWizardModal-BtDMY9pa.js +0 -1
  196. package/dist/client/assets/agentSkills-B-w5wFHh.js +0 -1
  197. package/dist/client/assets/index-Bc6ZdGMz.css +0 -1
  198. package/dist/client/assets/index-D__RMku8.js +0 -694
  199. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.css +0 -141
  200. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.tsx +0 -428
  201. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraphView.test.tsx +0 -261
  202. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/storage.test.ts +0 -41
  203. package/dist/plugins/fusion-plugin-dependency-graph/src/storage.ts +0 -22
  204. /package/dist/client/assets/{RoadmapsView-DdGlfuu-.css → dashboard-view-DdGlfuu-.css} +0 -0
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "fusion-plugin-whatsapp-chat",
3
+ "name": "WhatsApp Chat",
4
+ "version": "0.1.0",
5
+ "description": "WhatsApp Web (multi-device) bridge — pairs via QR/code, no Meta Cloud webhook required.",
6
+ "author": "Fusion Team",
7
+ "fusionVersion": ">=0.1.0"
8
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@fusion-plugin-examples/whatsapp-chat",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "WhatsApp Web (Baileys) chat bridge for Fusion agents",
6
+ "keywords": [
7
+ "fusion-plugin",
8
+ "whatsapp",
9
+ "chat",
10
+ "integration"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./src/index.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "private": true,
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "test": "vitest run --silent=passed-only --reporter=dot"
22
+ },
23
+ "dependencies": {
24
+ "@fusion/plugin-sdk": "workspace:*",
25
+ "@whiskeysockets/baileys": "^6.7.21",
26
+ "pino": "^9.9.0",
27
+ "qrcode": "^1.5.4"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^25.5.2",
31
+ "typescript": "^5.7.0",
32
+ "vitest": "^3.2.4"
33
+ }
34
+ }
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { clearAuthState, createPluginDbAuthState } from "../auth-state.js";
3
+
4
+ function createInMemoryDb() {
5
+ const creds = new Map<string, string>();
6
+ const keys = new Map<string, string>();
7
+ const makeKey = (category: string, id: string) => `${category}:${id}`;
8
+
9
+ return {
10
+ prepare(sql: string) {
11
+ return {
12
+ get: (...args: unknown[]) => {
13
+ if (sql.includes("FROM whatsapp_auth_creds")) {
14
+ const value = creds.get("creds");
15
+ return value ? { value } : undefined;
16
+ }
17
+ if (sql.includes("FROM whatsapp_auth_keys")) {
18
+ const key = makeKey(args[0] as string, args[1] as string);
19
+ const value = keys.get(key);
20
+ return value ? { value } : undefined;
21
+ }
22
+ return undefined;
23
+ },
24
+ run: (...args: unknown[]) => {
25
+ if (sql.includes("INSERT INTO whatsapp_auth_creds")) {
26
+ creds.set("creds", args[0] as string);
27
+ }
28
+ if (sql.includes("DELETE FROM whatsapp_auth_creds")) {
29
+ creds.clear();
30
+ }
31
+ if (sql.includes("INSERT INTO whatsapp_auth_keys")) {
32
+ keys.set(makeKey(args[0] as string, args[1] as string), args[2] as string);
33
+ }
34
+ if (sql.includes("DELETE FROM whatsapp_auth_keys WHERE category")) {
35
+ keys.delete(makeKey(args[0] as string, args[1] as string));
36
+ }
37
+ if (sql.includes("DELETE FROM whatsapp_auth_keys")) {
38
+ keys.clear();
39
+ }
40
+ },
41
+ };
42
+ },
43
+ exec() {},
44
+ _creds: creds,
45
+ _keys: keys,
46
+ };
47
+ }
48
+
49
+ describe("auth-state", () => {
50
+ it("round-trips creds", async () => {
51
+ const db = createInMemoryDb();
52
+ const auth = createPluginDbAuthState(db as any);
53
+ auth.state.creds.me = { id: "123@s.whatsapp.net", name: "Fusion" } as any;
54
+ await auth.saveCreds();
55
+
56
+ const next = createPluginDbAuthState(db as any);
57
+ expect(next.state.creds.me?.id).toBe("123@s.whatsapp.net");
58
+ });
59
+
60
+ it("sets, gets, and deletes key categories", async () => {
61
+ const db = createInMemoryDb();
62
+ const auth = createPluginDbAuthState(db as any);
63
+
64
+ await auth.state.keys.set({
65
+ session: { alpha: { foo: "bar" } as any },
66
+ "sender-key": { beta: { baz: "qux" } as any },
67
+ });
68
+
69
+ const loaded = await auth.state.keys.get("session", ["alpha", "missing"]);
70
+ expect((loaded as any).alpha.foo).toBe("bar");
71
+ expect((loaded as any).missing).toBeUndefined();
72
+
73
+ await auth.state.keys.set({ session: { alpha: null } });
74
+ const removed = await auth.state.keys.get("session", ["alpha"]);
75
+ expect((removed as any).alpha).toBeUndefined();
76
+ });
77
+
78
+ it("clears auth state", async () => {
79
+ const db = createInMemoryDb();
80
+ const auth = createPluginDbAuthState(db as any);
81
+ auth.state.creds.me = { id: "123@s.whatsapp.net", name: "Fusion" } as any;
82
+ await auth.saveCreds();
83
+ await auth.state.keys.set({ session: { alpha: { ok: true } as any } });
84
+
85
+ clearAuthState(db as any);
86
+
87
+ expect(db._creds.size).toBe(0);
88
+ expect(db._keys.size).toBe(0);
89
+ });
90
+
91
+ it("handles corrupt json gracefully", async () => {
92
+ const db = createInMemoryDb();
93
+ db._keys.set("session:bad", "not-json");
94
+
95
+ const auth = createPluginDbAuthState(db as any);
96
+ const loaded = await auth.state.keys.get("session", ["bad"]);
97
+ expect((loaded as any).bad).toBeUndefined();
98
+ });
99
+ });
@@ -0,0 +1,145 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mockState = vi.hoisted(() => {
4
+ const handlers = new Map<string, (payload: any) => void>();
5
+ const sendMessage = vi.fn();
6
+ const end = vi.fn();
7
+ const logout = vi.fn();
8
+ const requestPairingCode = vi.fn().mockResolvedValue("123-456");
9
+ const makeWASocket = vi.fn(() => ({
10
+ ev: {
11
+ on: (name: string, handler: (payload: any) => void) => handlers.set(name, handler),
12
+ off: (name: string) => handlers.delete(name),
13
+ },
14
+ user: { id: "15550001111@s.whatsapp.net" },
15
+ sendMessage,
16
+ end,
17
+ logout,
18
+ requestPairingCode,
19
+ }));
20
+ return { handlers, sendMessage, end, logout, requestPairingCode, makeWASocket };
21
+ });
22
+
23
+ vi.mock("@whiskeysockets/baileys", () => ({
24
+ default: mockState.makeWASocket,
25
+ makeWASocket: mockState.makeWASocket,
26
+ DisconnectReason: { loggedOut: 401 },
27
+ BufferJSON: { reviver: undefined, replacer: undefined },
28
+ initAuthCreds: () => ({}),
29
+ }));
30
+
31
+ vi.mock("qrcode", () => ({
32
+ default: { toDataURL: vi.fn().mockResolvedValue("data:image/png;base64,abc") },
33
+ }));
34
+
35
+ import { WhatsAppConnection } from "../connection.js";
36
+
37
+ function createInMemoryDb() {
38
+ const sessions = new Map<string, string>();
39
+ const dedupe = new Set<string>();
40
+ const creds = new Map<string, string>();
41
+ const keys = new Map<string, string>();
42
+ return {
43
+ exec() {},
44
+ prepare(sql: string) {
45
+ return {
46
+ get: (...args: unknown[]) => {
47
+ if (sql.includes("FROM whatsapp_chat_sessions")) {
48
+ const history = sessions.get(args[0] as string);
49
+ return history ? { history } : undefined;
50
+ }
51
+ if (sql.includes("FROM whatsapp_chat_dedupe")) return dedupe.has(args[0] as string) ? { found: 1 } : undefined;
52
+ if (sql.includes("FROM whatsapp_auth_creds")) return creds.get("creds") ? { value: creds.get("creds") } : undefined;
53
+ if (sql.includes("FROM whatsapp_auth_keys")) return keys.get(`${args[0]}:${args[1]}`) ? { value: keys.get(`${args[0]}:${args[1]}`) } : undefined;
54
+ return undefined;
55
+ },
56
+ run: (...args: unknown[]) => {
57
+ if (sql.includes("whatsapp_chat_sessions")) sessions.set(args[0] as string, args[1] as string);
58
+ if (sql.includes("whatsapp_chat_dedupe")) dedupe.add(args[0] as string);
59
+ if (sql.includes("INSERT INTO whatsapp_auth_creds")) creds.set("creds", args[0] as string);
60
+ if (sql.includes("DELETE FROM whatsapp_auth_creds")) creds.clear();
61
+ if (sql.includes("INSERT INTO whatsapp_auth_keys")) keys.set(`${args[0]}:${args[1]}`, args[2] as string);
62
+ if (sql.includes("DELETE FROM whatsapp_auth_keys WHERE category")) keys.delete(`${args[0]}:${args[1]}`);
63
+ if (sql.includes("DELETE FROM whatsapp_auth_keys")) keys.clear();
64
+ },
65
+ };
66
+ },
67
+ };
68
+ }
69
+
70
+ function makeCtx(settings: Record<string, unknown> = {}) {
71
+ return {
72
+ pluginId: "fusion-plugin-whatsapp-chat",
73
+ settings: { allowedSenders: ["15550001111"], ...settings },
74
+ taskStore: { getRootDir: () => "/tmp", getPluginStore: () => ({}) },
75
+ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
76
+ emitEvent: vi.fn(),
77
+ } as any;
78
+ }
79
+
80
+ describe("WhatsAppConnection", () => {
81
+ beforeEach(() => {
82
+ mockState.handlers.clear();
83
+ mockState.makeWASocket.mockClear();
84
+ mockState.sendMessage.mockClear();
85
+ mockState.end.mockClear();
86
+ });
87
+
88
+ it("starts and stops idempotently", async () => {
89
+ const connection = new WhatsAppConnection(makeCtx(), "0.1.0", vi.fn().mockResolvedValue("reply"), createInMemoryDb() as any);
90
+ await connection.start();
91
+ await connection.stop();
92
+ await connection.stop();
93
+ expect(mockState.makeWASocket).toHaveBeenCalledTimes(1);
94
+ expect(mockState.end).toHaveBeenCalledTimes(1);
95
+ });
96
+
97
+ it("exposes qr updates", async () => {
98
+ const connection = new WhatsAppConnection(makeCtx(), "0.1.0", vi.fn().mockResolvedValue("reply"), createInMemoryDb() as any);
99
+ await connection.start();
100
+ await mockState.handlers.get("connection.update")?.({ qr: "abc" });
101
+ expect(connection.getStatus()).toMatchObject({ state: "awaiting-qr", qr: "abc" });
102
+ });
103
+
104
+ it("reconnects on close unless logged out", async () => {
105
+ vi.useFakeTimers();
106
+ const connection = new WhatsAppConnection(makeCtx(), "0.1.0", vi.fn().mockResolvedValue("reply"), createInMemoryDb() as any);
107
+ await connection.start();
108
+ await mockState.handlers.get("connection.update")?.({ connection: "close", lastDisconnect: { error: new Error("boom") } });
109
+ vi.advanceTimersByTime(1000);
110
+ expect(mockState.makeWASocket).toHaveBeenCalledTimes(2);
111
+
112
+ await mockState.handlers.get("connection.update")?.({ connection: "close", lastDisconnect: { error: { output: { statusCode: 401 } } } });
113
+ vi.advanceTimersByTime(1000);
114
+ expect(mockState.makeWASocket).toHaveBeenCalledTimes(2);
115
+ vi.useRealTimers();
116
+ });
117
+
118
+ it("drops unsupported inbound traffic", async () => {
119
+ const reply = vi.fn().mockResolvedValue("hello");
120
+ const connection = new WhatsAppConnection(makeCtx(), "0.1.0", reply, createInMemoryDb() as any);
121
+ await connection.start();
122
+ const upsert = mockState.handlers.get("messages.upsert")!;
123
+ await upsert({ type: "notify", messages: [{ key: { remoteJid: "abc@g.us", id: "1", fromMe: false }, message: { conversation: "hi" } }] });
124
+ await upsert({ type: "notify", messages: [{ key: { remoteJid: "15550001111@s.whatsapp.net", id: "2", fromMe: true }, message: { conversation: "hi" } }] });
125
+ await upsert({ type: "notify", messages: [{ key: { remoteJid: "15550001111@s.whatsapp.net", id: "3", fromMe: false }, message: {} }] });
126
+ expect(reply).not.toHaveBeenCalled();
127
+ });
128
+
129
+ it("dedupes and handles reply failure with fallback", async () => {
130
+ const reply = vi.fn().mockRejectedValue(new Error("nope"));
131
+ const connection = new WhatsAppConnection(makeCtx(), "0.1.0", reply, createInMemoryDb() as any);
132
+ await connection.start();
133
+ const payload = { type: "notify", messages: [{ key: { remoteJid: "15550001111@s.whatsapp.net", id: "m-1", fromMe: false }, message: { conversation: "hi" } }] };
134
+ await mockState.handlers.get("messages.upsert")?.(payload);
135
+ await mockState.handlers.get("messages.upsert")?.(payload);
136
+ expect(reply).toHaveBeenCalledTimes(1);
137
+ expect(mockState.sendMessage).toHaveBeenCalledWith("15550001111@s.whatsapp.net", { text: "Sorry, I hit an internal error while processing that message." });
138
+ });
139
+
140
+ it("splits oversized messages", () => {
141
+ const chunks = WhatsAppConnection.splitMessageForWhatsapp("x".repeat(9000));
142
+ expect(chunks.length).toBeGreaterThan(2);
143
+ expect(chunks[0].length).toBeLessThanOrEqual(4096);
144
+ });
145
+ });
@@ -0,0 +1,216 @@
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+ import type { PluginContext } from "@fusion/plugin-sdk";
3
+
4
+ const connectionInstances: Array<{
5
+ start: ReturnType<typeof vi.fn>;
6
+ stop: ReturnType<typeof vi.fn>;
7
+ getStatus: ReturnType<typeof vi.fn>;
8
+ requestPairingCode: ReturnType<typeof vi.fn>;
9
+ logout: ReturnType<typeof vi.fn>;
10
+ }> = [];
11
+
12
+ vi.mock("../connection.js", () => {
13
+ const ctor = vi.fn((ctx: PluginContext) => {
14
+ const root = ctx.taskStore.getRootDir();
15
+ const instance = {
16
+ start: vi.fn(async () => {}),
17
+ stop: vi.fn(async () => {}),
18
+ getStatus: vi.fn(() => ({ state: "open", jid: root })),
19
+ requestPairingCode: vi.fn(async () => "123-456"),
20
+ logout: vi.fn(async () => {}),
21
+ };
22
+ connectionInstances.push(instance);
23
+ return instance;
24
+ });
25
+ (ctor as unknown as { splitMessageForWhatsapp: (text: string) => string[] }).splitMessageForWhatsapp =
26
+ (text: string) => (text.length > 4096 ? [text.slice(0, 4096), text.slice(4096, 8192), text.slice(8192)] : [text]);
27
+ return { WhatsAppConnection: ctor };
28
+ });
29
+
30
+ import plugin, { ensureSchema, getDedupeRetentionDays, markProcessed, splitMessageForWhatsapp, wasProcessed } from "../index.js";
31
+ import { WhatsAppConnection } from "../connection.js";
32
+
33
+ function createInMemoryDb() {
34
+ const dedupe = new Map<string, { sender: string; receivedAt: string }>();
35
+
36
+ return {
37
+ exec(_sql: string) {},
38
+ prepare(sql: string) {
39
+ return {
40
+ get: (...args: unknown[]) => {
41
+ if (sql.includes("FROM whatsapp_chat_dedupe") && sql.includes("messageId = ?")) {
42
+ const row = dedupe.get(args[0] as string);
43
+ return row ? { found: 1, ...row } : undefined;
44
+ }
45
+ return undefined;
46
+ },
47
+ run: (...args: unknown[]) => {
48
+ if (sql.includes("INSERT INTO whatsapp_chat_dedupe")) {
49
+ dedupe.set(args[0] as string, {
50
+ sender: args[1] as string,
51
+ receivedAt: args[2] as string,
52
+ });
53
+ }
54
+ if (sql.includes("DELETE FROM whatsapp_chat_dedupe WHERE receivedAt < ?")) {
55
+ const cutoff = args[0] as string;
56
+ for (const [id, row] of dedupe.entries()) {
57
+ if (row.receivedAt < cutoff) dedupe.delete(id);
58
+ }
59
+ }
60
+ },
61
+ };
62
+ },
63
+ _dedupe: dedupe,
64
+ };
65
+ }
66
+
67
+ describe("whatsapp plugin", () => {
68
+ beforeEach(() => {
69
+ connectionInstances.length = 0;
70
+ vi.clearAllMocks();
71
+ });
72
+ it("registers schema init hook", () => {
73
+ expect(plugin.hooks?.onSchemaInit).toBeDefined();
74
+ });
75
+
76
+ it("registers pairing routes", () => {
77
+ const paths = (plugin.routes ?? []).map((route) => `${route.method} ${route.path}`);
78
+ expect(paths).toContain("GET /status");
79
+ expect(paths).toContain("GET /qr");
80
+ expect(paths).toContain("POST /pair-code");
81
+ expect(paths).toContain("POST /logout");
82
+ });
83
+
84
+ it("uses only pairing-era settings", () => {
85
+ const schema = plugin.manifest.settingsSchema ?? {};
86
+ expect(Object.keys(schema).sort()).toEqual([
87
+ "agentSystemPrompt",
88
+ "allowedSenders",
89
+ "dedupeRetentionDays",
90
+ "historyTurnLimit",
91
+ "pairingMode",
92
+ "pairingPhoneNumber",
93
+ ]);
94
+ });
95
+
96
+ it("splits oversized messages", () => {
97
+ const chunks = splitMessageForWhatsapp("x".repeat(9000));
98
+ expect(chunks.length).toBeGreaterThan(2);
99
+ expect(chunks[0].length).toBeLessThanOrEqual(4096);
100
+ });
101
+ });
102
+
103
+ describe("multi-project isolation", () => {
104
+ it("keeps project contexts isolated with shared plugin id", async () => {
105
+ const db = createInMemoryDb();
106
+ const makeCtx = (rootDir: string): PluginContext => ({
107
+ pluginId: "fusion-plugin-whatsapp-chat",
108
+ settings: {},
109
+ logger: {
110
+ info: vi.fn(),
111
+ warn: vi.fn(),
112
+ error: vi.fn(),
113
+ debug: vi.fn(),
114
+ },
115
+ emitEvent: vi.fn(),
116
+ taskStore: {
117
+ getRootDir: () => rootDir,
118
+ getPluginStore: () => ({
119
+ db,
120
+ }),
121
+ } as unknown as PluginContext["taskStore"],
122
+ });
123
+
124
+ const ctxA = makeCtx("/repo-a");
125
+ const ctxB = makeCtx("/repo-b");
126
+
127
+ await plugin.hooks!.onLoad!(ctxA);
128
+ await plugin.hooks!.onLoad!(ctxB);
129
+
130
+ expect(WhatsAppConnection).toHaveBeenCalledTimes(2);
131
+ expect(connectionInstances[0]?.start).toHaveBeenCalledTimes(1);
132
+ expect(connectionInstances[1]?.start).toHaveBeenCalledTimes(1);
133
+
134
+ const statusRoute = plugin.routes!.find((route) => route.method === "GET" && route.path === "/status")!;
135
+
136
+ const statusA = await statusRoute.handler({} as never, ctxA) as { status: number; body: unknown };
137
+ const statusB = await statusRoute.handler({} as never, ctxB) as { status: number; body: unknown };
138
+ expect(statusA.status).toBe(200);
139
+ expect((statusA.body as { jid: string }).jid).toBe("/repo-a");
140
+ expect(statusB.status).toBe(200);
141
+ expect((statusB.body as { jid: string }).jid).toBe("/repo-b");
142
+
143
+ await plugin.hooks!.onUnload!(ctxA);
144
+ expect(connectionInstances[0]?.stop).toHaveBeenCalledTimes(1);
145
+ expect(connectionInstances[1]?.stop).not.toHaveBeenCalled();
146
+
147
+ const afterUnloadA = await statusRoute.handler({} as never, ctxA) as { status: number; body: unknown };
148
+ const afterUnloadB = await statusRoute.handler({} as never, ctxB) as { status: number; body: unknown };
149
+ expect(afterUnloadA.status).toBe(503);
150
+ expect(afterUnloadB.status).toBe(200);
151
+ expect((afterUnloadB.body as { jid: string }).jid).toBe("/repo-b");
152
+
153
+ await plugin.hooks!.onUnload!(ctxB);
154
+ expect(connectionInstances[1]?.stop).toHaveBeenCalledTimes(1);
155
+
156
+ const finalStatusA = await statusRoute.handler({} as never, ctxA) as { status: number; body: unknown };
157
+ const finalStatusB = await statusRoute.handler({} as never, ctxB) as { status: number; body: unknown };
158
+ expect(finalStatusA.status).toBe(503);
159
+ expect(finalStatusB.status).toBe(503);
160
+ });
161
+ });
162
+
163
+ describe("markProcessed retention", () => {
164
+ it("prunes rows older than retention and keeps recent rows", () => {
165
+ const db = createInMemoryDb();
166
+ ensureSchema(db as any);
167
+ const now = Date.now();
168
+
169
+ db.prepare("INSERT INTO whatsapp_chat_dedupe(messageId, sender, receivedAt) VALUES(?, ?, ?)").run(
170
+ "old-id",
171
+ "sender",
172
+ new Date(now - 30 * 86_400_000).toISOString(),
173
+ );
174
+ db.prepare("INSERT INTO whatsapp_chat_dedupe(messageId, sender, receivedAt) VALUES(?, ?, ?)").run(
175
+ "recent-id",
176
+ "sender",
177
+ new Date(now - 3_600_000).toISOString(),
178
+ );
179
+
180
+ markProcessed(db as any, "new-id", "sender", 7);
181
+
182
+ const oldRow = db.prepare("SELECT 1 as found FROM whatsapp_chat_dedupe WHERE messageId = ?").get("old-id") as { found?: number } | undefined;
183
+ const recentRow = db.prepare("SELECT 1 as found FROM whatsapp_chat_dedupe WHERE messageId = ?").get("recent-id") as { found?: number } | undefined;
184
+ expect(Boolean(oldRow?.found)).toBe(false);
185
+ expect(Boolean(recentRow?.found)).toBe(true);
186
+ expect(wasProcessed(db as any, "new-id")).toBe(true);
187
+ });
188
+
189
+ it("keeps entries inside retention window", () => {
190
+ const db = createInMemoryDb();
191
+ ensureSchema(db as any);
192
+
193
+ db.prepare("INSERT INTO whatsapp_chat_dedupe(messageId, sender, receivedAt) VALUES(?, ?, ?)").run(
194
+ "one-day-old-id",
195
+ "sender",
196
+ new Date(Date.now() - 86_400_000).toISOString(),
197
+ );
198
+
199
+ markProcessed(db as any, "new-id", "sender", 7);
200
+
201
+ const oneDayOld = db.prepare("SELECT 1 as found FROM whatsapp_chat_dedupe WHERE messageId = ?").get("one-day-old-id") as { found?: number } | undefined;
202
+ expect(Boolean(oneDayOld?.found)).toBe(true);
203
+ });
204
+
205
+ it("parses dedupeRetentionDays safely", () => {
206
+ expect(getDedupeRetentionDays({})).toBe(7);
207
+ expect(getDedupeRetentionDays({ dedupeRetentionDays: undefined })).toBe(7);
208
+ expect(getDedupeRetentionDays({ dedupeRetentionDays: null })).toBe(7);
209
+ expect(getDedupeRetentionDays({ dedupeRetentionDays: 0 })).toBe(7);
210
+ expect(getDedupeRetentionDays({ dedupeRetentionDays: -3 })).toBe(7);
211
+ expect(getDedupeRetentionDays({ dedupeRetentionDays: "foo" })).toBe(7);
212
+ expect(getDedupeRetentionDays({ dedupeRetentionDays: Number.POSITIVE_INFINITY })).toBe(7);
213
+ expect(getDedupeRetentionDays({ dedupeRetentionDays: 14 })).toBe(14);
214
+ expect(getDedupeRetentionDays({ dedupeRetentionDays: 3.7 })).toBe(3);
215
+ });
216
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { generateReply } from "../reply.js";
3
+
4
+ function makeCtx(overrides: Record<string, unknown> = {}) {
5
+ const prompt = vi.fn();
6
+ const createAiSession = vi.fn().mockResolvedValue({
7
+ session: {
8
+ prompt,
9
+ state: { messages: [{ role: "assistant", content: "hello back" }] },
10
+ },
11
+ });
12
+
13
+ return {
14
+ settings: {},
15
+ taskStore: { getRootDir: () => "/repo" },
16
+ createAiSession,
17
+ ...overrides,
18
+ } as any;
19
+ }
20
+
21
+ describe("generateReply", () => {
22
+ it("creates ai session with readonly tools and cwd root", async () => {
23
+ const ctx = makeCtx();
24
+ await expect(generateReply(ctx, "1555", "hi", [])).resolves.toBe("hello back");
25
+ expect(ctx.createAiSession).toHaveBeenCalledWith(expect.objectContaining({ cwd: "/repo", tools: "readonly" }));
26
+ });
27
+
28
+ it("uses system prompt override and transcript continuity", async () => {
29
+ const prompt = vi.fn();
30
+ const ctx = makeCtx({
31
+ settings: { agentSystemPrompt: "custom prompt" },
32
+ createAiSession: vi.fn().mockResolvedValue({ session: { prompt, state: { messages: [{ role: "assistant", content: "ok" }] } } }),
33
+ });
34
+
35
+ await generateReply(ctx, "1555", "new", [{ role: "user", text: "old", createdAt: "t" }]);
36
+ expect(ctx.createAiSession).toHaveBeenCalledWith(expect.objectContaining({ systemPrompt: "custom prompt" }));
37
+ expect(prompt.mock.calls[0][0]).toContain("User: old");
38
+ expect(prompt.mock.calls[0][0]).toContain("User: new");
39
+ });
40
+
41
+ it("throws on empty assistant content", async () => {
42
+ const ctx = makeCtx({
43
+ createAiSession: vi.fn().mockResolvedValue({ session: { prompt: vi.fn(), state: { messages: [{ role: "assistant", content: " " }] } } }),
44
+ });
45
+ await expect(generateReply(ctx, "1555", "hi", [])).rejects.toThrow("no assistant text");
46
+ });
47
+
48
+ it("throws when createAiSession missing", async () => {
49
+ const ctx = makeCtx({ createAiSession: undefined });
50
+ await expect(generateReply(ctx, "1555", "hi", [])).rejects.toThrow("AI session factory unavailable");
51
+ });
52
+ });
@@ -0,0 +1,89 @@
1
+ import { BufferJSON, initAuthCreds, type AuthenticationState, type AuthenticationCreds, type SignalDataSet, type SignalDataTypeMap } from "@whiskeysockets/baileys";
2
+ import type { PluginDb } from "./index.js";
3
+
4
+ type AuthStateResult = {
5
+ state: AuthenticationState;
6
+ saveCreds: () => Promise<void>;
7
+ };
8
+
9
+ type AuthRow = { value: string };
10
+
11
+ function parseStoredValue<T>(value: string): T | null {
12
+ try {
13
+ return JSON.parse(value, BufferJSON.reviver) as T;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function serialize(value: unknown): string {
20
+ return JSON.stringify(value, BufferJSON.replacer);
21
+ }
22
+
23
+ function loadCreds(db: PluginDb): AuthenticationCreds {
24
+ const row = db.prepare("SELECT value FROM whatsapp_auth_creds WHERE id = 'creds'").get() as AuthRow | undefined;
25
+ if (!row) return initAuthCreds();
26
+ return parseStoredValue<AuthenticationCreds>(row.value) ?? initAuthCreds();
27
+ }
28
+
29
+ export function clearAuthState(db: PluginDb): void {
30
+ db.prepare("DELETE FROM whatsapp_auth_creds").run();
31
+ db.prepare("DELETE FROM whatsapp_auth_keys").run();
32
+ }
33
+
34
+ export function createPluginDbAuthState(db: PluginDb): AuthStateResult {
35
+ const state: AuthenticationState = {
36
+ creds: loadCreds(db),
37
+ keys: {
38
+ get: async <T extends keyof SignalDataTypeMap>(type: T, ids: string[]) => {
39
+ const result: Record<string, SignalDataTypeMap[T]> = {};
40
+ const select = db.prepare("SELECT value FROM whatsapp_auth_keys WHERE category = ? AND keyId = ?");
41
+ for (const id of ids) {
42
+ const row = select.get(type, id) as AuthRow | undefined;
43
+ if (!row) continue;
44
+ const parsed = parseStoredValue<SignalDataTypeMap[T]>(row.value);
45
+ if (parsed != null) {
46
+ result[id] = parsed;
47
+ }
48
+ }
49
+ return result;
50
+ },
51
+ set: async (data: SignalDataSet) => {
52
+ const upsert = db.prepare(`
53
+ INSERT INTO whatsapp_auth_keys(category, keyId, value, updatedAt)
54
+ VALUES(?, ?, ?, ?)
55
+ ON CONFLICT(category, keyId)
56
+ DO UPDATE SET value = excluded.value, updatedAt = excluded.updatedAt
57
+ `);
58
+ const remove = db.prepare("DELETE FROM whatsapp_auth_keys WHERE category = ? AND keyId = ?");
59
+ const now = new Date().toISOString();
60
+
61
+ for (const category of Object.keys(data) as Array<keyof SignalDataSet>) {
62
+ const categoryEntries = data[category];
63
+ if (!categoryEntries) continue;
64
+ for (const id of Object.keys(categoryEntries)) {
65
+ const value = categoryEntries[id];
66
+ if (value == null) {
67
+ remove.run(category, id);
68
+ continue;
69
+ }
70
+ upsert.run(category, id, serialize(value), now);
71
+ }
72
+ }
73
+ },
74
+ },
75
+ };
76
+
77
+ return {
78
+ state,
79
+ saveCreds: async () => {
80
+ const now = new Date().toISOString();
81
+ db.prepare(`
82
+ INSERT INTO whatsapp_auth_creds(id, value, updatedAt)
83
+ VALUES('creds', ?, ?)
84
+ ON CONFLICT(id)
85
+ DO UPDATE SET value = excluded.value, updatedAt = excluded.updatedAt
86
+ `).run(serialize(state.creds), now);
87
+ },
88
+ };
89
+ }