@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,176 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ DEFAULT_ACCOUNT_ID,
5
+ normalizeAccountId,
6
+ resolveUserPath,
7
+ type AutoBotConfig,
8
+ } from "autobot/plugin-sdk/account-core";
9
+ import type { DmPolicy, GroupPolicy, ReplyToMode } from "autobot/plugin-sdk/config-contracts";
10
+ import { resolveOAuthDir } from "autobot/plugin-sdk/state-paths";
11
+ import { normalizeOptionalString } from "autobot/plugin-sdk/string-coerce-runtime";
12
+ import { resolveMergedWhatsAppAccountConfig } from "./account-config.js";
13
+ import {
14
+ listConfiguredAccountIds,
15
+ listWhatsAppAccountIds,
16
+ resolveDefaultWhatsAppAccountId,
17
+ } from "./account-ids.js";
18
+ import type { WhatsAppAccountConfig } from "./account-types.js";
19
+ import { hasWebCredsSync } from "./creds-files.js";
20
+
21
+ export { listWhatsAppAccountIds, resolveDefaultWhatsAppAccountId } from "./account-ids.js";
22
+
23
+ export type ResolvedWhatsAppAccount = {
24
+ accountId: string;
25
+ name?: string;
26
+ enabled: boolean;
27
+ sendReadReceipts: boolean;
28
+ messagePrefix?: string;
29
+ defaultTo?: string;
30
+ authDir: string;
31
+ isLegacyAuthDir: boolean;
32
+ selfChatMode?: boolean;
33
+ allowFrom?: string[];
34
+ groupAllowFrom?: string[];
35
+ groupPolicy?: GroupPolicy;
36
+ dmPolicy?: DmPolicy;
37
+ historyLimit?: number;
38
+ textChunkLimit?: number;
39
+ chunkMode?: "length" | "newline";
40
+ mediaMaxMb?: number;
41
+ blockStreaming?: boolean;
42
+ ackReaction?: WhatsAppAccountConfig["ackReaction"];
43
+ reactionLevel?: WhatsAppAccountConfig["reactionLevel"];
44
+ groups?: WhatsAppAccountConfig["groups"];
45
+ direct?: WhatsAppAccountConfig["direct"];
46
+ debounceMs?: number;
47
+ replyToMode?: ReplyToMode;
48
+ };
49
+
50
+ export const DEFAULT_WHATSAPP_MEDIA_MAX_MB = 50;
51
+
52
+ export function listWhatsAppAuthDirs(cfg: AutoBotConfig): string[] {
53
+ const oauthDir = resolveOAuthDir();
54
+ const whatsappDir = path.join(oauthDir, "whatsapp");
55
+ const authDirs = new Set<string>([oauthDir, path.join(whatsappDir, DEFAULT_ACCOUNT_ID)]);
56
+
57
+ const accountIds = listConfiguredAccountIds(cfg);
58
+ for (const accountId of accountIds) {
59
+ authDirs.add(resolveWhatsAppAuthDir({ cfg, accountId }).authDir);
60
+ }
61
+
62
+ try {
63
+ const entries = fs.readdirSync(whatsappDir, { withFileTypes: true });
64
+ for (const entry of entries) {
65
+ if (!entry.isDirectory()) {
66
+ continue;
67
+ }
68
+ authDirs.add(path.join(whatsappDir, entry.name));
69
+ }
70
+ } catch {
71
+ // ignore missing dirs
72
+ }
73
+
74
+ return Array.from(authDirs);
75
+ }
76
+
77
+ export function hasAnyWhatsAppAuth(cfg: AutoBotConfig): boolean {
78
+ return listWhatsAppAuthDirs(cfg).some((authDir) => hasWebCredsSync(authDir));
79
+ }
80
+
81
+ function resolveDefaultAuthDir(accountId: string): string {
82
+ return path.join(resolveOAuthDir(), "whatsapp", normalizeAccountId(accountId));
83
+ }
84
+
85
+ function resolveLegacyAuthDir(): string {
86
+ // Legacy Baileys creds lived in the same directory as OAuth tokens.
87
+ return resolveOAuthDir();
88
+ }
89
+
90
+ function legacyAuthExists(authDir: string): boolean {
91
+ try {
92
+ return fs.existsSync(path.join(authDir, "creds.json"));
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ export function resolveWhatsAppAuthDir(params: { cfg: AutoBotConfig; accountId: string }): {
99
+ authDir: string;
100
+ isLegacy: boolean;
101
+ } {
102
+ const accountId = params.accountId.trim() || DEFAULT_ACCOUNT_ID;
103
+ const account = resolveMergedWhatsAppAccountConfig({ cfg: params.cfg, accountId });
104
+ const configured = account?.authDir?.trim();
105
+ if (configured) {
106
+ return { authDir: resolveUserPath(configured), isLegacy: false };
107
+ }
108
+
109
+ const defaultDir = resolveDefaultAuthDir(accountId);
110
+ if (accountId === DEFAULT_ACCOUNT_ID) {
111
+ const legacyDir = resolveLegacyAuthDir();
112
+ if (legacyAuthExists(legacyDir) && !legacyAuthExists(defaultDir)) {
113
+ return { authDir: legacyDir, isLegacy: true };
114
+ }
115
+ }
116
+
117
+ return { authDir: defaultDir, isLegacy: false };
118
+ }
119
+
120
+ export function resolveWhatsAppAccount(params: {
121
+ cfg: AutoBotConfig;
122
+ accountId?: string | null;
123
+ }): ResolvedWhatsAppAccount {
124
+ const merged = resolveMergedWhatsAppAccountConfig({
125
+ cfg: params.cfg,
126
+ accountId: params.accountId?.trim() || resolveDefaultWhatsAppAccountId(params.cfg),
127
+ });
128
+ const accountId = merged.accountId;
129
+ const enabled = merged.enabled !== false;
130
+ const { authDir, isLegacy } = resolveWhatsAppAuthDir({
131
+ cfg: params.cfg,
132
+ accountId,
133
+ });
134
+ return {
135
+ accountId,
136
+ name: normalizeOptionalString(merged.name),
137
+ enabled,
138
+ sendReadReceipts: merged.sendReadReceipts ?? true,
139
+ messagePrefix: merged.messagePrefix ?? params.cfg.messages?.messagePrefix,
140
+ defaultTo: merged.defaultTo,
141
+ authDir,
142
+ isLegacyAuthDir: isLegacy,
143
+ selfChatMode: merged.selfChatMode,
144
+ dmPolicy: merged.dmPolicy,
145
+ allowFrom: merged.allowFrom,
146
+ groupAllowFrom: merged.groupAllowFrom,
147
+ groupPolicy: merged.groupPolicy,
148
+ historyLimit: merged.historyLimit,
149
+ textChunkLimit: merged.textChunkLimit,
150
+ chunkMode: merged.chunkMode,
151
+ mediaMaxMb: merged.mediaMaxMb,
152
+ blockStreaming: merged.blockStreaming,
153
+ ackReaction: merged.ackReaction,
154
+ reactionLevel: merged.reactionLevel,
155
+ groups: merged.groups,
156
+ direct: merged.direct,
157
+ debounceMs: merged.debounceMs,
158
+ replyToMode: merged.replyToMode,
159
+ };
160
+ }
161
+
162
+ export function resolveWhatsAppMediaMaxBytes(
163
+ account: Pick<ResolvedWhatsAppAccount, "mediaMaxMb">,
164
+ ): number {
165
+ const mediaMaxMb =
166
+ typeof account.mediaMaxMb === "number" && account.mediaMaxMb > 0
167
+ ? account.mediaMaxMb
168
+ : DEFAULT_WHATSAPP_MEDIA_MAX_MB;
169
+ return mediaMaxMb * 1024 * 1024;
170
+ }
171
+
172
+ export function listEnabledWhatsAppAccounts(cfg: AutoBotConfig): ResolvedWhatsAppAccount[] {
173
+ return listWhatsAppAccountIds(cfg)
174
+ .map((accountId) => resolveWhatsAppAccount({ cfg, accountId }))
175
+ .filter((account) => account.enabled);
176
+ }
@@ -0,0 +1,27 @@
1
+ import { ToolAuthorizationError } from "autobot/plugin-sdk/channel-actions";
2
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
3
+ import { resolveWhatsAppAccount } from "./accounts.js";
4
+ import { resolveWhatsAppOutboundTarget } from "./resolve-outbound-target.js";
5
+
6
+ export function resolveAuthorizedWhatsAppOutboundTarget(params: {
7
+ cfg: AutoBotConfig;
8
+ chatJid: string;
9
+ accountId?: string;
10
+ actionLabel: string;
11
+ }): { to: string; accountId: string } {
12
+ const account = resolveWhatsAppAccount({
13
+ cfg: params.cfg,
14
+ accountId: params.accountId,
15
+ });
16
+ const resolution = resolveWhatsAppOutboundTarget({
17
+ to: params.chatJid,
18
+ allowFrom: account.allowFrom ?? [],
19
+ mode: "implicit",
20
+ });
21
+ if (!resolution.ok) {
22
+ throw new ToolAuthorizationError(
23
+ `WhatsApp ${params.actionLabel} blocked: chatJid "${params.chatJid}" is not in the configured allowFrom list for account "${account.accountId}".`,
24
+ );
25
+ }
26
+ return { to: resolution.to, accountId: account.accountId };
27
+ }
@@ -0,0 +1,76 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import {
3
+ createActionGate,
4
+ jsonResult,
5
+ readReactionParams,
6
+ readStringParam,
7
+ } from "autobot/plugin-sdk/channel-actions";
8
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
9
+ import { resolveAuthorizedWhatsAppOutboundTarget } from "./action-runtime-target-auth.js";
10
+ import { resolveWhatsAppReactionLevel } from "./reaction-level.js";
11
+ import { sendReactionWhatsApp } from "./send.js";
12
+
13
+ export const whatsAppActionRuntime = {
14
+ resolveAuthorizedWhatsAppOutboundTarget,
15
+ sendReactionWhatsApp,
16
+ };
17
+
18
+ export async function handleWhatsAppAction(
19
+ params: Record<string, unknown>,
20
+ cfg: AutoBotConfig,
21
+ ): Promise<AgentToolResult<unknown>> {
22
+ const action = readStringParam(params, "action", { required: true });
23
+ const whatsAppConfig = cfg.channels?.whatsapp;
24
+ const isActionEnabled = createActionGate(whatsAppConfig?.actions);
25
+
26
+ if (action === "react") {
27
+ const accountId = readStringParam(params, "accountId");
28
+ if (!whatsAppConfig) {
29
+ throw new Error("WhatsApp reactions are disabled.");
30
+ }
31
+ if (!isActionEnabled("reactions")) {
32
+ throw new Error("WhatsApp reactions are disabled.");
33
+ }
34
+ const reactionLevelInfo = resolveWhatsAppReactionLevel({
35
+ cfg,
36
+ accountId: accountId ?? undefined,
37
+ });
38
+ if (!reactionLevelInfo.agentReactionsEnabled) {
39
+ throw new Error(
40
+ `WhatsApp agent reactions disabled (reactionLevel="${reactionLevelInfo.level}"). ` +
41
+ `Set channels.whatsapp.reactionLevel to "minimal" or "extensive" to enable.`,
42
+ );
43
+ }
44
+ const chatJid = readStringParam(params, "chatJid", { required: true });
45
+ const messageId = readStringParam(params, "messageId", { required: true });
46
+ const { emoji, remove, isEmpty } = readReactionParams(params, {
47
+ removeErrorMessage: "Emoji is required to remove a WhatsApp reaction.",
48
+ });
49
+ const participant = readStringParam(params, "participant");
50
+ const fromMeRaw = params.fromMe;
51
+ const fromMe = typeof fromMeRaw === "boolean" ? fromMeRaw : undefined;
52
+
53
+ // Resolve account + allowFrom via shared account logic so auth and routing stay aligned.
54
+ const resolved = whatsAppActionRuntime.resolveAuthorizedWhatsAppOutboundTarget({
55
+ cfg,
56
+ chatJid,
57
+ accountId,
58
+ actionLabel: "reaction",
59
+ });
60
+
61
+ const resolvedEmoji = remove ? "" : emoji;
62
+ await whatsAppActionRuntime.sendReactionWhatsApp(resolved.to, messageId, resolvedEmoji, {
63
+ verbose: false,
64
+ fromMe,
65
+ participant: participant ?? undefined,
66
+ accountId: resolved.accountId,
67
+ cfg,
68
+ });
69
+ if (!remove && !isEmpty) {
70
+ return jsonResult({ ok: true, added: emoji });
71
+ }
72
+ return jsonResult({ ok: true, removed: true });
73
+ }
74
+
75
+ throw new Error(`Unsupported WhatsApp action: ${action}`);
76
+ }
@@ -0,0 +1,17 @@
1
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
2
+ import { resolveDefaultWhatsAppAccountId } from "./account-ids.js";
3
+ import { getRegisteredWhatsAppConnectionController } from "./connection-controller-registry.js";
4
+ import type { ActiveWebListener } from "./inbound/types.js";
5
+
6
+ export type { ActiveWebListener, ActiveWebSendOptions } from "./inbound/types.js";
7
+
8
+ export function resolveWebAccountId(params: {
9
+ cfg: AutoBotConfig;
10
+ accountId?: string | null;
11
+ }): string {
12
+ return (params.accountId ?? "").trim() || resolveDefaultWhatsAppAccountId(params.cfg);
13
+ }
14
+
15
+ export function getActiveWebListener(accountId: string): ActiveWebListener | null {
16
+ return getRegisteredWhatsAppConnectionController(accountId)?.getActiveListener() ?? null;
17
+ }
@@ -0,0 +1,113 @@
1
+ import type { ChannelAgentTool } from "autobot/plugin-sdk/channel-contract";
2
+ import { Type } from "typebox";
3
+ import { startWebLoginWithQr, waitForWebLogin } from "../login-qr-api.js";
4
+
5
+ const QR_DATA_URL_MAX_LENGTH = 16_384;
6
+
7
+ function readOptionalString(value: unknown): string | undefined {
8
+ return typeof value === "string" && value.trim() ? value : undefined;
9
+ }
10
+
11
+ export function createWhatsAppLoginTool(): ChannelAgentTool {
12
+ return {
13
+ label: "WhatsApp Login",
14
+ name: "whatsapp_login",
15
+ ownerOnly: true,
16
+ description: "Generate a WhatsApp QR code for linking, or wait for the scan to complete.",
17
+ // NOTE: Using Type.Unsafe for action enum instead of Type.Union([Type.Literal(...)]
18
+ // because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema.
19
+ parameters: Type.Object({
20
+ action: Type.Unsafe<"start" | "wait">({
21
+ type: "string",
22
+ enum: ["start", "wait"],
23
+ }),
24
+ timeoutMs: Type.Optional(Type.Number()),
25
+ force: Type.Optional(Type.Boolean()),
26
+ accountId: Type.Optional(Type.String()),
27
+ currentQrDataUrl: Type.Optional(
28
+ Type.String({
29
+ maxLength: QR_DATA_URL_MAX_LENGTH,
30
+ pattern: "^data:image/png;base64,",
31
+ }),
32
+ ),
33
+ }),
34
+ execute: async (_toolCallId, args) => {
35
+ const renderQrReply = (params: {
36
+ message: string;
37
+ qrDataUrl: string;
38
+ connected?: boolean;
39
+ }) => {
40
+ const text = [
41
+ params.message,
42
+ "",
43
+ "Open WhatsApp → Linked Devices and scan:",
44
+ "",
45
+ `![whatsapp-qr](${params.qrDataUrl})`,
46
+ ].join("\n");
47
+ return {
48
+ content: [{ type: "text" as const, text }],
49
+ details: {
50
+ connected: params.connected ?? false,
51
+ qr: true,
52
+ },
53
+ };
54
+ };
55
+
56
+ const action = (args as { action?: string })?.action ?? "start";
57
+ const accountId = readOptionalString((args as { accountId?: unknown }).accountId);
58
+ if (action === "wait") {
59
+ const result = await waitForWebLogin({
60
+ accountId,
61
+ timeoutMs:
62
+ typeof (args as { timeoutMs?: unknown }).timeoutMs === "number"
63
+ ? (args as { timeoutMs?: number }).timeoutMs
64
+ : undefined,
65
+ currentQrDataUrl: readOptionalString(
66
+ (args as { currentQrDataUrl?: unknown }).currentQrDataUrl,
67
+ ),
68
+ });
69
+ if (result.qrDataUrl) {
70
+ return renderQrReply({
71
+ message: result.message,
72
+ qrDataUrl: result.qrDataUrl,
73
+ connected: result.connected,
74
+ });
75
+ }
76
+ return {
77
+ content: [{ type: "text", text: result.message }],
78
+ details: { connected: result.connected },
79
+ };
80
+ }
81
+
82
+ const result = await startWebLoginWithQr({
83
+ accountId,
84
+ timeoutMs:
85
+ typeof (args as { timeoutMs?: unknown }).timeoutMs === "number"
86
+ ? (args as { timeoutMs?: number }).timeoutMs
87
+ : undefined,
88
+ force:
89
+ typeof (args as { force?: unknown }).force === "boolean"
90
+ ? (args as { force?: boolean }).force
91
+ : false,
92
+ });
93
+
94
+ if (!result.qrDataUrl) {
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: result.message,
100
+ },
101
+ ],
102
+ details: { qr: false },
103
+ };
104
+ }
105
+
106
+ return renderQrReply({
107
+ message: result.message,
108
+ qrDataUrl: result.qrDataUrl,
109
+ connected: result.connected,
110
+ });
111
+ },
112
+ };
113
+ }
@@ -0,0 +1,27 @@
1
+ import {
2
+ createResolvedApproverActionAuthAdapter,
3
+ resolveApprovalApprovers,
4
+ } from "autobot/plugin-sdk/approval-auth-runtime";
5
+ import { resolveWhatsAppAccount } from "./accounts.js";
6
+ import { normalizeWhatsAppTarget } from "./normalize.js";
7
+
8
+ function normalizeWhatsAppApproverId(value: string | number): string | undefined {
9
+ const normalized = normalizeWhatsAppTarget(String(value));
10
+ if (!normalized || normalized.endsWith("@g.us")) {
11
+ return undefined;
12
+ }
13
+ return normalized;
14
+ }
15
+
16
+ export const whatsappApprovalAuth = createResolvedApproverActionAuthAdapter({
17
+ channelLabel: "WhatsApp",
18
+ resolveApprovers: ({ cfg, accountId }) => {
19
+ const account = resolveWhatsAppAccount({ cfg, accountId });
20
+ return resolveApprovalApprovers({
21
+ allowFrom: account.allowFrom,
22
+ defaultTo: account.defaultTo,
23
+ normalizeApprover: normalizeWhatsAppApproverId,
24
+ });
25
+ },
26
+ normalizeSenderId: (value) => normalizeWhatsAppApproverId(value),
27
+ });
@@ -0,0 +1 @@
1
+ export { resolveOAuthDir } from "autobot/plugin-sdk/state-paths";