@symerian/symi 3.0.20 → 3.0.21

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 (52) hide show
  1. package/dist/{audio-preflight-BaCdNfrk.js → audio-preflight-D7BVT-ls.js} +4 -4
  2. package/dist/build-info.json +3 -3
  3. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  4. package/dist/{chrome-UfmVM0xR.js → chrome-B5CO2vB5.js} +7 -7
  5. package/dist/{deliver-BqXdac6W.js → deliver-CrwjsDwv.js} +1 -1
  6. package/dist/extensionAPI.js +7 -7
  7. package/dist/{image-DIWsXYcW.js → image-Csu7WcLW.js} +1 -1
  8. package/dist/{manager-DW3SxcPr.js → manager-BkkVjTO8.js} +1 -1
  9. package/dist/{pi-embedded-BNch0U5F.js → pi-embedded-Dhp64z5l.js} +16 -16
  10. package/dist/{pi-embedded-helpers-IkHl02JF.js → pi-embedded-helpers-840E4hop.js} +4 -4
  11. package/dist/{pw-ai-nMkA-oDJ.js → pw-ai-CBgJf_RR.js} +1 -1
  12. package/dist/{runner-DNEC58JI.js → runner-BbFKo1ne.js} +1 -1
  13. package/dist/{synthesis-BWAr0sZ9.js → synthesis-DoEM0E8_.js} +7 -7
  14. package/dist/{web-7a-m_UxL.js → web-BYXJn-Ps.js} +7 -7
  15. package/package.json +1 -1
  16. package/extensions/imessage/index.ts +0 -17
  17. package/extensions/imessage/node_modules/.bin/symi +0 -21
  18. package/extensions/imessage/package.json +0 -15
  19. package/extensions/imessage/src/channel.outbound.test.ts +0 -66
  20. package/extensions/imessage/src/channel.ts +0 -298
  21. package/extensions/imessage/src/runtime.ts +0 -14
  22. package/extensions/imessage/symi.plugin.json +0 -9
  23. package/extensions/line/index.ts +0 -19
  24. package/extensions/line/node_modules/.bin/symi +0 -21
  25. package/extensions/line/package.json +0 -30
  26. package/extensions/line/src/card-command.ts +0 -344
  27. package/extensions/line/src/channel.logout.test.ts +0 -133
  28. package/extensions/line/src/channel.sendPayload.test.ts +0 -312
  29. package/extensions/line/src/channel.startup.test.ts +0 -133
  30. package/extensions/line/src/channel.ts +0 -801
  31. package/extensions/line/src/runtime.ts +0 -14
  32. package/extensions/line/symi.plugin.json +0 -9
  33. package/extensions/signal/index.ts +0 -17
  34. package/extensions/signal/node_modules/.bin/symi +0 -21
  35. package/extensions/signal/package.json +0 -15
  36. package/extensions/signal/src/channel.ts +0 -302
  37. package/extensions/signal/src/runtime.ts +0 -14
  38. package/extensions/signal/symi.plugin.json +0 -9
  39. package/extensions/telegram/index.ts +0 -17
  40. package/extensions/telegram/node_modules/.bin/symi +0 -21
  41. package/extensions/telegram/package.json +0 -15
  42. package/extensions/telegram/src/channel.test.ts +0 -125
  43. package/extensions/telegram/src/channel.ts +0 -560
  44. package/extensions/telegram/src/runtime.ts +0 -14
  45. package/extensions/telegram/symi.plugin.json +0 -9
  46. package/extensions/whatsapp/index.ts +0 -17
  47. package/extensions/whatsapp/node_modules/.bin/symi +0 -21
  48. package/extensions/whatsapp/package.json +0 -15
  49. package/extensions/whatsapp/src/channel.ts +0 -465
  50. package/extensions/whatsapp/src/resolve-target.test.ts +0 -170
  51. package/extensions/whatsapp/src/runtime.ts +0 -14
  52. package/extensions/whatsapp/symi.plugin.json +0 -9
@@ -1,465 +0,0 @@
1
- import {
2
- applyAccountNameToChannelSection,
3
- buildChannelConfigSchema,
4
- collectWhatsAppStatusIssues,
5
- createActionGate,
6
- DEFAULT_ACCOUNT_ID,
7
- escapeRegExp,
8
- formatPairingApproveHint,
9
- getChatChannelMeta,
10
- listWhatsAppAccountIds,
11
- listWhatsAppDirectoryGroupsFromConfig,
12
- listWhatsAppDirectoryPeersFromConfig,
13
- looksLikeWhatsAppTargetId,
14
- migrateBaseNameToDefaultAccount,
15
- normalizeAccountId,
16
- normalizeE164,
17
- normalizeWhatsAppMessagingTarget,
18
- normalizeWhatsAppTarget,
19
- readStringParam,
20
- resolveDefaultWhatsAppAccountId,
21
- resolveWhatsAppOutboundTarget,
22
- resolveWhatsAppAccount,
23
- resolveWhatsAppGroupRequireMention,
24
- resolveWhatsAppGroupToolPolicy,
25
- resolveWhatsAppHeartbeatRecipients,
26
- whatsappOnboardingAdapter,
27
- WhatsAppConfigSchema,
28
- type ChannelMessageActionName,
29
- type ChannelPlugin,
30
- type ResolvedWhatsAppAccount,
31
- } from "symi/plugin-sdk";
32
- import { getWhatsAppRuntime } from "./runtime.js";
33
-
34
- const meta = getChatChannelMeta("whatsapp");
35
-
36
- export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
37
- id: "whatsapp",
38
- meta: {
39
- ...meta,
40
- showConfigured: false,
41
- quickstartAllowFrom: true,
42
- forceAccountBinding: true,
43
- preferSessionLookupForAnnounceTarget: true,
44
- },
45
- onboarding: whatsappOnboardingAdapter,
46
- agentTools: () => [getWhatsAppRuntime().channel.whatsapp.createLoginTool()],
47
- pairing: {
48
- idLabel: "whatsappSenderId",
49
- },
50
- capabilities: {
51
- chatTypes: ["direct", "group"],
52
- polls: true,
53
- reactions: true,
54
- media: true,
55
- },
56
- reload: { configPrefixes: ["web"], noopPrefixes: ["channels.whatsapp"] },
57
- gatewayMethods: ["web.login.start", "web.login.wait"],
58
- configSchema: buildChannelConfigSchema(WhatsAppConfigSchema),
59
- config: {
60
- listAccountIds: (cfg) => listWhatsAppAccountIds(cfg),
61
- resolveAccount: (cfg, accountId) => resolveWhatsAppAccount({ cfg, accountId }),
62
- defaultAccountId: (cfg) => resolveDefaultWhatsAppAccountId(cfg),
63
- setAccountEnabled: ({ cfg, accountId, enabled }) => {
64
- const accountKey = accountId || DEFAULT_ACCOUNT_ID;
65
- const accounts = { ...cfg.channels?.whatsapp?.accounts };
66
- const existing = accounts[accountKey] ?? {};
67
- return {
68
- ...cfg,
69
- channels: {
70
- ...cfg.channels,
71
- whatsapp: {
72
- ...cfg.channels?.whatsapp,
73
- accounts: {
74
- ...accounts,
75
- [accountKey]: {
76
- ...existing,
77
- enabled,
78
- },
79
- },
80
- },
81
- },
82
- };
83
- },
84
- deleteAccount: ({ cfg, accountId }) => {
85
- const accountKey = accountId || DEFAULT_ACCOUNT_ID;
86
- const accounts = { ...cfg.channels?.whatsapp?.accounts };
87
- delete accounts[accountKey];
88
- return {
89
- ...cfg,
90
- channels: {
91
- ...cfg.channels,
92
- whatsapp: {
93
- ...cfg.channels?.whatsapp,
94
- accounts: Object.keys(accounts).length ? accounts : undefined,
95
- },
96
- },
97
- };
98
- },
99
- isEnabled: (account, cfg) => account.enabled && cfg.web?.enabled !== false,
100
- disabledReason: () => "disabled",
101
- isConfigured: async (account) =>
102
- await getWhatsAppRuntime().channel.whatsapp.webAuthExists(account.authDir),
103
- unconfiguredReason: () => "not linked",
104
- describeAccount: (account) => ({
105
- accountId: account.accountId,
106
- name: account.name,
107
- enabled: account.enabled,
108
- configured: Boolean(account.authDir),
109
- linked: Boolean(account.authDir),
110
- dmPolicy: account.dmPolicy,
111
- allowFrom: account.allowFrom,
112
- }),
113
- resolveAllowFrom: ({ cfg, accountId }) =>
114
- resolveWhatsAppAccount({ cfg, accountId }).allowFrom ?? [],
115
- formatAllowFrom: ({ allowFrom }) =>
116
- allowFrom
117
- .map((entry) => String(entry).trim())
118
- .filter((entry): entry is string => Boolean(entry))
119
- .map((entry) => (entry === "*" ? entry : normalizeWhatsAppTarget(entry)))
120
- .filter((entry): entry is string => Boolean(entry)),
121
- resolveDefaultTo: ({ cfg, accountId }) => {
122
- const root = cfg.channels?.whatsapp;
123
- const normalized = normalizeAccountId(accountId);
124
- const account = root?.accounts?.[normalized];
125
- return (account?.defaultTo ?? root?.defaultTo)?.trim() || undefined;
126
- },
127
- },
128
- security: {
129
- resolveDmPolicy: ({ cfg, accountId, account }) => {
130
- const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
131
- const useAccountPath = Boolean(cfg.channels?.whatsapp?.accounts?.[resolvedAccountId]);
132
- const basePath = useAccountPath
133
- ? `channels.whatsapp.accounts.${resolvedAccountId}.`
134
- : "channels.whatsapp.";
135
- return {
136
- policy: account.dmPolicy ?? "pairing",
137
- allowFrom: account.allowFrom ?? [],
138
- policyPath: `${basePath}dmPolicy`,
139
- allowFromPath: basePath,
140
- approveHint: formatPairingApproveHint("whatsapp"),
141
- normalizeEntry: (raw) => normalizeE164(raw),
142
- };
143
- },
144
- collectWarnings: ({ account, cfg }) => {
145
- const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
146
- const groupPolicy = account.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
147
- if (groupPolicy !== "open") {
148
- return [];
149
- }
150
- const groupAllowlistConfigured =
151
- Boolean(account.groups) && Object.keys(account.groups ?? {}).length > 0;
152
- if (groupAllowlistConfigured) {
153
- return [
154
- `- WhatsApp groups: groupPolicy="open" allows any member in allowed groups to trigger (mention-gated). Set channels.whatsapp.groupPolicy="allowlist" + channels.whatsapp.groupAllowFrom to restrict senders.`,
155
- ];
156
- }
157
- return [
158
- `- WhatsApp groups: groupPolicy="open" with no channels.whatsapp.groups allowlist; any group can add + ping (mention-gated). Set channels.whatsapp.groupPolicy="allowlist" + channels.whatsapp.groupAllowFrom or configure channels.whatsapp.groups.`,
159
- ];
160
- },
161
- },
162
- setup: {
163
- resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
164
- applyAccountName: ({ cfg, accountId, name }) =>
165
- applyAccountNameToChannelSection({
166
- cfg,
167
- channelKey: "whatsapp",
168
- accountId,
169
- name,
170
- alwaysUseAccounts: true,
171
- }),
172
- applyAccountConfig: ({ cfg, accountId, input }) => {
173
- const namedConfig = applyAccountNameToChannelSection({
174
- cfg,
175
- channelKey: "whatsapp",
176
- accountId,
177
- name: input.name,
178
- alwaysUseAccounts: true,
179
- });
180
- const next = migrateBaseNameToDefaultAccount({
181
- cfg: namedConfig,
182
- channelKey: "whatsapp",
183
- alwaysUseAccounts: true,
184
- });
185
- const entry = {
186
- ...next.channels?.whatsapp?.accounts?.[accountId],
187
- ...(input.authDir ? { authDir: input.authDir } : {}),
188
- enabled: true,
189
- };
190
- return {
191
- ...next,
192
- channels: {
193
- ...next.channels,
194
- whatsapp: {
195
- ...next.channels?.whatsapp,
196
- accounts: {
197
- ...next.channels?.whatsapp?.accounts,
198
- [accountId]: entry,
199
- },
200
- },
201
- },
202
- };
203
- },
204
- },
205
- groups: {
206
- resolveRequireMention: resolveWhatsAppGroupRequireMention,
207
- resolveToolPolicy: resolveWhatsAppGroupToolPolicy,
208
- resolveGroupIntroHint: () =>
209
- "WhatsApp IDs: SenderId is the participant JID (group participant id).",
210
- },
211
- mentions: {
212
- stripPatterns: ({ ctx }) => {
213
- const selfE164 = (ctx.To ?? "").replace(/^whatsapp:/, "");
214
- if (!selfE164) {
215
- return [];
216
- }
217
- const escaped = escapeRegExp(selfE164);
218
- return [escaped, `@${escaped}`];
219
- },
220
- },
221
- commands: {
222
- enforceOwnerForCommands: true,
223
- skipWhenConfigEmpty: true,
224
- },
225
- messaging: {
226
- normalizeTarget: normalizeWhatsAppMessagingTarget,
227
- targetResolver: {
228
- looksLikeId: looksLikeWhatsAppTargetId,
229
- hint: "<E.164|group JID>",
230
- },
231
- },
232
- directory: {
233
- self: async ({ cfg, accountId }) => {
234
- const account = resolveWhatsAppAccount({ cfg, accountId });
235
- const { e164, jid } = getWhatsAppRuntime().channel.whatsapp.readWebSelfId(account.authDir);
236
- const id = e164 ?? jid;
237
- if (!id) {
238
- return null;
239
- }
240
- return {
241
- kind: "user",
242
- id,
243
- name: account.name,
244
- raw: { e164, jid },
245
- };
246
- },
247
- listPeers: async (params) => listWhatsAppDirectoryPeersFromConfig(params),
248
- listGroups: async (params) => listWhatsAppDirectoryGroupsFromConfig(params),
249
- },
250
- actions: {
251
- listActions: ({ cfg }) => {
252
- if (!cfg.channels?.whatsapp) {
253
- return [];
254
- }
255
- const gate = createActionGate(cfg.channels.whatsapp.actions);
256
- const actions = new Set<ChannelMessageActionName>();
257
- if (gate("reactions")) {
258
- actions.add("react");
259
- }
260
- if (gate("polls")) {
261
- actions.add("poll");
262
- }
263
- return Array.from(actions);
264
- },
265
- supportsAction: ({ action }) => action === "react",
266
- handleAction: async ({ action, params, cfg, accountId }) => {
267
- if (action !== "react") {
268
- throw new Error(`Action ${action} is not supported for provider ${meta.id}.`);
269
- }
270
- const messageId = readStringParam(params, "messageId", {
271
- required: true,
272
- });
273
- const emoji = readStringParam(params, "emoji", { allowEmpty: true });
274
- const remove = typeof params.remove === "boolean" ? params.remove : undefined;
275
- return await getWhatsAppRuntime().channel.whatsapp.handleWhatsAppAction(
276
- {
277
- action: "react",
278
- chatJid:
279
- readStringParam(params, "chatJid") ?? readStringParam(params, "to", { required: true }),
280
- messageId,
281
- emoji,
282
- remove,
283
- participant: readStringParam(params, "participant"),
284
- accountId: accountId ?? undefined,
285
- fromMe: typeof params.fromMe === "boolean" ? params.fromMe : undefined,
286
- },
287
- cfg,
288
- );
289
- },
290
- },
291
- outbound: {
292
- deliveryMode: "gateway",
293
- chunker: (text, limit) => getWhatsAppRuntime().channel.text.chunkText(text, limit),
294
- chunkerMode: "text",
295
- textChunkLimit: 4000,
296
- pollMaxOptions: 12,
297
- resolveTarget: ({ to, allowFrom, mode }) =>
298
- resolveWhatsAppOutboundTarget({ to, allowFrom, mode }),
299
- sendText: async ({ to, text, accountId, deps, gifPlayback }) => {
300
- const send = deps?.sendWhatsApp ?? getWhatsAppRuntime().channel.whatsapp.sendMessageWhatsApp;
301
- const result = await send(to, text, {
302
- verbose: false,
303
- accountId: accountId ?? undefined,
304
- gifPlayback,
305
- });
306
- return { channel: "whatsapp", ...result };
307
- },
308
- sendMedia: async ({ to, text, mediaUrl, accountId, deps, gifPlayback }) => {
309
- const send = deps?.sendWhatsApp ?? getWhatsAppRuntime().channel.whatsapp.sendMessageWhatsApp;
310
- const result = await send(to, text, {
311
- verbose: false,
312
- mediaUrl,
313
- accountId: accountId ?? undefined,
314
- gifPlayback,
315
- });
316
- return { channel: "whatsapp", ...result };
317
- },
318
- sendPoll: async ({ to, poll, accountId }) =>
319
- await getWhatsAppRuntime().channel.whatsapp.sendPollWhatsApp(to, poll, {
320
- verbose: getWhatsAppRuntime().logging.shouldLogVerbose(),
321
- accountId: accountId ?? undefined,
322
- }),
323
- },
324
- auth: {
325
- login: async ({ cfg, accountId, runtime, verbose }) => {
326
- const resolvedAccountId = accountId?.trim() || resolveDefaultWhatsAppAccountId(cfg);
327
- await getWhatsAppRuntime().channel.whatsapp.loginWeb(
328
- Boolean(verbose),
329
- undefined,
330
- runtime,
331
- resolvedAccountId,
332
- );
333
- },
334
- },
335
- heartbeat: {
336
- checkReady: async ({ cfg, accountId, deps }) => {
337
- if (cfg.web?.enabled === false) {
338
- return { ok: false, reason: "whatsapp-disabled" };
339
- }
340
- const account = resolveWhatsAppAccount({ cfg, accountId });
341
- const authExists = await (
342
- deps?.webAuthExists ?? getWhatsAppRuntime().channel.whatsapp.webAuthExists
343
- )(account.authDir);
344
- if (!authExists) {
345
- return { ok: false, reason: "whatsapp-not-linked" };
346
- }
347
- const listenerActive = deps?.hasActiveWebListener
348
- ? deps.hasActiveWebListener()
349
- : Boolean(getWhatsAppRuntime().channel.whatsapp.getActiveWebListener());
350
- if (!listenerActive) {
351
- return { ok: false, reason: "whatsapp-not-running" };
352
- }
353
- return { ok: true, reason: "ok" };
354
- },
355
- resolveRecipients: ({ cfg, opts }) => resolveWhatsAppHeartbeatRecipients(cfg, opts),
356
- },
357
- status: {
358
- defaultRuntime: {
359
- accountId: DEFAULT_ACCOUNT_ID,
360
- running: false,
361
- connected: false,
362
- reconnectAttempts: 0,
363
- lastConnectedAt: null,
364
- lastDisconnect: null,
365
- lastMessageAt: null,
366
- lastEventAt: null,
367
- lastError: null,
368
- },
369
- collectStatusIssues: collectWhatsAppStatusIssues,
370
- buildChannelSummary: async ({ account, snapshot }) => {
371
- const authDir = account.authDir;
372
- const linked =
373
- typeof snapshot.linked === "boolean"
374
- ? snapshot.linked
375
- : authDir
376
- ? await getWhatsAppRuntime().channel.whatsapp.webAuthExists(authDir)
377
- : false;
378
- const authAgeMs =
379
- linked && authDir ? getWhatsAppRuntime().channel.whatsapp.getWebAuthAgeMs(authDir) : null;
380
- const self =
381
- linked && authDir
382
- ? getWhatsAppRuntime().channel.whatsapp.readWebSelfId(authDir)
383
- : { e164: null, jid: null };
384
- return {
385
- configured: linked,
386
- linked,
387
- authAgeMs,
388
- self,
389
- running: snapshot.running ?? false,
390
- connected: snapshot.connected ?? false,
391
- lastConnectedAt: snapshot.lastConnectedAt ?? null,
392
- lastDisconnect: snapshot.lastDisconnect ?? null,
393
- reconnectAttempts: snapshot.reconnectAttempts,
394
- lastMessageAt: snapshot.lastMessageAt ?? null,
395
- lastEventAt: snapshot.lastEventAt ?? null,
396
- lastError: snapshot.lastError ?? null,
397
- };
398
- },
399
- buildAccountSnapshot: async ({ account, runtime }) => {
400
- const linked = await getWhatsAppRuntime().channel.whatsapp.webAuthExists(account.authDir);
401
- return {
402
- accountId: account.accountId,
403
- name: account.name,
404
- enabled: account.enabled,
405
- configured: true,
406
- linked,
407
- running: runtime?.running ?? false,
408
- connected: runtime?.connected ?? false,
409
- reconnectAttempts: runtime?.reconnectAttempts,
410
- lastConnectedAt: runtime?.lastConnectedAt ?? null,
411
- lastDisconnect: runtime?.lastDisconnect ?? null,
412
- lastMessageAt: runtime?.lastMessageAt ?? null,
413
- lastEventAt: runtime?.lastEventAt ?? null,
414
- lastError: runtime?.lastError ?? null,
415
- dmPolicy: account.dmPolicy,
416
- allowFrom: account.allowFrom,
417
- };
418
- },
419
- resolveAccountState: ({ configured }) => (configured ? "linked" : "not linked"),
420
- logSelfId: ({ account, runtime, includeChannelPrefix }) => {
421
- getWhatsAppRuntime().channel.whatsapp.logWebSelfId(
422
- account.authDir,
423
- runtime,
424
- includeChannelPrefix,
425
- );
426
- },
427
- },
428
- gateway: {
429
- startAccount: async (ctx) => {
430
- const account = ctx.account;
431
- const { e164, jid } = getWhatsAppRuntime().channel.whatsapp.readWebSelfId(account.authDir);
432
- const identity = e164 ? e164 : jid ? `jid ${jid}` : "unknown";
433
- ctx.log?.info(`[${account.accountId}] starting provider (${identity})`);
434
- return getWhatsAppRuntime().channel.whatsapp.monitorWebChannel(
435
- getWhatsAppRuntime().logging.shouldLogVerbose(),
436
- undefined,
437
- true,
438
- undefined,
439
- ctx.runtime,
440
- ctx.abortSignal,
441
- {
442
- statusSink: (next) => ctx.setStatus({ accountId: ctx.accountId, ...next }),
443
- accountId: account.accountId,
444
- },
445
- );
446
- },
447
- loginWithQrStart: async ({ accountId, force, timeoutMs, verbose }) =>
448
- await getWhatsAppRuntime().channel.whatsapp.startWebLoginWithQr({
449
- accountId,
450
- force,
451
- timeoutMs,
452
- verbose,
453
- }),
454
- loginWithQrWait: async ({ accountId, timeoutMs }) =>
455
- await getWhatsAppRuntime().channel.whatsapp.waitForWebLogin({ accountId, timeoutMs }),
456
- logoutAccount: async ({ account, runtime }) => {
457
- const cleared = await getWhatsAppRuntime().channel.whatsapp.logoutWeb({
458
- authDir: account.authDir,
459
- isLegacyAuthDir: account.isLegacyAuthDir,
460
- runtime,
461
- });
462
- return { cleared, loggedOut: cleared };
463
- },
464
- },
465
- };
@@ -1,170 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { installCommonResolveTargetErrorCases } from "../../shared/resolve-target-test-helpers.js";
3
-
4
- vi.mock("symi/plugin-sdk", () => ({
5
- getChatChannelMeta: () => ({ id: "whatsapp", label: "WhatsApp" }),
6
- normalizeWhatsAppTarget: (value: string) => {
7
- if (value === "invalid-target") return null;
8
- // Simulate E.164 normalization: strip leading + and whatsapp: prefix
9
- const stripped = value.replace(/^whatsapp:/i, "").replace(/^\+/, "");
10
- return stripped.includes("@g.us") ? stripped : `${stripped}@s.whatsapp.net`;
11
- },
12
- isWhatsAppGroupJid: (value: string) => value.endsWith("@g.us"),
13
- resolveWhatsAppOutboundTarget: ({
14
- to,
15
- allowFrom,
16
- mode,
17
- }: {
18
- to?: string;
19
- allowFrom: string[];
20
- mode: "explicit" | "implicit";
21
- }) => {
22
- const raw = typeof to === "string" ? to.trim() : "";
23
- if (!raw) {
24
- return { ok: false, error: new Error("missing target") };
25
- }
26
- const normalizeWhatsAppTarget = (value: string) => {
27
- if (value === "invalid-target") return null;
28
- const stripped = value.replace(/^whatsapp:/i, "").replace(/^\+/, "");
29
- return stripped.includes("@g.us") ? stripped : `${stripped}@s.whatsapp.net`;
30
- };
31
- const normalized = normalizeWhatsAppTarget(raw);
32
- if (!normalized) {
33
- return { ok: false, error: new Error("invalid target") };
34
- }
35
-
36
- if (mode === "implicit" && !normalized.endsWith("@g.us")) {
37
- const allowAll = allowFrom.includes("*");
38
- const allowExact = allowFrom.some((entry) => {
39
- if (!entry) {
40
- return false;
41
- }
42
- const normalizedEntry = normalizeWhatsAppTarget(entry.trim());
43
- return normalizedEntry?.toLowerCase() === normalized.toLowerCase();
44
- });
45
- if (!allowAll && !allowExact) {
46
- return { ok: false, error: new Error("target not allowlisted") };
47
- }
48
- }
49
-
50
- return { ok: true, to: normalized };
51
- },
52
- missingTargetError: (provider: string, hint: string) =>
53
- new Error(`Delivering to ${provider} requires target ${hint}`),
54
- WhatsAppConfigSchema: {},
55
- whatsappOnboardingAdapter: {},
56
- resolveWhatsAppHeartbeatRecipients: vi.fn(),
57
- buildChannelConfigSchema: vi.fn(),
58
- collectWhatsAppStatusIssues: vi.fn(),
59
- createActionGate: vi.fn(),
60
- DEFAULT_ACCOUNT_ID: "default",
61
- escapeRegExp: vi.fn(),
62
- formatPairingApproveHint: vi.fn(),
63
- listWhatsAppAccountIds: vi.fn(),
64
- listWhatsAppDirectoryGroupsFromConfig: vi.fn(),
65
- listWhatsAppDirectoryPeersFromConfig: vi.fn(),
66
- looksLikeWhatsAppTargetId: vi.fn(),
67
- migrateBaseNameToDefaultAccount: vi.fn(),
68
- normalizeAccountId: vi.fn(),
69
- normalizeE164: vi.fn(),
70
- normalizeWhatsAppMessagingTarget: vi.fn(),
71
- readStringParam: vi.fn(),
72
- resolveDefaultWhatsAppAccountId: vi.fn(),
73
- resolveWhatsAppAccount: vi.fn(),
74
- resolveWhatsAppGroupRequireMention: vi.fn(),
75
- resolveWhatsAppGroupToolPolicy: vi.fn(),
76
- applyAccountNameToChannelSection: vi.fn(),
77
- }));
78
-
79
- vi.mock("./runtime.js", () => ({
80
- getWhatsAppRuntime: vi.fn(() => ({
81
- channel: {
82
- text: { chunkText: vi.fn() },
83
- whatsapp: {
84
- sendMessageWhatsApp: vi.fn(),
85
- createLoginTool: vi.fn(),
86
- },
87
- },
88
- })),
89
- }));
90
-
91
- import { whatsappPlugin } from "./channel.js";
92
-
93
- const resolveTarget = whatsappPlugin.outbound!.resolveTarget!;
94
-
95
- describe("whatsapp resolveTarget", () => {
96
- it("should resolve valid target in explicit mode", () => {
97
- const result = resolveTarget({
98
- to: "5511999999999",
99
- mode: "explicit",
100
- allowFrom: [],
101
- });
102
-
103
- expect(result.ok).toBe(true);
104
- if (!result.ok) {
105
- throw result.error;
106
- }
107
- expect(result.to).toBe("5511999999999@s.whatsapp.net");
108
- });
109
-
110
- it("should resolve target in implicit mode with wildcard", () => {
111
- const result = resolveTarget({
112
- to: "5511999999999",
113
- mode: "implicit",
114
- allowFrom: ["*"],
115
- });
116
-
117
- expect(result.ok).toBe(true);
118
- if (!result.ok) {
119
- throw result.error;
120
- }
121
- expect(result.to).toBe("5511999999999@s.whatsapp.net");
122
- });
123
-
124
- it("should resolve target in implicit mode when in allowlist", () => {
125
- const result = resolveTarget({
126
- to: "5511999999999",
127
- mode: "implicit",
128
- allowFrom: ["5511999999999"],
129
- });
130
-
131
- expect(result.ok).toBe(true);
132
- if (!result.ok) {
133
- throw result.error;
134
- }
135
- expect(result.to).toBe("5511999999999@s.whatsapp.net");
136
- });
137
-
138
- it("should allow group JID regardless of allowlist", () => {
139
- const result = resolveTarget({
140
- to: "120363123456789@g.us",
141
- mode: "implicit",
142
- allowFrom: ["5511999999999"],
143
- });
144
-
145
- expect(result.ok).toBe(true);
146
- if (!result.ok) {
147
- throw result.error;
148
- }
149
- expect(result.to).toBe("120363123456789@g.us");
150
- });
151
-
152
- it("should error when target not in allowlist (implicit mode)", () => {
153
- const result = resolveTarget({
154
- to: "5511888888888",
155
- mode: "implicit",
156
- allowFrom: ["5511999999999", "5511777777777"],
157
- });
158
-
159
- expect(result.ok).toBe(false);
160
- if (result.ok) {
161
- throw new Error("expected resolution to fail");
162
- }
163
- expect(result.error).toBeDefined();
164
- });
165
-
166
- installCommonResolveTargetErrorCases({
167
- resolveTarget,
168
- implicitAllowFrom: ["5511999999999"],
169
- });
170
- });
@@ -1,14 +0,0 @@
1
- import type { PluginRuntime } from "symi/plugin-sdk";
2
-
3
- let runtime: PluginRuntime | null = null;
4
-
5
- export function setWhatsAppRuntime(next: PluginRuntime) {
6
- runtime = next;
7
- }
8
-
9
- export function getWhatsAppRuntime(): PluginRuntime {
10
- if (!runtime) {
11
- throw new Error("WhatsApp runtime not initialized");
12
- }
13
- return runtime;
14
- }
@@ -1,9 +0,0 @@
1
- {
2
- "id": "whatsapp",
3
- "channels": ["whatsapp"],
4
- "configSchema": {
5
- "type": "object",
6
- "additionalProperties": false,
7
- "properties": {}
8
- }
9
- }