@moxxy/plugin-channel-whatsapp 0.27.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth-state.d.ts +66 -0
  3. package/dist/auth-state.d.ts.map +1 -0
  4. package/dist/auth-state.js +99 -0
  5. package/dist/auth-state.js.map +1 -0
  6. package/dist/baileys-socket.d.ts +10 -0
  7. package/dist/baileys-socket.d.ts.map +1 -0
  8. package/dist/baileys-socket.js +93 -0
  9. package/dist/baileys-socket.js.map +1 -0
  10. package/dist/channel/turn-runner.d.ts +58 -0
  11. package/dist/channel/turn-runner.d.ts.map +1 -0
  12. package/dist/channel/turn-runner.js +130 -0
  13. package/dist/channel/turn-runner.js.map +1 -0
  14. package/dist/channel/voice-handler.d.ts +27 -0
  15. package/dist/channel/voice-handler.d.ts.map +1 -0
  16. package/dist/channel/voice-handler.js +55 -0
  17. package/dist/channel/voice-handler.js.map +1 -0
  18. package/dist/channel.d.ts +105 -0
  19. package/dist/channel.d.ts.map +1 -0
  20. package/dist/channel.js +459 -0
  21. package/dist/channel.js.map +1 -0
  22. package/dist/consent-prompt.d.ts +9 -0
  23. package/dist/consent-prompt.d.ts.map +1 -0
  24. package/dist/consent-prompt.js +28 -0
  25. package/dist/consent-prompt.js.map +1 -0
  26. package/dist/consent.d.ts +22 -0
  27. package/dist/consent.d.ts.map +1 -0
  28. package/dist/consent.js +40 -0
  29. package/dist/consent.js.map +1 -0
  30. package/dist/index.d.ts +26 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +211 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/keys.d.ts +39 -0
  35. package/dist/keys.d.ts.map +1 -0
  36. package/dist/keys.js +86 -0
  37. package/dist/keys.js.map +1 -0
  38. package/dist/message-gate.d.ts +50 -0
  39. package/dist/message-gate.d.ts.map +1 -0
  40. package/dist/message-gate.js +150 -0
  41. package/dist/message-gate.js.map +1 -0
  42. package/dist/pair-flow.d.ts +12 -0
  43. package/dist/pair-flow.d.ts.map +1 -0
  44. package/dist/pair-flow.js +120 -0
  45. package/dist/pair-flow.js.map +1 -0
  46. package/dist/permission.d.ts +29 -0
  47. package/dist/permission.d.ts.map +1 -0
  48. package/dist/permission.js +78 -0
  49. package/dist/permission.js.map +1 -0
  50. package/dist/setup-wizard.d.ts +13 -0
  51. package/dist/setup-wizard.d.ts.map +1 -0
  52. package/dist/setup-wizard.js +127 -0
  53. package/dist/setup-wizard.js.map +1 -0
  54. package/dist/socket.d.ts +72 -0
  55. package/dist/socket.d.ts.map +1 -0
  56. package/dist/socket.js +22 -0
  57. package/dist/socket.js.map +1 -0
  58. package/package.json +99 -0
  59. package/src/auth-state.test.ts +103 -0
  60. package/src/auth-state.ts +161 -0
  61. package/src/baileys-socket.ts +154 -0
  62. package/src/channel/turn-runner.test.ts +31 -0
  63. package/src/channel/turn-runner.ts +156 -0
  64. package/src/channel/voice-handler.ts +83 -0
  65. package/src/channel.test.ts +344 -0
  66. package/src/channel.ts +571 -0
  67. package/src/consent-prompt.ts +28 -0
  68. package/src/consent.test.ts +56 -0
  69. package/src/consent.ts +48 -0
  70. package/src/index.ts +284 -0
  71. package/src/keys.test.ts +59 -0
  72. package/src/keys.ts +78 -0
  73. package/src/message-gate.test.ts +121 -0
  74. package/src/message-gate.ts +184 -0
  75. package/src/pair-flow.ts +133 -0
  76. package/src/permission.test.ts +94 -0
  77. package/src/permission.ts +114 -0
  78. package/src/setup-wizard.ts +162 -0
  79. package/src/socket.test.ts +17 -0
  80. package/src/socket.ts +89 -0
  81. package/src/subcommands.test.ts +198 -0
@@ -0,0 +1,150 @@
1
+ import { z } from 'zod';
2
+ import { normalizeJid } from './keys.js';
3
+ /**
4
+ * The single inbound gate every session-reaching path goes through (AGENTS.md:
5
+ * gate EVERY session-reaching path behind pairing; A8: zod-validate inbound
6
+ * before it touches the session). Pure — the channel feeds it the message plus
7
+ * its identity/allow-list state and branches on the verdict.
8
+ *
9
+ * Order of checks (each is load-bearing):
10
+ * 1. only `notify` upserts — history syncs / appends are not fresh input.
11
+ * 2. shape-validate + size-cap the consumed fields (zod).
12
+ * 3. drop own echoes: Baileys receives this client's OWN outbound sends back
13
+ * via `messages.upsert`; without the sent-id check the bot replies to
14
+ * itself in a loop.
15
+ * 4. drop `fromMe` messages outside the owner's self-chat ("Note to Self"):
16
+ * those are the owner talking to OTHER people from their phone — the bot
17
+ * must never treat someone's private conversation as a prompt.
18
+ * 5. allow-list by normalized JID: the owner's self-chat is allowed by
19
+ * default (seeded by the channel); everything else must be explicitly
20
+ * allow-listed. Unauthorized senders are dropped SILENTLY — replying would
21
+ * both leak the bot's existence and look like spam (ban risk).
22
+ */
23
+ /** Cap on inbound prompt text (WhatsApp itself allows ~65k). */
24
+ export const MAX_TEXT_CHARS = 16_000;
25
+ /** Cap on a voice note / audio file we will download + buffer for transcription. */
26
+ export const MAX_AUDIO_BYTES = 20 * 1024 * 1024;
27
+ const keySchema = z.object({
28
+ remoteJid: z.string().min(1).max(256),
29
+ fromMe: z.boolean().nullish(),
30
+ id: z.string().min(1).max(256).nullish(),
31
+ });
32
+ const audioMessageSchema = z.object({
33
+ mimetype: z.string().max(256).nullish(),
34
+ fileLength: z.union([z.number(), z.string(), z.object({}).passthrough()]).nullish(),
35
+ seconds: z.number().nullish(),
36
+ ptt: z.boolean().nullish(),
37
+ });
38
+ export function gateInboundMessage(state, upsertType, raw) {
39
+ if (upsertType !== 'notify')
40
+ return drop('not a live notify upsert');
41
+ const key = keySchema.safeParse(raw?.key ?? {});
42
+ if (!key.success)
43
+ return drop('invalid message key shape');
44
+ const { remoteJid, fromMe, id } = key.data;
45
+ if (remoteJid === 'status@broadcast')
46
+ return drop('status broadcast');
47
+ const jid = normalizeJid(remoteJid);
48
+ if (!jid)
49
+ return drop('unparseable chat JID');
50
+ if (id && state.isOwnSend(id))
51
+ return drop('own outbound echo');
52
+ if (fromMe) {
53
+ // Same-account messages: only the owner's self-chat is a prompt surface.
54
+ if (state.ownJid == null || jid !== state.ownJid) {
55
+ return drop('own message in a foreign chat');
56
+ }
57
+ }
58
+ else if (!state.allowedJids.has(jid)) {
59
+ return drop('sender not allow-listed');
60
+ }
61
+ const content = unwrapMessageContent(raw?.message ?? null);
62
+ if (!content)
63
+ return drop('no message content');
64
+ const text = extractText(content);
65
+ if (text != null) {
66
+ const trimmed = text.trim();
67
+ if (trimmed.length === 0)
68
+ return drop('empty text');
69
+ if (trimmed.length > MAX_TEXT_CHARS)
70
+ return drop('text over size cap');
71
+ return { ok: true, kind: 'text', jid, text: trimmed };
72
+ }
73
+ const audioRaw = content['audioMessage'];
74
+ if (audioRaw != null && typeof audioRaw === 'object') {
75
+ const audio = audioMessageSchema.safeParse(audioRaw);
76
+ if (!audio.success)
77
+ return drop('invalid audio message shape');
78
+ const declaredBytes = toFiniteNumber(audio.data.fileLength);
79
+ if (declaredBytes != null && declaredBytes > MAX_AUDIO_BYTES) {
80
+ return drop('audio over size cap');
81
+ }
82
+ return {
83
+ ok: true,
84
+ kind: 'audio',
85
+ jid,
86
+ mimeType: normalizeMime(audio.data.mimetype),
87
+ declaredBytes,
88
+ };
89
+ }
90
+ return drop('unsupported message type');
91
+ }
92
+ function drop(reason) {
93
+ return { ok: false, reason };
94
+ }
95
+ /**
96
+ * Unwrap the containers WhatsApp nests real content in (disappearing-message
97
+ * chats wrap everything in `ephemeralMessage`, view-once in `viewOnceMessage*`).
98
+ * Bounded depth — malformed nesting must not recurse forever.
99
+ */
100
+ function unwrapMessageContent(message) {
101
+ let current = message;
102
+ for (let depth = 0; current != null && depth < 4; depth++) {
103
+ const wrapper = pickInner(current, 'ephemeralMessage') ??
104
+ pickInner(current, 'viewOnceMessage') ??
105
+ pickInner(current, 'viewOnceMessageV2') ??
106
+ pickInner(current, 'documentWithCaptionMessage');
107
+ if (!wrapper)
108
+ return current;
109
+ current = wrapper;
110
+ }
111
+ return current;
112
+ }
113
+ function pickInner(container, field) {
114
+ const wrapper = container[field];
115
+ if (wrapper == null || typeof wrapper !== 'object')
116
+ return null;
117
+ const inner = wrapper.message;
118
+ return inner != null && typeof inner === 'object' ? inner : null;
119
+ }
120
+ function extractText(content) {
121
+ const conversation = content['conversation'];
122
+ if (typeof conversation === 'string')
123
+ return conversation;
124
+ const extended = content['extendedTextMessage'];
125
+ if (extended != null && typeof extended === 'object') {
126
+ const text = extended.text;
127
+ if (typeof text === 'string')
128
+ return text;
129
+ }
130
+ return null;
131
+ }
132
+ /** `fileLength` arrives as number, decimal string, or a Long-like object. */
133
+ function toFiniteNumber(value) {
134
+ if (typeof value === 'number' && Number.isFinite(value))
135
+ return value;
136
+ if (typeof value === 'string') {
137
+ const n = Number(value);
138
+ return Number.isFinite(n) ? n : null;
139
+ }
140
+ if (value != null && typeof value === 'object') {
141
+ const n = Number(value.toString());
142
+ return Number.isFinite(n) ? n : null;
143
+ }
144
+ return null;
145
+ }
146
+ function normalizeMime(mime) {
147
+ const trimmed = mime?.trim();
148
+ return trimmed && trimmed.length > 0 ? trimmed : 'audio/ogg; codecs=opus';
149
+ }
150
+ //# sourceMappingURL=message-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-gate.js","sourceRoot":"","sources":["../src/message-gate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { type ChannelSubcommandContext } from '@moxxy/sdk';
2
+ /**
3
+ * Drive the QR device-link pairing flow end-to-end in a terminal — the SAME
4
+ * mechanism the desktop Channels panel uses (it renders `channel.requestUrl`
5
+ * from the status file), just rendered inline here.
6
+ *
7
+ * Unlike Telegram's one-shot deep link, the Baileys QR payload ROTATES every
8
+ * ~20-60s while unlinked, so the flow re-renders on every connect-change until
9
+ * the scan lands.
10
+ */
11
+ export declare function runWhatsAppPairFlow(ctx: ChannelSubcommandContext): Promise<number>;
12
+ //# sourceMappingURL=pair-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pair-flow.d.ts","sourceRoot":"","sources":["../src/pair-flow.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0B,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAUnF;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkGxF"}
@@ -0,0 +1,120 @@
1
+ import { log, outro, spinner } from '@clack/prompts';
2
+ import QRCode from 'qrcode';
3
+ import { exitAfterPairRequested } from '@moxxy/sdk';
4
+ import { WhatsAppChannel } from './channel.js';
5
+ import { CONSENT_REQUIRED_MESSAGE } from './consent.js';
6
+ import { ensureConsentInteractive } from './consent-prompt.js';
7
+ // Tiny zero-dep ANSI dim helper, so this flow stays inside the plugin.
8
+ const ANSI = process.stdout.isTTY && !process.env.NO_COLOR;
9
+ const dim = (s) => (ANSI ? `\x1b[2m${s}\x1b[22m` : s);
10
+ /**
11
+ * Drive the QR device-link pairing flow end-to-end in a terminal — the SAME
12
+ * mechanism the desktop Channels panel uses (it renders `channel.requestUrl`
13
+ * from the status file), just rendered inline here.
14
+ *
15
+ * Unlike Telegram's one-shot deep link, the Baileys QR payload ROTATES every
16
+ * ~20-60s while unlinked, so the flow re-renders on every connect-change until
17
+ * the scan lands.
18
+ */
19
+ export async function runWhatsAppPairFlow(ctx) {
20
+ const vault = ctx.deps.vault;
21
+ // Consent gate FIRST — pairing is the moment the account actually links.
22
+ if (!(await ensureConsentInteractive(vault))) {
23
+ process.stderr.write(`${CONSENT_REQUIRED_MESSAGE}\n`);
24
+ return 1;
25
+ }
26
+ const session = ctx.session;
27
+ const channel = new WhatsAppChannel({
28
+ vault,
29
+ logger: ctx.deps.logger,
30
+ });
31
+ session.setPermissionResolver(channel.permissionResolver);
32
+ // Subscribe BEFORE start so a fast scan can't race us.
33
+ let pairedResolve = null;
34
+ const paired = new Promise((resolve) => {
35
+ pairedResolve = resolve;
36
+ });
37
+ const unsubscribePaired = channel.onPaired((ownerJid) => {
38
+ pairedResolve?.(ownerJid);
39
+ pairedResolve = null;
40
+ });
41
+ outro(dim('opening WhatsApp pairing window...'));
42
+ const handle = await channel.start({ session, pair: true });
43
+ const stopChannel = async () => {
44
+ unsubscribePaired();
45
+ try {
46
+ await handle.stop('pair flow');
47
+ }
48
+ catch {
49
+ /* ignore */
50
+ }
51
+ };
52
+ if (channel.connected) {
53
+ log.info('A WhatsApp account is already linked. Run `moxxy channels whatsapp unpair` first to link a different one.');
54
+ await stopChannel();
55
+ return 0;
56
+ }
57
+ // Render every FRESH QR payload (they rotate while unlinked).
58
+ let lastQr = null;
59
+ const renderQr = async () => {
60
+ const qr = channel.requestUrl;
61
+ if (!qr || qr === lastQr)
62
+ return;
63
+ lastQr = qr;
64
+ await printPairQr(qr);
65
+ log.info('On your phone: WhatsApp -> Settings -> Linked devices -> Link a device, then scan the QR.');
66
+ };
67
+ const unsubscribeConnect = handle.onConnectChange?.(() => void renderQr()) ?? (() => undefined);
68
+ await renderQr();
69
+ let stopping = false;
70
+ const shutdown = async () => {
71
+ if (stopping)
72
+ return;
73
+ stopping = true;
74
+ unsubscribeConnect();
75
+ await stopChannel();
76
+ await session.close('SIGINT').catch(() => undefined);
77
+ process.exit(0);
78
+ };
79
+ const onSignal = () => void shutdown();
80
+ process.once('SIGINT', onSignal);
81
+ process.once('SIGTERM', onSignal);
82
+ const spin = spinner();
83
+ spin.start('Waiting for the scan (the QR refreshes periodically)...');
84
+ const ownerJid = await paired;
85
+ spin.stop(`Linked as ${ownerJid}. Your Note-to-Self chat now talks to moxxy.`);
86
+ if (exitAfterPairRequested(ctx)) {
87
+ // Orchestrated pairing (`moxxy onboard`): hand control back — the caller
88
+ // starts the channel under its own service afterwards. Our SIGINT
89
+ // handlers would `process.exit` the orchestrator, so drop them first.
90
+ unsubscribeConnect();
91
+ process.removeListener('SIGINT', onSignal);
92
+ process.removeListener('SIGTERM', onSignal);
93
+ await stopChannel();
94
+ return 0;
95
+ }
96
+ log.info('Channel is running. Press Ctrl+C to stop.');
97
+ try {
98
+ await handle.running;
99
+ return 0;
100
+ }
101
+ finally {
102
+ unsubscribeConnect();
103
+ process.removeListener('SIGINT', onSignal);
104
+ process.removeListener('SIGTERM', onSignal);
105
+ }
106
+ }
107
+ /** Render the rotating pairing payload as a scannable terminal QR. */
108
+ async function printPairQr(payload) {
109
+ let qr = '';
110
+ try {
111
+ qr = await QRCode.toString(payload, { type: 'terminal', small: true });
112
+ }
113
+ catch {
114
+ qr = '';
115
+ }
116
+ // CLI surface — intentional stdout. (The payload itself is an opaque pairing
117
+ // blob, not a URL, so there is no "open the link" fallback to print.)
118
+ console.log(['', qr].join('\n'));
119
+ }
120
+ //# sourceMappingURL=pair-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pair-flow.js","sourceRoot":"","sources":["../src/pair-flow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { type DeferredPermissionResolver, type PendingToolCall, type PermissionDecision } from '@moxxy/sdk';
2
+ /**
3
+ * Human-in-the-loop permissions over plain WhatsApp messages. Baileys has no
4
+ * reliable interactive buttons on multi-device, so the prompt is text: the
5
+ * channel sends a numbered question into the owner's chat and interprets the
6
+ * owner's NEXT short reply (`1`/`yes` allow once, `2`/`always` allow for the
7
+ * session, `3`/`no` deny) — the same capture-next-message mechanism Telegram
8
+ * uses for approval follow-up text.
9
+ *
10
+ * Built ON `createDeferredPermissionResolver` (never re-implement the pending
11
+ * tracking — the audit caught a TUI hang from exactly that): the sdk scaffold
12
+ * owns session-allows + abort-on-stop; this wrapper only owns the reply
13
+ * queue + parsing.
14
+ */
15
+ export interface WhatsAppPermissionController {
16
+ readonly resolver: DeferredPermissionResolver;
17
+ /** Wire the outbound sender once the socket is up (null on teardown). */
18
+ setSender(send: ((text: string) => Promise<void>) | null): void;
19
+ /** True while a prompt is awaiting the owner's reply. */
20
+ hasPending(): boolean;
21
+ /** Offer an owner message as a reply; true when it resolved a prompt. */
22
+ offerReply(text: string): boolean;
23
+ /** Deny every in-flight prompt (channel stop / session reset). */
24
+ abortAll(reason?: string): void;
25
+ }
26
+ export declare function parsePermissionReply(text: string): PermissionDecision | null;
27
+ export declare function formatPermissionPrompt(call: PendingToolCall): string;
28
+ export declare function createWhatsAppPermissionController(): WhatsAppPermissionController;
29
+ //# sourceMappingURL=permission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,0BAA0B,EAC/B,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACxB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;IAC9C,yEAAyE;IACzE,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAChE,yDAAyD;IACzD,UAAU,IAAI,OAAO,CAAC;IACtB,yEAAyE;IACzE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAMD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAY5E;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAapE;AAED,wBAAgB,kCAAkC,IAAI,4BAA4B,CAgDjF"}
@@ -0,0 +1,78 @@
1
+ import { createDeferredPermissionResolver, } from '@moxxy/sdk';
2
+ export function parsePermissionReply(text) {
3
+ const v = text.trim().toLowerCase();
4
+ if (v === '1' || v === 'yes' || v === 'y' || v === 'allow') {
5
+ return { mode: 'allow', reason: 'owner approved via WhatsApp' };
6
+ }
7
+ if (v === '2' || v === 'always') {
8
+ return { mode: 'allow_session', reason: 'owner approved for session via WhatsApp' };
9
+ }
10
+ if (v === '3' || v === 'no' || v === 'n' || v === 'deny') {
11
+ return { mode: 'deny', reason: 'owner denied via WhatsApp' };
12
+ }
13
+ return null;
14
+ }
15
+ export function formatPermissionPrompt(call) {
16
+ let input = '';
17
+ try {
18
+ input = JSON.stringify(call.input ?? {});
19
+ }
20
+ catch {
21
+ input = '(unserializable input)';
22
+ }
23
+ if (input.length > 400)
24
+ input = `${input.slice(0, 400)}…`;
25
+ return (`Permission needed: tool *${call.name}*\n` +
26
+ `${input}\n\n` +
27
+ 'Reply: 1 = allow once, 2 = allow for session, 3 = deny');
28
+ }
29
+ export function createWhatsAppPermissionController() {
30
+ // FIFO of prompts awaiting a reply. Tool calls within a turn are sequential,
31
+ // so this is ~1 deep; the queue is defensive against overlap.
32
+ const pending = [];
33
+ let sender = null;
34
+ const resolver = createDeferredPermissionResolver({
35
+ name: 'whatsapp',
36
+ prompt: (call) => new Promise((resolve, reject) => {
37
+ if (!sender) {
38
+ resolve({ mode: 'deny', reason: 'whatsapp channel not connected' });
39
+ return;
40
+ }
41
+ pending.push({ resolve });
42
+ sender(formatPermissionPrompt(call)).catch((err) => {
43
+ const idx = pending.findIndex((p) => p.resolve === resolve);
44
+ if (idx >= 0)
45
+ pending.splice(idx, 1);
46
+ reject(err instanceof Error ? err : new Error(String(err)));
47
+ });
48
+ }),
49
+ });
50
+ return {
51
+ resolver,
52
+ setSender(send) {
53
+ sender = send;
54
+ },
55
+ hasPending() {
56
+ return pending.length > 0;
57
+ },
58
+ offerReply(text) {
59
+ if (pending.length === 0)
60
+ return false;
61
+ const decision = parsePermissionReply(text);
62
+ if (!decision)
63
+ return false;
64
+ const prompt = pending.shift();
65
+ prompt.resolve(decision);
66
+ return true;
67
+ },
68
+ abortAll(reason = 'channel closed') {
69
+ // Resolve OUR pending prompt promises (deny) so the sdk scaffold's outer
70
+ // promises settle, then clear the scaffold's own in-flight set too.
71
+ for (const prompt of pending.splice(0)) {
72
+ prompt.resolve({ mode: 'deny', reason });
73
+ }
74
+ resolver.abortAll(reason);
75
+ },
76
+ };
77
+ }
78
+ //# sourceMappingURL=permission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.js","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { ChannelSubcommandContext } from '@moxxy/sdk';
2
+ import type { VaultStore } from '@moxxy/plugin-vault';
3
+ /**
4
+ * Interactive WhatsApp setup menu (the channel's `interactiveCommand`).
5
+ *
6
+ * The FIRST step — before any menu — is the consent gate: this channel rides an
7
+ * UNOFFICIAL WhatsApp client; the wizard states the ToS/ban risk plainly and
8
+ * requires a typed "yes" before anything else. No acknowledgment, no menu.
9
+ */
10
+ export declare function runWhatsAppWizard(ctx: ChannelSubcommandContext): Promise<number>;
11
+ /** Clear the rotating auth-state dir + the persisted owner identity. */
12
+ export declare function unpairLocal(vault: VaultStore): Promise<boolean>;
13
+ //# sourceMappingURL=setup-wizard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-wizard.d.ts","sourceRoot":"","sources":["../src/setup-wizard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAE3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAwBtD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyCtF;AAED,wEAAwE;AACxE,wBAAsB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAMrE"}
@@ -0,0 +1,127 @@
1
+ import { cancel, intro, isCancel, log, note, outro, select, text } from '@clack/prompts';
2
+ import { moxxyPath } from '@moxxy/sdk/server';
3
+ import { createFileAuthStorage, hasStoredCreds } from './auth-state.js';
4
+ import { ensureConsentInteractive } from './consent-prompt.js';
5
+ import { WHATSAPP_ALLOWED_JIDS_KEY, WHATSAPP_AUTH_DIR, WHATSAPP_OWNER_JID_KEY, parseAllowedJids, } from './keys.js';
6
+ import { runWhatsAppPairFlow } from './pair-flow.js';
7
+ // Tiny zero-dep ANSI helpers so the wizard stays inside the plugin.
8
+ const ANSI = process.stdout.isTTY && !process.env.NO_COLOR;
9
+ const bold = (s) => (ANSI ? `\x1b[1m${s}\x1b[22m` : s);
10
+ const dim = (s) => (ANSI ? `\x1b[2m${s}\x1b[22m` : s);
11
+ /**
12
+ * Interactive WhatsApp setup menu (the channel's `interactiveCommand`).
13
+ *
14
+ * The FIRST step — before any menu — is the consent gate: this channel rides an
15
+ * UNOFFICIAL WhatsApp client; the wizard states the ToS/ban risk plainly and
16
+ * requires a typed "yes" before anything else. No acknowledgment, no menu.
17
+ */
18
+ export async function runWhatsAppWizard(ctx) {
19
+ const vault = ctx.deps.vault;
20
+ intro(bold('moxxy whatsapp setup'));
21
+ if (!(await ensureConsentInteractive(vault))) {
22
+ cancel('cancelled.');
23
+ return 1;
24
+ }
25
+ while (true) {
26
+ const state = await readState(vault);
27
+ printStatus(state);
28
+ const action = await pickAction(state);
29
+ if (action === null) {
30
+ cancel('cancelled.');
31
+ return 0;
32
+ }
33
+ if (action === 'quit') {
34
+ outro(dim('done.'));
35
+ return 0;
36
+ }
37
+ if (action === 'pair') {
38
+ return await runWhatsAppPairFlow(ctx);
39
+ }
40
+ if (action === 'unpair') {
41
+ await unpairLocal(vault);
42
+ log.success('Local credentials cleared. Also remove the device on your phone: WhatsApp -> Settings -> Linked devices.');
43
+ continue;
44
+ }
45
+ if (action === 'allow-list') {
46
+ await editAllowList(vault, state.allowedJids);
47
+ continue;
48
+ }
49
+ if (action === 'start') {
50
+ log.info('Starting the channel. Press Ctrl+C to stop.');
51
+ outro(dim('handing off to the channel...'));
52
+ return ctx.startChannel();
53
+ }
54
+ }
55
+ }
56
+ /** Clear the rotating auth-state dir + the persisted owner identity. */
57
+ export async function unpairLocal(vault) {
58
+ const storage = createFileAuthStorage(moxxyPath(WHATSAPP_AUTH_DIR));
59
+ const had = await hasStoredCreds(storage);
60
+ await storage.clear();
61
+ await vault.delete(WHATSAPP_OWNER_JID_KEY).catch(() => undefined);
62
+ return had;
63
+ }
64
+ async function readState(vault) {
65
+ const storage = createFileAuthStorage(moxxyPath(WHATSAPP_AUTH_DIR));
66
+ return {
67
+ linked: await hasStoredCreds(storage),
68
+ ownerJid: await vault.get(WHATSAPP_OWNER_JID_KEY),
69
+ allowedJids: parseAllowedJids(await vault.get(WHATSAPP_ALLOWED_JIDS_KEY)),
70
+ };
71
+ }
72
+ function printStatus(state) {
73
+ const lines = [];
74
+ lines.push(`Linked ${state.linked ? bold(state.ownerJid ?? 'yes') : dim('no')}`);
75
+ lines.push(`Allow-list ${state.allowedJids.length > 0
76
+ ? bold(state.allowedJids.join(', '))
77
+ : dim('only your own Note-to-Self chat')}`);
78
+ note(lines.join('\n'), 'status');
79
+ }
80
+ async function pickAction(state) {
81
+ const options = [];
82
+ if (state.linked) {
83
+ options.push({
84
+ value: 'start',
85
+ label: 'Start the channel',
86
+ hint: 'runs forever - Ctrl+C to stop',
87
+ });
88
+ options.push({
89
+ value: 'unpair',
90
+ label: 'Unpair (forget local credentials)',
91
+ hint: 'also remove the device under Linked devices on the phone',
92
+ });
93
+ }
94
+ else {
95
+ options.push({
96
+ value: 'pair',
97
+ label: 'Link a WhatsApp account (scan QR)',
98
+ hint: 'WhatsApp -> Settings -> Linked devices -> Link a device',
99
+ });
100
+ }
101
+ options.push({
102
+ value: 'allow-list',
103
+ label: 'Edit the JID allow-list',
104
+ hint: 'who besides your own Note-to-Self chat may talk to the agent',
105
+ });
106
+ options.push({ value: 'quit', label: 'Quit' });
107
+ const choice = await select({ message: 'What do you want to do?', options });
108
+ if (isCancel(choice))
109
+ return null;
110
+ return choice;
111
+ }
112
+ async function editAllowList(vault, current) {
113
+ note('Comma-separated JIDs, e.g. 15551234567@s.whatsapp.net (a group JID ends in\n' +
114
+ '@g.us — allow-listing a group trusts EVERYONE in it). Empty input clears the\n' +
115
+ 'list; your own Note-to-Self chat is always allowed.', 'allow-list');
116
+ const answer = await text({
117
+ message: 'Allowed JIDs',
118
+ initialValue: current.join(', '),
119
+ placeholder: '15551234567@s.whatsapp.net',
120
+ });
121
+ if (isCancel(answer))
122
+ return;
123
+ const parsed = parseAllowedJids(String(answer));
124
+ await vault.set(WHATSAPP_ALLOWED_JIDS_KEY, JSON.stringify(parsed), ['whatsapp']);
125
+ log.success(parsed.length > 0 ? `Allow-list saved (${parsed.length} JIDs).` : 'Allow-list cleared.');
126
+ }
127
+ //# sourceMappingURL=setup-wizard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-wizard.js","sourceRoot":"","sources":["../src/setup-wizard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ import type { WhatsAppAuthStorage } from './auth-state.js';
2
+ /**
3
+ * The narrow transport contract the channel is written against — the slice of a
4
+ * Baileys socket it actually touches, adapted in `baileys-socket.ts`. Tests
5
+ * inject a fake implementing THIS, so no test ever opens a WhatsApp connection
6
+ * (and the heavyweight Baileys import stays out of unit tests entirely).
7
+ */
8
+ export interface WaMessageKey {
9
+ readonly remoteJid?: string | null;
10
+ readonly fromMe?: boolean | null;
11
+ readonly id?: string | null;
12
+ readonly participant?: string | null;
13
+ }
14
+ /** The consumed subset of a Baileys `WAMessage` (validated in message-gate). */
15
+ export interface WaInboundMessage {
16
+ readonly key?: WaMessageKey | null;
17
+ readonly message?: Record<string, unknown> | null;
18
+ }
19
+ export interface WaMessagesUpsert {
20
+ /** 'notify' = fresh inbound; 'append'/'history' = sync artifacts (dropped). */
21
+ readonly type: string;
22
+ readonly messages: ReadonlyArray<WaInboundMessage>;
23
+ }
24
+ export interface WaConnectionUpdate {
25
+ readonly connection?: 'close' | 'connecting' | 'open' | undefined;
26
+ /** Fresh QR pairing payload — rotates every ~20-60s while unlinked. */
27
+ readonly qr?: string | undefined;
28
+ readonly lastDisconnect?: {
29
+ readonly error?: unknown;
30
+ } | undefined;
31
+ }
32
+ export interface WaSentMessage {
33
+ readonly key: WaMessageKey;
34
+ }
35
+ export interface WhatsAppSocket {
36
+ /** The linked account's raw JID once the connection is open; null before. */
37
+ userJid(): string | null;
38
+ onConnectionUpdate(cb: (update: WaConnectionUpdate) => void): void;
39
+ onMessages(cb: (upsert: WaMessagesUpsert) => void): void;
40
+ sendText(jid: string, text: string): Promise<WaSentMessage | null>;
41
+ /** Edit a previously sent message in place (WhatsApp MESSAGE_EDIT). */
42
+ editText(jid: string, key: WaMessageKey, text: string): Promise<void>;
43
+ /** Download a media message's bytes (voice notes). */
44
+ downloadMedia(message: WaInboundMessage): Promise<Uint8Array>;
45
+ /** Tear the connection down WITHOUT logging out (creds stay valid). */
46
+ end(): void;
47
+ }
48
+ export interface WhatsAppSocketLogger {
49
+ debug?(msg: string, meta?: Record<string, unknown>): void;
50
+ info?(msg: string, meta?: Record<string, unknown>): void;
51
+ warn?(msg: string, meta?: Record<string, unknown>): void;
52
+ }
53
+ export interface WhatsAppSocketFactoryOptions {
54
+ readonly storage: WhatsAppAuthStorage;
55
+ readonly logger?: WhatsAppSocketLogger;
56
+ }
57
+ /** Opens one connection attempt; the channel owns the reconnect loop. */
58
+ export type WhatsAppSocketFactory = (opts: WhatsAppSocketFactoryOptions) => Promise<WhatsAppSocket>;
59
+ /**
60
+ * Extract the WhatsApp disconnect status code from a close error (Baileys
61
+ * throws Boom errors: `err.output.statusCode`). Pure so the reconnect policy is
62
+ * unit-testable. Returns null when no code can be found (treated as retriable).
63
+ */
64
+ export declare function disconnectStatusCode(err: unknown): number | null;
65
+ /** WhatsApp disconnect reasons the channel branches on (Baileys' DisconnectReason). */
66
+ export declare const WA_DISCONNECT: {
67
+ readonly loggedOut: 401;
68
+ readonly forbidden: 403;
69
+ readonly connectionReplaced: 440;
70
+ readonly restartRequired: 515;
71
+ };
72
+ //# sourceMappingURL=socket.d.ts.map