@openclaw/whatsapp 2026.5.1-beta.1 → 2026.5.2-beta.2

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.
@@ -1,203 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { resolveWhatsAppHeartbeatRecipients } from "./heartbeat-recipients.js";
3
- import type { OpenClawConfig } from "./runtime-api.js";
4
-
5
- const loadSessionStoreMock = vi.hoisted(() => vi.fn());
6
- const readChannelAllowFromStoreSyncMock = vi.hoisted(() => vi.fn<() => string[]>(() => []));
7
-
8
- vi.mock("./heartbeat-recipients.runtime.js", () => ({
9
- DEFAULT_ACCOUNT_ID: "default",
10
- loadSessionStore: loadSessionStoreMock,
11
- readChannelAllowFromStoreSync: readChannelAllowFromStoreSyncMock,
12
- resolveStorePath: vi.fn(() => "/tmp/test-sessions.json"),
13
- normalizeChannelId: (value?: string | null) => {
14
- const trimmed = value?.trim().toLowerCase();
15
- return trimmed ? (trimmed as "whatsapp") : null;
16
- },
17
- normalizeE164: (value?: string | null) => {
18
- const digits = (value ?? "").replace(/[^\d+]/g, "");
19
- if (!digits) {
20
- return "";
21
- }
22
- return digits.startsWith("+") ? digits : `+${digits}`;
23
- },
24
- }));
25
-
26
- function makeCfg(overrides?: Partial<OpenClawConfig>): OpenClawConfig {
27
- return {
28
- bindings: [],
29
- channels: {},
30
- ...overrides,
31
- } as OpenClawConfig;
32
- }
33
-
34
- describe("resolveWhatsAppHeartbeatRecipients", () => {
35
- function setSessionStore(store: Record<string, unknown>) {
36
- loadSessionStoreMock.mockReturnValue(store);
37
- }
38
-
39
- function setAllowFromStore(entries: string[]) {
40
- readChannelAllowFromStoreSyncMock.mockReturnValue(entries);
41
- }
42
-
43
- function resolveWith(
44
- cfgOverrides: Partial<OpenClawConfig> = {},
45
- opts?: Parameters<typeof resolveWhatsAppHeartbeatRecipients>[1],
46
- ) {
47
- return resolveWhatsAppHeartbeatRecipients(makeCfg(cfgOverrides), opts);
48
- }
49
-
50
- function setSingleUnauthorizedSessionWithAllowFrom() {
51
- setSessionStore({
52
- a: { lastChannel: "whatsapp", lastTo: "+15550000099", updatedAt: 2, sessionId: "a" },
53
- });
54
- setAllowFromStore(["+15550000001"]);
55
- }
56
-
57
- beforeEach(() => {
58
- loadSessionStoreMock.mockReset();
59
- readChannelAllowFromStoreSyncMock.mockReset();
60
- loadSessionStoreMock.mockReturnValue({});
61
- setAllowFromStore([]);
62
- });
63
-
64
- it("uses allowFrom store recipients when session recipients are ambiguous", () => {
65
- setSessionStore({
66
- a: { lastChannel: "whatsapp", lastTo: "+15550000001", updatedAt: 2, sessionId: "a" },
67
- b: { lastChannel: "whatsapp", lastTo: "+15550000002", updatedAt: 1, sessionId: "b" },
68
- });
69
- setAllowFromStore(["+15550000001"]);
70
-
71
- const result = resolveWith();
72
-
73
- expect(result).toEqual({ recipients: ["+15550000001"], source: "session-single" });
74
- });
75
-
76
- it("falls back to allowFrom when no session recipient is authorized", () => {
77
- setSingleUnauthorizedSessionWithAllowFrom();
78
-
79
- const result = resolveWith();
80
-
81
- expect(result).toEqual({ recipients: ["+15550000001"], source: "allowFrom" });
82
- });
83
-
84
- it("includes both session and allowFrom recipients when --all is set", () => {
85
- setSingleUnauthorizedSessionWithAllowFrom();
86
-
87
- const result = resolveWith({}, { all: true });
88
-
89
- expect(result).toEqual({
90
- recipients: ["+15550000099", "+15550000001"],
91
- source: "all",
92
- });
93
- });
94
-
95
- it("returns explicit --to recipient and source flag", () => {
96
- setSessionStore({
97
- a: { lastChannel: "whatsapp", lastTo: "+15550000099", updatedAt: 2, sessionId: "a" },
98
- });
99
- const result = resolveWith({}, { to: " +1 555 000 7777 " });
100
- expect(result).toEqual({ recipients: ["+15550007777"], source: "flag" });
101
- });
102
-
103
- it("returns ambiguous session recipients when no allowFrom list exists", () => {
104
- setSessionStore({
105
- a: { lastChannel: "whatsapp", lastTo: "+15550000001", updatedAt: 2, sessionId: "a" },
106
- b: { lastChannel: "whatsapp", lastTo: "+15550000002", updatedAt: 1, sessionId: "b" },
107
- });
108
- const result = resolveWith();
109
- expect(result).toEqual({
110
- recipients: ["+15550000001", "+15550000002"],
111
- source: "session-ambiguous",
112
- });
113
- });
114
-
115
- it("returns single session recipient when allowFrom is empty", () => {
116
- setSessionStore({
117
- a: { lastChannel: "whatsapp", lastTo: "+15550000001", updatedAt: 2, sessionId: "a" },
118
- });
119
- const result = resolveWith();
120
- expect(result).toEqual({ recipients: ["+15550000001"], source: "session-single" });
121
- });
122
-
123
- it("returns all authorized session recipients when allowFrom matches multiple", () => {
124
- setSessionStore({
125
- a: { lastChannel: "whatsapp", lastTo: "+15550000001", updatedAt: 2, sessionId: "a" },
126
- b: { lastChannel: "whatsapp", lastTo: "+15550000002", updatedAt: 1, sessionId: "b" },
127
- c: { lastChannel: "whatsapp", lastTo: "+15550000003", updatedAt: 0, sessionId: "c" },
128
- });
129
- setAllowFromStore(["+15550000001", "+15550000002"]);
130
- const result = resolveWith();
131
- expect(result).toEqual({
132
- recipients: ["+15550000001", "+15550000002"],
133
- source: "session-ambiguous",
134
- });
135
- });
136
-
137
- it("ignores session store when session scope is global", () => {
138
- setSessionStore({
139
- a: { lastChannel: "whatsapp", lastTo: "+15550000001", updatedAt: 2, sessionId: "a" },
140
- });
141
- const result = resolveWith({
142
- session: { scope: "global" } as OpenClawConfig["session"],
143
- channels: { whatsapp: { allowFrom: ["*", "+15550000009"] } as never },
144
- });
145
- expect(result).toEqual({ recipients: ["+15550000009"], source: "allowFrom" });
146
- });
147
-
148
- it("uses the requested account allowFrom config and pairing store", () => {
149
- setSessionStore({
150
- a: { lastChannel: "whatsapp", lastTo: "+15550000077", updatedAt: 2, sessionId: "a" },
151
- });
152
- setAllowFromStore(["+15550000002"]);
153
-
154
- const result = resolveWith(
155
- {
156
- channels: {
157
- whatsapp: {
158
- allowFrom: ["+15550000001"],
159
- accounts: {
160
- work: {
161
- allowFrom: ["+15550000003"],
162
- },
163
- },
164
- } as never,
165
- },
166
- },
167
- { accountId: "work" },
168
- );
169
-
170
- expect(readChannelAllowFromStoreSyncMock).toHaveBeenCalledWith("whatsapp", process.env, "work");
171
- expect(result).toEqual({
172
- recipients: ["+15550000003", "+15550000002"],
173
- source: "allowFrom",
174
- });
175
- });
176
-
177
- it("uses configured defaultAccount allowFrom config and pairing store when accountId is omitted", () => {
178
- setSessionStore({
179
- a: { lastChannel: "whatsapp", lastTo: "+15550000077", updatedAt: 2, sessionId: "a" },
180
- });
181
- setAllowFromStore(["+15550000002"]);
182
-
183
- const result = resolveWith({
184
- channels: {
185
- whatsapp: {
186
- defaultAccount: "work",
187
- allowFrom: ["+15550000001"],
188
- accounts: {
189
- work: {
190
- allowFrom: ["+15550000003"],
191
- },
192
- },
193
- } as never,
194
- },
195
- });
196
-
197
- expect(readChannelAllowFromStoreSyncMock).toHaveBeenCalledWith("whatsapp", process.env, "work");
198
- expect(result).toEqual({
199
- recipients: ["+15550000003", "+15550000002"],
200
- source: "allowFrom",
201
- });
202
- });
203
- });
@@ -1,104 +0,0 @@
1
- import { resolveDefaultWhatsAppAccountId, resolveWhatsAppAccount } from "./accounts.js";
2
- import {
3
- DEFAULT_ACCOUNT_ID,
4
- loadSessionStore,
5
- normalizeChannelId,
6
- normalizeE164,
7
- readChannelAllowFromStoreSync,
8
- resolveStorePath,
9
- type OpenClawConfig,
10
- } from "./heartbeat-recipients.runtime.js";
11
-
12
- type HeartbeatRecipientsResult = { recipients: string[]; source: string };
13
- type HeartbeatRecipientsOpts = { to?: string; all?: boolean; accountId?: string };
14
-
15
- function getSessionRecipients(cfg: OpenClawConfig) {
16
- const sessionCfg = cfg.session;
17
- const scope = sessionCfg?.scope ?? "per-sender";
18
- if (scope === "global") {
19
- return [];
20
- }
21
- const storePath = resolveStorePath(cfg.session?.store);
22
- const store = loadSessionStore(storePath);
23
- const isGroupKey = (key: string) =>
24
- key.includes(":group:") || key.includes(":channel:") || key.includes("@g.us");
25
- const isCronKey = (key: string) => key.startsWith("cron:");
26
-
27
- const recipients = Object.entries(store)
28
- .filter(([key]) => key !== "global" && key !== "unknown")
29
- .filter(([key]) => !isGroupKey(key) && !isCronKey(key))
30
- .map(([_, entry]) => ({
31
- to:
32
- normalizeChannelId(entry?.lastChannel) === "whatsapp" && entry?.lastTo
33
- ? normalizeE164(entry.lastTo)
34
- : "",
35
- updatedAt: entry?.updatedAt ?? 0,
36
- }))
37
- .filter(({ to }) => to.length > 1)
38
- .toSorted((a, b) => b.updatedAt - a.updatedAt);
39
-
40
- const seen = new Set<string>();
41
- return recipients.filter((recipient) => {
42
- if (seen.has(recipient.to)) {
43
- return false;
44
- }
45
- seen.add(recipient.to);
46
- return true;
47
- });
48
- }
49
-
50
- export function resolveWhatsAppHeartbeatRecipients(
51
- cfg: OpenClawConfig,
52
- opts: HeartbeatRecipientsOpts = {},
53
- ): HeartbeatRecipientsResult {
54
- if (opts.to) {
55
- return { recipients: [normalizeE164(opts.to)], source: "flag" };
56
- }
57
-
58
- const sessionRecipients = getSessionRecipients(cfg);
59
- const resolvedAccountId =
60
- opts.accountId?.trim() || resolveDefaultWhatsAppAccountId(cfg) || DEFAULT_ACCOUNT_ID;
61
- const configuredAllowFrom = (
62
- resolveWhatsAppAccount({ cfg, accountId: resolvedAccountId }).allowFrom ?? []
63
- )
64
- .filter((value) => value !== "*")
65
- .map(normalizeE164);
66
- const storeAllowFrom = readChannelAllowFromStoreSync(
67
- "whatsapp",
68
- process.env,
69
- resolvedAccountId,
70
- ).map(normalizeE164);
71
-
72
- const unique = (list: string[]) => [...new Set(list.filter(Boolean))];
73
- const allowFrom = unique([...configuredAllowFrom, ...storeAllowFrom]);
74
-
75
- if (opts.all) {
76
- return {
77
- recipients: unique([...sessionRecipients.map((entry) => entry.to), ...allowFrom]),
78
- source: "all",
79
- };
80
- }
81
-
82
- if (allowFrom.length > 0) {
83
- const allowSet = new Set(allowFrom);
84
- const authorizedSessionRecipients = sessionRecipients
85
- .map((entry) => entry.to)
86
- .filter((recipient) => allowSet.has(recipient));
87
- if (authorizedSessionRecipients.length === 1) {
88
- return { recipients: [authorizedSessionRecipients[0]], source: "session-single" };
89
- }
90
- if (authorizedSessionRecipients.length > 1) {
91
- return { recipients: authorizedSessionRecipients, source: "session-ambiguous" };
92
- }
93
- return { recipients: allowFrom, source: "allowFrom" };
94
- }
95
-
96
- if (sessionRecipients.length === 1) {
97
- return { recipients: [sessionRecipients[0].to], source: "session-single" };
98
- }
99
- if (sessionRecipients.length > 1) {
100
- return { recipients: sessionRecipients.map((entry) => entry.to), source: "session-ambiguous" };
101
- }
102
-
103
- return { recipients: allowFrom, source: "allowFrom" };
104
- }