@gakr-gakr/whatsapp 0.1.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 (159) hide show
  1. package/action-runtime-api.ts +1 -0
  2. package/action-runtime.runtime.ts +1 -0
  3. package/api.ts +67 -0
  4. package/auth-presence.ts +80 -0
  5. package/autobot.plugin.json +23 -0
  6. package/channel-config-api.ts +1 -0
  7. package/channel-plugin-api.ts +3 -0
  8. package/config-api.ts +4 -0
  9. package/constants.ts +1 -0
  10. package/contract-api.ts +29 -0
  11. package/directory-contract-api.ts +4 -0
  12. package/doctor-contract-api.ts +8 -0
  13. package/index.ts +16 -0
  14. package/legacy-session-surface-api.ts +6 -0
  15. package/legacy-state-migrations-api.ts +1 -0
  16. package/light-runtime-api.ts +12 -0
  17. package/login-qr-api.ts +1 -0
  18. package/login-qr-runtime.ts +23 -0
  19. package/outbound-payload-test-api.ts +1 -0
  20. package/package.json +76 -0
  21. package/runtime-api.ts +84 -0
  22. package/secret-contract-api.ts +4 -0
  23. package/security-contract-api.ts +4 -0
  24. package/setup-entry.ts +21 -0
  25. package/setup-plugin-api.ts +3 -0
  26. package/src/account-config.ts +77 -0
  27. package/src/account-ids.ts +17 -0
  28. package/src/account-types.ts +5 -0
  29. package/src/accounts.ts +176 -0
  30. package/src/action-runtime-target-auth.ts +27 -0
  31. package/src/action-runtime.ts +76 -0
  32. package/src/active-listener.ts +17 -0
  33. package/src/agent-tools-login.ts +113 -0
  34. package/src/approval-auth.ts +27 -0
  35. package/src/auth-store.runtime.ts +1 -0
  36. package/src/auth-store.ts +494 -0
  37. package/src/auto-reply/config.runtime.ts +16 -0
  38. package/src/auto-reply/constants.ts +1 -0
  39. package/src/auto-reply/deliver-reply.ts +332 -0
  40. package/src/auto-reply/loggers.ts +6 -0
  41. package/src/auto-reply/mentions.ts +131 -0
  42. package/src/auto-reply/monitor/ack-reaction.ts +99 -0
  43. package/src/auto-reply/monitor/audio-preflight.runtime.ts +9 -0
  44. package/src/auto-reply/monitor/broadcast.ts +153 -0
  45. package/src/auto-reply/monitor/commands.ts +19 -0
  46. package/src/auto-reply/monitor/echo.ts +64 -0
  47. package/src/auto-reply/monitor/group-activation.runtime.ts +1 -0
  48. package/src/auto-reply/monitor/group-activation.ts +73 -0
  49. package/src/auto-reply/monitor/group-gating.runtime.ts +8 -0
  50. package/src/auto-reply/monitor/group-gating.ts +218 -0
  51. package/src/auto-reply/monitor/group-members.ts +65 -0
  52. package/src/auto-reply/monitor/inbound-context.ts +92 -0
  53. package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +22 -0
  54. package/src/auto-reply/monitor/inbound-dispatch.ts +749 -0
  55. package/src/auto-reply/monitor/last-route.ts +61 -0
  56. package/src/auto-reply/monitor/listener-log.ts +28 -0
  57. package/src/auto-reply/monitor/message-line.runtime.ts +38 -0
  58. package/src/auto-reply/monitor/message-line.ts +54 -0
  59. package/src/auto-reply/monitor/on-message.ts +333 -0
  60. package/src/auto-reply/monitor/peer.ts +17 -0
  61. package/src/auto-reply/monitor/process-message.ts +584 -0
  62. package/src/auto-reply/monitor/runtime-api.ts +36 -0
  63. package/src/auto-reply/monitor/status-reaction.ts +108 -0
  64. package/src/auto-reply/monitor-state.ts +114 -0
  65. package/src/auto-reply/monitor.ts +720 -0
  66. package/src/auto-reply/reply-resolver.runtime.ts +1 -0
  67. package/src/auto-reply/types.ts +48 -0
  68. package/src/auto-reply/util.ts +62 -0
  69. package/src/auto-reply.impl.ts +6 -0
  70. package/src/auto-reply.ts +1 -0
  71. package/src/channel-actions.runtime.ts +7 -0
  72. package/src/channel-actions.ts +85 -0
  73. package/src/channel-outbound.ts +87 -0
  74. package/src/channel-react-action.runtime.ts +10 -0
  75. package/src/channel-react-action.ts +247 -0
  76. package/src/channel.runtime.ts +117 -0
  77. package/src/channel.setup.ts +32 -0
  78. package/src/channel.ts +356 -0
  79. package/src/command-policy.ts +7 -0
  80. package/src/config-accessors.ts +22 -0
  81. package/src/config-schema.ts +6 -0
  82. package/src/config-ui-hints.ts +24 -0
  83. package/src/connection-controller-registry.ts +49 -0
  84. package/src/connection-controller.ts +680 -0
  85. package/src/creds-files.ts +19 -0
  86. package/src/creds-persistence.ts +71 -0
  87. package/src/directory-config.ts +40 -0
  88. package/src/doctor-contract.ts +11 -0
  89. package/src/doctor.ts +56 -0
  90. package/src/document-filename.ts +17 -0
  91. package/src/group-intro.ts +15 -0
  92. package/src/group-policy.ts +40 -0
  93. package/src/group-session-contract.ts +20 -0
  94. package/src/group-session-key.ts +42 -0
  95. package/src/heartbeat.ts +34 -0
  96. package/src/identity.ts +164 -0
  97. package/src/inbound/access-control.ts +187 -0
  98. package/src/inbound/dedupe.ts +132 -0
  99. package/src/inbound/extract.ts +484 -0
  100. package/src/inbound/lifecycle.ts +39 -0
  101. package/src/inbound/media.ts +128 -0
  102. package/src/inbound/monitor.ts +1042 -0
  103. package/src/inbound/outbound-mentions.ts +260 -0
  104. package/src/inbound/runtime-api.ts +7 -0
  105. package/src/inbound/save-media.runtime.ts +1 -0
  106. package/src/inbound/send-api.ts +203 -0
  107. package/src/inbound/send-result.ts +109 -0
  108. package/src/inbound/types.ts +107 -0
  109. package/src/inbound-policy.ts +215 -0
  110. package/src/inbound.ts +9 -0
  111. package/src/login-qr.ts +542 -0
  112. package/src/login.ts +83 -0
  113. package/src/media.ts +10 -0
  114. package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +417 -0
  115. package/src/monitor-inbox.append-upsert.test-support.ts +133 -0
  116. package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +418 -0
  117. package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +308 -0
  118. package/src/monitor-inbox.streams-inbound-messages.test-support.ts +824 -0
  119. package/src/normalize-target.ts +148 -0
  120. package/src/normalize.ts +8 -0
  121. package/src/outbound-adapter.ts +36 -0
  122. package/src/outbound-base.ts +256 -0
  123. package/src/outbound-media-contract.ts +307 -0
  124. package/src/outbound-media.runtime.ts +41 -0
  125. package/src/outbound-send-deps.ts +1 -0
  126. package/src/outbound-test-support.ts +16 -0
  127. package/src/qa-driver.runtime.ts +189 -0
  128. package/src/qr-image.ts +1 -0
  129. package/src/qr-terminal.ts +1 -0
  130. package/src/quoted-message.ts +184 -0
  131. package/src/reaction-level.ts +24 -0
  132. package/src/reconnect.ts +55 -0
  133. package/src/resolve-outbound-target.ts +58 -0
  134. package/src/runtime-api.ts +59 -0
  135. package/src/runtime-group-policy.ts +16 -0
  136. package/src/runtime.ts +9 -0
  137. package/src/security-contract.ts +47 -0
  138. package/src/security-fix.ts +71 -0
  139. package/src/send.ts +342 -0
  140. package/src/session-contract.ts +43 -0
  141. package/src/session-errors.ts +125 -0
  142. package/src/session-route.ts +32 -0
  143. package/src/session.runtime.ts +8 -0
  144. package/src/session.ts +327 -0
  145. package/src/setup-core.ts +52 -0
  146. package/src/setup-finalize.ts +450 -0
  147. package/src/setup-surface.ts +71 -0
  148. package/src/setup-test-helpers.ts +217 -0
  149. package/src/shared.ts +291 -0
  150. package/src/socket-timing.ts +38 -0
  151. package/src/state-migrations.ts +55 -0
  152. package/src/status-issues.ts +185 -0
  153. package/src/system-prompt.ts +31 -0
  154. package/src/targets-runtime.ts +221 -0
  155. package/src/text-runtime.ts +18 -0
  156. package/src/vcard.ts +84 -0
  157. package/targets.ts +5 -0
  158. package/test-api.ts +2 -0
  159. package/tsconfig.json +16 -0
@@ -0,0 +1,418 @@
1
+ import "./monitor-inbox.test-harness.js";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import {
4
+ DEFAULT_ACCOUNT_ID,
5
+ expectPairingPromptSent,
6
+ getAuthDir,
7
+ getMonitorWebInbox,
8
+ getSock,
9
+ installWebMonitorInboxUnitTestHooks,
10
+ mockLoadConfig,
11
+ settleInboundWork,
12
+ waitForMessageCalls,
13
+ } from "./monitor-inbox.test-harness.js";
14
+
15
+ const nowSeconds = (offsetMs = 0) => Math.floor((Date.now() + offsetMs) / 1000);
16
+ const DEFAULT_MESSAGES_CFG = {
17
+ messagePrefix: undefined,
18
+ responsePrefix: undefined,
19
+ } as const;
20
+ const TIMESTAMP_OFF_MESSAGES_CFG = {
21
+ ...DEFAULT_MESSAGES_CFG,
22
+ timestampPrefix: false,
23
+ } as const;
24
+
25
+ const createNotifyUpsert = (message: Record<string, unknown>) => ({
26
+ type: "notify",
27
+ messages: [message],
28
+ });
29
+
30
+ const createDmMessage = (params: { id: string; remoteJid: string; conversation: string }) => ({
31
+ key: {
32
+ id: params.id,
33
+ fromMe: false,
34
+ remoteJid: params.remoteJid,
35
+ },
36
+ message: { conversation: params.conversation },
37
+ messageTimestamp: nowSeconds(),
38
+ });
39
+
40
+ const createGroupMessage = (params: {
41
+ id: string;
42
+ remoteJid?: string;
43
+ participant: string;
44
+ conversation: string;
45
+ }) => ({
46
+ key: {
47
+ id: params.id,
48
+ fromMe: false,
49
+ remoteJid: params.remoteJid ?? "11111@g.us",
50
+ participant: params.participant,
51
+ },
52
+ message: { conversation: params.conversation },
53
+ messageTimestamp: nowSeconds(),
54
+ });
55
+
56
+ async function startWebInboxMonitor(params: {
57
+ config?: Record<string, unknown>;
58
+ loadConfig?: () => Record<string, unknown>;
59
+ sendReadReceipts?: boolean;
60
+ }) {
61
+ const monitorWebInbox = getMonitorWebInbox();
62
+ if (params.config) {
63
+ mockLoadConfig.mockReturnValue(params.config);
64
+ }
65
+ const onMessage = vi.fn();
66
+ const base = {
67
+ cfg: (params.config ?? mockLoadConfig()) as never,
68
+ ...(params.loadConfig ? { loadConfig: params.loadConfig as never } : {}),
69
+ verbose: false,
70
+ accountId: DEFAULT_ACCOUNT_ID,
71
+ authDir: getAuthDir(),
72
+ onMessage,
73
+ };
74
+ const listener = await monitorWebInbox(
75
+ params.sendReadReceipts === undefined
76
+ ? base
77
+ : {
78
+ ...base,
79
+ sendReadReceipts: params.sendReadReceipts,
80
+ },
81
+ );
82
+ return { onMessage, listener, sock: getSock() };
83
+ }
84
+
85
+ function firstInboundPayload(onMessage: ReturnType<typeof vi.fn>) {
86
+ const payload = onMessage.mock.calls[0]?.[0];
87
+ if (!payload || typeof payload !== "object") {
88
+ throw new Error("expected first inbound payload");
89
+ }
90
+ return payload as Record<string, unknown>;
91
+ }
92
+
93
+ describe("web monitor inbox", () => {
94
+ installWebMonitorInboxUnitTestHooks();
95
+
96
+ it("blocks messages from unauthorized senders not in allowFrom", async () => {
97
+ // Test for auto-recovery fix: early allowFrom filtering prevents Bad MAC errors
98
+ // from unauthorized senders corrupting sessions
99
+ const config = {
100
+ channels: {
101
+ whatsapp: {
102
+ // Only allow +111
103
+ allowFrom: ["+111"],
104
+ },
105
+ },
106
+ messages: DEFAULT_MESSAGES_CFG,
107
+ };
108
+
109
+ const { onMessage, listener, sock } = await startWebInboxMonitor({
110
+ config,
111
+ });
112
+
113
+ // Message from unauthorized sender +999 (not in allowFrom)
114
+ sock.ev.emit(
115
+ "messages.upsert",
116
+ createNotifyUpsert(
117
+ createDmMessage({
118
+ id: "unauth1",
119
+ remoteJid: "999@s.whatsapp.net",
120
+ conversation: "unauthorized message",
121
+ }),
122
+ ),
123
+ );
124
+ await vi.waitFor(() => expectPairingPromptSent(sock, "999@s.whatsapp.net", "+999"));
125
+
126
+ // Should NOT call onMessage for unauthorized senders
127
+ expect(onMessage).not.toHaveBeenCalled();
128
+ // Should NOT send read receipts for blocked senders (privacy + avoids Baileys Bad MAC churn).
129
+ expect(sock.readMessages).not.toHaveBeenCalled();
130
+
131
+ await listener.close();
132
+ });
133
+
134
+ it("applies hot-reloaded dmPolicy allowlist to the active listener", async () => {
135
+ const startupConfig = {
136
+ channels: {
137
+ whatsapp: {
138
+ dmPolicy: "open",
139
+ allowFrom: ["*"],
140
+ },
141
+ },
142
+ messages: DEFAULT_MESSAGES_CFG,
143
+ };
144
+ const reloadedConfig = {
145
+ channels: {
146
+ whatsapp: {
147
+ dmPolicy: "allowlist",
148
+ allowFrom: ["+111"],
149
+ },
150
+ },
151
+ messages: DEFAULT_MESSAGES_CFG,
152
+ };
153
+ mockLoadConfig.mockReturnValue(startupConfig);
154
+
155
+ const { onMessage, listener, sock } = await startWebInboxMonitor({
156
+ config: startupConfig,
157
+ loadConfig: () => mockLoadConfig() as Record<string, unknown>,
158
+ });
159
+ mockLoadConfig.mockReturnValue(reloadedConfig);
160
+
161
+ sock.ev.emit(
162
+ "messages.upsert",
163
+ createNotifyUpsert(
164
+ createDmMessage({
165
+ id: "hot-reload-block",
166
+ remoteJid: "999@s.whatsapp.net",
167
+ conversation: "should be blocked after reload",
168
+ }),
169
+ ),
170
+ );
171
+ await settleInboundWork();
172
+
173
+ expect(onMessage).not.toHaveBeenCalled();
174
+ expect(sock.sendMessage).not.toHaveBeenCalled();
175
+ expect(sock.readMessages).not.toHaveBeenCalled();
176
+
177
+ await listener.close();
178
+ });
179
+
180
+ it("skips read receipts in self-chat mode", async () => {
181
+ const config = {
182
+ channels: {
183
+ whatsapp: {
184
+ // Self-chat heuristic: allowFrom includes selfE164 (+123).
185
+ allowFrom: ["+123"],
186
+ },
187
+ },
188
+ messages: DEFAULT_MESSAGES_CFG,
189
+ };
190
+
191
+ const { onMessage, listener, sock } = await startWebInboxMonitor({
192
+ config,
193
+ });
194
+
195
+ sock.ev.emit(
196
+ "messages.upsert",
197
+ createNotifyUpsert(
198
+ createDmMessage({
199
+ id: "self1",
200
+ remoteJid: "123@s.whatsapp.net",
201
+ conversation: "self ping",
202
+ }),
203
+ ),
204
+ );
205
+ await waitForMessageCalls(onMessage, 1);
206
+
207
+ expect(onMessage).toHaveBeenCalledTimes(1);
208
+ expect(onMessage).toHaveBeenCalledWith(
209
+ expect.objectContaining({
210
+ from: "+123",
211
+ to: "+123",
212
+ body: "self ping",
213
+ accessControlPassed: true,
214
+ }),
215
+ );
216
+ expect(sock.readMessages).not.toHaveBeenCalled();
217
+
218
+ await listener.close();
219
+ });
220
+
221
+ it("skips read receipts when disabled", async () => {
222
+ const { onMessage, listener, sock } = await startWebInboxMonitor({
223
+ sendReadReceipts: false,
224
+ });
225
+ sock.ev.emit(
226
+ "messages.upsert",
227
+ createNotifyUpsert(
228
+ createDmMessage({
229
+ id: "rr-off-1",
230
+ remoteJid: "222@s.whatsapp.net",
231
+ conversation: "read receipts off",
232
+ }),
233
+ ),
234
+ );
235
+ await waitForMessageCalls(onMessage, 1);
236
+
237
+ expect(onMessage).toHaveBeenCalledTimes(1);
238
+ expect(sock.readMessages).not.toHaveBeenCalled();
239
+
240
+ await listener.close();
241
+ });
242
+
243
+ it("lets group messages through even when sender not in allowFrom", async () => {
244
+ const { onMessage, listener, sock } = await startWebInboxMonitor({
245
+ config: {
246
+ channels: { whatsapp: { allowFrom: ["+1234"], groupPolicy: "open" } },
247
+ messages: DEFAULT_MESSAGES_CFG,
248
+ },
249
+ });
250
+ sock.ev.emit(
251
+ "messages.upsert",
252
+ createNotifyUpsert(
253
+ createGroupMessage({
254
+ id: "grp3",
255
+ participant: "999@s.whatsapp.net",
256
+ conversation: "unauthorized group message",
257
+ }),
258
+ ),
259
+ );
260
+ await settleInboundWork();
261
+
262
+ expect(onMessage).toHaveBeenCalledTimes(1);
263
+ const payload = firstInboundPayload(onMessage);
264
+ expect(payload.chatType).toBe("group");
265
+ expect(payload.senderE164).toBe("+999");
266
+
267
+ await listener.close();
268
+ });
269
+
270
+ it("blocks all group messages when groupPolicy is 'disabled'", async () => {
271
+ const { onMessage, listener, sock } = await startWebInboxMonitor({
272
+ config: {
273
+ channels: { whatsapp: { allowFrom: ["+1234"], groupPolicy: "disabled" } },
274
+ messages: TIMESTAMP_OFF_MESSAGES_CFG,
275
+ },
276
+ });
277
+ sock.ev.emit(
278
+ "messages.upsert",
279
+ createNotifyUpsert(
280
+ createGroupMessage({
281
+ id: "grp-disabled",
282
+ participant: "999@s.whatsapp.net",
283
+ conversation: "group message should be blocked",
284
+ }),
285
+ ),
286
+ );
287
+ await settleInboundWork();
288
+
289
+ // Should NOT call onMessage because groupPolicy is disabled
290
+ expect(onMessage).not.toHaveBeenCalled();
291
+
292
+ await listener.close();
293
+ });
294
+
295
+ it("blocks group messages from senders not in groupAllowFrom when groupPolicy is 'allowlist'", async () => {
296
+ const { onMessage, listener, sock } = await startWebInboxMonitor({
297
+ config: {
298
+ channels: {
299
+ whatsapp: {
300
+ groupAllowFrom: ["+1234"], // Does not include +999
301
+ groupPolicy: "allowlist",
302
+ },
303
+ },
304
+ messages: TIMESTAMP_OFF_MESSAGES_CFG,
305
+ },
306
+ });
307
+ sock.ev.emit(
308
+ "messages.upsert",
309
+ createNotifyUpsert(
310
+ createGroupMessage({
311
+ id: "grp-allowlist-blocked",
312
+ participant: "999@s.whatsapp.net",
313
+ conversation: "unauthorized group sender",
314
+ }),
315
+ ),
316
+ );
317
+ await settleInboundWork();
318
+
319
+ // Should NOT call onMessage because sender +999 not in groupAllowFrom
320
+ expect(onMessage).not.toHaveBeenCalled();
321
+
322
+ await listener.close();
323
+ });
324
+
325
+ it("allows group messages from senders in groupAllowFrom when groupPolicy is 'allowlist'", async () => {
326
+ const { onMessage, listener, sock } = await startWebInboxMonitor({
327
+ config: {
328
+ channels: {
329
+ whatsapp: {
330
+ groupAllowFrom: ["+15551234567"], // Includes the sender
331
+ groupPolicy: "allowlist",
332
+ },
333
+ },
334
+ messages: TIMESTAMP_OFF_MESSAGES_CFG,
335
+ },
336
+ });
337
+ sock.ev.emit(
338
+ "messages.upsert",
339
+ createNotifyUpsert(
340
+ createGroupMessage({
341
+ id: "grp-allowlist-allowed",
342
+ participant: "15551234567@s.whatsapp.net",
343
+ conversation: "authorized group sender",
344
+ }),
345
+ ),
346
+ );
347
+ await settleInboundWork();
348
+
349
+ // Should call onMessage because sender is in groupAllowFrom
350
+ expect(onMessage).toHaveBeenCalledTimes(1);
351
+ const payload = firstInboundPayload(onMessage);
352
+ expect(payload.chatType).toBe("group");
353
+ expect(payload.senderE164).toBe("+15551234567");
354
+
355
+ await listener.close();
356
+ });
357
+
358
+ it("allows all group senders with wildcard in groupPolicy allowlist", async () => {
359
+ const { onMessage, listener, sock } = await startWebInboxMonitor({
360
+ config: {
361
+ channels: {
362
+ whatsapp: {
363
+ groupAllowFrom: ["*"], // Wildcard allows everyone
364
+ groupPolicy: "allowlist",
365
+ },
366
+ },
367
+ messages: TIMESTAMP_OFF_MESSAGES_CFG,
368
+ },
369
+ });
370
+ sock.ev.emit(
371
+ "messages.upsert",
372
+ createNotifyUpsert(
373
+ createGroupMessage({
374
+ id: "grp-wildcard-test",
375
+ remoteJid: "22222@g.us",
376
+ participant: "9999999999@s.whatsapp.net",
377
+ conversation: "wildcard group sender",
378
+ }),
379
+ ),
380
+ );
381
+ await settleInboundWork();
382
+
383
+ // Should call onMessage because wildcard allows all senders
384
+ expect(onMessage).toHaveBeenCalledTimes(1);
385
+ const payload = firstInboundPayload(onMessage);
386
+ expect(payload.chatType).toBe("group");
387
+
388
+ await listener.close();
389
+ });
390
+
391
+ it("blocks group messages when groupPolicy allowlist has no groupAllowFrom", async () => {
392
+ const { onMessage, listener, sock } = await startWebInboxMonitor({
393
+ config: {
394
+ channels: {
395
+ whatsapp: {
396
+ groupPolicy: "allowlist",
397
+ },
398
+ },
399
+ messages: TIMESTAMP_OFF_MESSAGES_CFG,
400
+ },
401
+ });
402
+ sock.ev.emit(
403
+ "messages.upsert",
404
+ createNotifyUpsert(
405
+ createGroupMessage({
406
+ id: "grp-allowlist-empty",
407
+ participant: "999@s.whatsapp.net",
408
+ conversation: "blocked by empty allowlist",
409
+ }),
410
+ ),
411
+ );
412
+ await settleInboundWork();
413
+
414
+ expect(onMessage).not.toHaveBeenCalled();
415
+
416
+ await listener.close();
417
+ });
418
+ });