@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 @@
1
+ {"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"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"}
package/dist/socket.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Extract the WhatsApp disconnect status code from a close error (Baileys
3
+ * throws Boom errors: `err.output.statusCode`). Pure so the reconnect policy is
4
+ * unit-testable. Returns null when no code can be found (treated as retriable).
5
+ */
6
+ export function disconnectStatusCode(err) {
7
+ if (typeof err !== 'object' || err === null)
8
+ return null;
9
+ const output = err.output;
10
+ if (typeof output !== 'object' || output === null)
11
+ return null;
12
+ const code = output.statusCode;
13
+ return typeof code === 'number' && Number.isFinite(code) ? code : null;
14
+ }
15
+ /** WhatsApp disconnect reasons the channel branches on (Baileys' DisconnectReason). */
16
+ export const WA_DISCONNECT = {
17
+ loggedOut: 401,
18
+ forbidden: 403,
19
+ connectionReplaced: 440,
20
+ restartRequired: 515,
21
+ };
22
+ //# sourceMappingURL=socket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket.js","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AAqEA;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAY;IAC/C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACzD,MAAM,MAAM,GAAI,GAA4B,CAAC,MAAM,CAAC;IACpD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/D,MAAM,IAAI,GAAI,MAAmC,CAAC,UAAU,CAAC;IAC7D,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACzE,CAAC;AAED,uFAAuF;AACvF,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,SAAS,EAAE,GAAG;IACd,SAAS,EAAE,GAAG;IACd,kBAAkB,EAAE,GAAG;IACvB,eAAe,EAAE,GAAG;CACZ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "@moxxy/plugin-channel-whatsapp",
3
+ "version": "0.27.0",
4
+ "description": "WhatsApp channel for moxxy via Baileys (UNOFFICIAL WhatsApp Web protocol — violates WhatsApp's Terms of Service; the linked number can be banned, use a secondary number). QR device-link pairing, JID allow-list, voice-note transcription, dedicated isolated runner.",
5
+ "keywords": [
6
+ "moxxy",
7
+ "agent",
8
+ "channel",
9
+ "whatsapp",
10
+ "baileys",
11
+ "bot"
12
+ ],
13
+ "homepage": "https://moxxy.ai",
14
+ "bugs": {
15
+ "url": "https://github.com/moxxy-ai/moxxy/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/moxxy-ai/moxxy.git",
20
+ "directory": "packages/plugin-channel-whatsapp"
21
+ },
22
+ "author": "Michal Makowski <michal.makowski97@gmail.com>",
23
+ "license": "MIT",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src"
39
+ ],
40
+ "moxxy": {
41
+ "plugin": {
42
+ "entry": "./dist/index.js",
43
+ "kind": "cli"
44
+ },
45
+ "requirements": [
46
+ {
47
+ "kind": "plugin",
48
+ "name": "@moxxy/plugin-vault",
49
+ "hint": "whatsapp resolves the vault from the service registry for its consent receipt + owner JID + allow-list; @moxxy/plugin-vault must load first"
50
+ }
51
+ ],
52
+ "setup": {
53
+ "title": "WhatsApp channel (UNOFFICIAL API — read this first)",
54
+ "description": "This channel uses Baileys, an UNOFFICIAL WhatsApp Web client. Automating a personal account violates WhatsApp's Terms of Service and CAN GET THE PHONE NUMBER PERMANENTLY BANNED. Strongly consider a secondary number. Pairing itself happens later via QR (moxxy whatsapp setup).",
55
+ "fields": [
56
+ {
57
+ "key": "tosAcknowledged",
58
+ "label": "Type 'yes' to acknowledge the ToS/ban risk (anything else keeps the channel disabled)",
59
+ "kind": "secret",
60
+ "vaultKey": "whatsapp_tos_acknowledged",
61
+ "required": true
62
+ },
63
+ {
64
+ "key": "allowedJids",
65
+ "label": "Extra allowed WhatsApp JIDs (comma-separated, e.g. 15551234567@s.whatsapp.net) — your own Note-to-Self chat is always allowed",
66
+ "kind": "string",
67
+ "required": false,
68
+ "placeholder": "15551234567@s.whatsapp.net"
69
+ }
70
+ ]
71
+ }
72
+ },
73
+ "dependencies": {
74
+ "@clack/prompts": "^1.4.0",
75
+ "@whiskeysockets/baileys": "^6.7.21",
76
+ "qrcode": "^1.5.4",
77
+ "zod": "^3.24.0",
78
+ "@moxxy/core": "0.27.0",
79
+ "@moxxy/channel-kit": "0.27.0",
80
+ "@moxxy/plugin-vault": "0.27.0",
81
+ "@moxxy/sdk": "0.27.0"
82
+ },
83
+ "devDependencies": {
84
+ "@types/node": "^22.10.0",
85
+ "@types/qrcode": "^1.5.5",
86
+ "typescript": "^5.7.3",
87
+ "vitest": "^2.1.8",
88
+ "@moxxy/mode-default": "0.27.0",
89
+ "@moxxy/testing": "0.0.45",
90
+ "@moxxy/tsconfig": "0.0.0",
91
+ "@moxxy/vitest-preset": "0.0.0"
92
+ },
93
+ "scripts": {
94
+ "build": "tsc -p tsconfig.json",
95
+ "typecheck": "tsc -p tsconfig.json --noEmit",
96
+ "test": "vitest run",
97
+ "clean": "rm -rf dist .turbo"
98
+ }
99
+ }
@@ -0,0 +1,103 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import {
6
+ createFileAuthStorage,
7
+ createWhatsAppAuthState,
8
+ hasStoredCreds,
9
+ sanitizeAuthKey,
10
+ type BaileysAuthBridge,
11
+ type WhatsAppAuthStorage,
12
+ } from './auth-state.js';
13
+
14
+ // A trivial bridge: identity JSON (no Buffer categories), fresh creds counter.
15
+ function fakeBridge(): BaileysAuthBridge {
16
+ let n = 0;
17
+ return {
18
+ initAuthCreds: () => ({ me: `creds-${n++}` }),
19
+ BufferJSON: { reviver: (_k, v) => v, replacer: (_k, v) => v },
20
+ };
21
+ }
22
+
23
+ describe('sanitizeAuthKey', () => {
24
+ it('flattens signal key ids into safe filenames', () => {
25
+ expect(sanitizeAuthKey('pre-key-42')).toBe('pre-key-42');
26
+ expect(sanitizeAuthKey('session-15551234567:1.0')).toBe('session-15551234567_1.0');
27
+ expect(sanitizeAuthKey('../escape')).toBe('__escape');
28
+ });
29
+ });
30
+
31
+ describe('createWhatsAppAuthState (over an in-memory storage)', () => {
32
+ function memStorage(): WhatsAppAuthStorage & { map: Map<string, string> } {
33
+ const map = new Map<string, string>();
34
+ return {
35
+ map,
36
+ async read(k) {
37
+ return map.get(k) ?? null;
38
+ },
39
+ async write(k, v) {
40
+ map.set(k, v);
41
+ },
42
+ async delete(k) {
43
+ map.delete(k);
44
+ },
45
+ async clear() {
46
+ map.clear();
47
+ },
48
+ };
49
+ }
50
+
51
+ it('creates fresh creds when none stored, and persists them via saveCreds', async () => {
52
+ const storage = memStorage();
53
+ const { state, saveCreds } = await createWhatsAppAuthState(storage, fakeBridge());
54
+ expect(state.creds).toEqual({ me: 'creds-0' });
55
+ await saveCreds();
56
+ expect(storage.map.has('creds')).toBe(true);
57
+ expect(await hasStoredCreds(storage)).toBe(true);
58
+ });
59
+
60
+ it('round-trips key set/get and honors null-delete', async () => {
61
+ const storage = memStorage();
62
+ const { state } = await createWhatsAppAuthState(storage, fakeBridge());
63
+ await state.keys.set({ 'pre-key': { '1': { k: 'v' }, '2': { k: 'w' } } });
64
+ expect(await state.keys.get('pre-key', ['1', '2'])).toEqual({ '1': { k: 'v' }, '2': { k: 'w' } });
65
+
66
+ await state.keys.set({ 'pre-key': { '1': null } });
67
+ expect(await state.keys.get('pre-key', ['1', '2'])).toEqual({ '2': { k: 'w' } });
68
+ });
69
+
70
+ it('reuses stored creds on the next init', async () => {
71
+ const storage = memStorage();
72
+ const bridge = fakeBridge();
73
+ const first = await createWhatsAppAuthState(storage, bridge);
74
+ await first.saveCreds();
75
+ const second = await createWhatsAppAuthState(storage, bridge);
76
+ expect(second.state.creds).toEqual({ me: 'creds-0' });
77
+ });
78
+ });
79
+
80
+ describe('createFileAuthStorage', () => {
81
+ let dir: string;
82
+ beforeEach(async () => {
83
+ dir = await fs.mkdtemp(path.join(os.tmpdir(), 'wa-auth-'));
84
+ });
85
+ afterEach(async () => {
86
+ await fs.rm(dir, { recursive: true, force: true });
87
+ });
88
+
89
+ it('writes 0600 files and clears the whole dir', async () => {
90
+ const storage = createFileAuthStorage(path.join(dir, 'auth'));
91
+ await storage.write('creds', '{"x":1}');
92
+ expect(await storage.read('creds')).toBe('{"x":1}');
93
+ expect(await hasStoredCreds(storage)).toBe(true);
94
+
95
+ const file = path.join(dir, 'auth', 'creds.json');
96
+ const mode = (await fs.stat(file)).mode & 0o777;
97
+ expect(mode).toBe(0o600);
98
+
99
+ await storage.clear();
100
+ expect(await storage.read('creds')).toBeNull();
101
+ expect(await hasStoredCreds(storage)).toBe(false);
102
+ });
103
+ });
@@ -0,0 +1,161 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ /**
5
+ * Baileys auth-state persistence behind a swappable storage adapter.
6
+ *
7
+ * Baileys' multi-device auth is a ROTATING key store (signal sessions, pre-keys,
8
+ * sender keys, app-state sync keys) plus a creds record — dozens of small files
9
+ * that churn constantly. That churn is why the moxxy VAULT is the wrong home for
10
+ * it (the vault is an encrypted, tagged, human-curated secret store, not a
11
+ * high-write key-value backend); instead the state lives in a dedicated
12
+ * directory under the moxxy home with tight modes (dir 0700, files 0600).
13
+ * TRADE-OFF (recorded in TECH_DEBT + the PR): unlike vault entries, these files
14
+ * are NOT encrypted at rest — anyone with file access to the moxxy home can
15
+ * hijack the linked WhatsApp session. The {@link WhatsAppAuthStorage} interface
16
+ * exists precisely so an encrypted backend can be swapped in later without
17
+ * touching the channel.
18
+ */
19
+ export interface WhatsAppAuthStorage {
20
+ read(key: string): Promise<string | null>;
21
+ write(key: string, value: string): Promise<void>;
22
+ delete(key: string): Promise<void>;
23
+ /** Wipe the whole store (logout / unpair). */
24
+ clear(): Promise<void>;
25
+ }
26
+
27
+ /** Signal key ids contain `/` and `:`; map every key to one safe flat filename. */
28
+ export function sanitizeAuthKey(key: string): string {
29
+ return key.replace(/[^a-zA-Z0-9._-]/g, '_').replace(/^\.+/, '_');
30
+ }
31
+
32
+ /**
33
+ * The default storage backend: one JSON file per key under `dir`, created 0700
34
+ * with 0600 files so the signal identity is at least owner-only on disk.
35
+ */
36
+ export function createFileAuthStorage(dir: string): WhatsAppAuthStorage {
37
+ const fileFor = (key: string): string => path.join(dir, `${sanitizeAuthKey(key)}.json`);
38
+ const ensureDir = async (): Promise<void> => {
39
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
40
+ };
41
+ return {
42
+ async read(key) {
43
+ try {
44
+ return await fs.readFile(fileFor(key), 'utf8');
45
+ } catch {
46
+ return null;
47
+ }
48
+ },
49
+ async write(key, value) {
50
+ await ensureDir();
51
+ const target = fileFor(key);
52
+ // Write-then-rename so a crash mid-write can't truncate the creds record;
53
+ // chmod explicitly because writeFile's mode option is masked by umask.
54
+ const tmp = `${target}.tmp`;
55
+ await fs.writeFile(tmp, value, { mode: 0o600 });
56
+ await fs.chmod(tmp, 0o600);
57
+ await fs.rename(tmp, target);
58
+ },
59
+ async delete(key) {
60
+ await fs.rm(fileFor(key), { force: true });
61
+ },
62
+ async clear() {
63
+ await fs.rm(dir, { recursive: true, force: true });
64
+ },
65
+ };
66
+ }
67
+
68
+ /** The creds record's storage key (every other key is `<type>-<id>`). */
69
+ const CREDS_KEY = 'creds';
70
+
71
+ /** True when a linked-device credential record exists (i.e. paired before). */
72
+ export async function hasStoredCreds(storage: WhatsAppAuthStorage): Promise<boolean> {
73
+ return (await storage.read(CREDS_KEY)) != null;
74
+ }
75
+
76
+ /**
77
+ * The tiny slice of Baileys this module needs — injected so unit tests never
78
+ * import the real package, and so the production wiring (`baileys-socket.ts`)
79
+ * can hand in the lazily-imported module.
80
+ */
81
+ export interface BaileysAuthBridge {
82
+ initAuthCreds(): Record<string, unknown>;
83
+ readonly BufferJSON: {
84
+ readonly reviver: (key: string, value: unknown) => unknown;
85
+ readonly replacer: (key: string, value: unknown) => unknown;
86
+ };
87
+ /** `proto.Message.AppStateSyncKeyData.fromObject` — revives that one typed key
88
+ * category the way Baileys' own `useMultiFileAuthState` does. Optional so a
89
+ * fake bridge in tests can skip it. */
90
+ readonly reviveAppStateSyncKey?: (value: unknown) => unknown;
91
+ }
92
+
93
+ export interface WhatsAppAuthState {
94
+ /** Shaped exactly like Baileys' `AuthenticationState` (creds + keys store). */
95
+ readonly state: {
96
+ readonly creds: Record<string, unknown>;
97
+ readonly keys: {
98
+ get(type: string, ids: string[]): Promise<Record<string, unknown>>;
99
+ set(data: Record<string, Record<string, unknown | null>>): Promise<void>;
100
+ };
101
+ };
102
+ /** Persist the creds record; wire to the socket's `creds.update` event. */
103
+ saveCreds(): Promise<void>;
104
+ }
105
+
106
+ /**
107
+ * `useMultiFileAuthState` equivalent over a {@link WhatsAppAuthStorage} — the
108
+ * same creds + `<type>-<id>` key layout, serialized with Baileys' BufferJSON
109
+ * (signal keys are raw byte arrays), but with the storage backend swappable.
110
+ */
111
+ export async function createWhatsAppAuthState(
112
+ storage: WhatsAppAuthStorage,
113
+ bridge: BaileysAuthBridge,
114
+ ): Promise<WhatsAppAuthState> {
115
+ const readJson = async (key: string): Promise<unknown> => {
116
+ const raw = await storage.read(key);
117
+ if (raw == null) return null;
118
+ try {
119
+ return JSON.parse(raw, bridge.BufferJSON.reviver);
120
+ } catch {
121
+ return null;
122
+ }
123
+ };
124
+ const writeJson = (key: string, value: unknown): Promise<void> =>
125
+ storage.write(key, JSON.stringify(value, bridge.BufferJSON.replacer));
126
+
127
+ const creds =
128
+ ((await readJson(CREDS_KEY)) as Record<string, unknown> | null) ?? bridge.initAuthCreds();
129
+
130
+ return {
131
+ state: {
132
+ creds,
133
+ keys: {
134
+ async get(type, ids) {
135
+ const data: Record<string, unknown> = {};
136
+ await Promise.all(
137
+ ids.map(async (id) => {
138
+ let value = await readJson(`${type}-${id}`);
139
+ if (type === 'app-state-sync-key' && value != null && bridge.reviveAppStateSyncKey) {
140
+ value = bridge.reviveAppStateSyncKey(value);
141
+ }
142
+ if (value != null) data[id] = value;
143
+ }),
144
+ );
145
+ return data;
146
+ },
147
+ async set(data) {
148
+ const tasks: Promise<void>[] = [];
149
+ for (const [category, entries] of Object.entries(data)) {
150
+ for (const [id, value] of Object.entries(entries)) {
151
+ const key = `${category}-${id}`;
152
+ tasks.push(value == null ? storage.delete(key) : writeJson(key, value));
153
+ }
154
+ }
155
+ await Promise.all(tasks);
156
+ },
157
+ },
158
+ },
159
+ saveCreds: () => writeJson(CREDS_KEY, creds),
160
+ };
161
+ }
@@ -0,0 +1,154 @@
1
+ import { createWhatsAppAuthState, type BaileysAuthBridge } from './auth-state.js';
2
+ import type {
3
+ WaConnectionUpdate,
4
+ WaMessageKey,
5
+ WaMessagesUpsert,
6
+ WaSentMessage,
7
+ WhatsAppSocket,
8
+ WhatsAppSocketFactory,
9
+ WhatsAppSocketLogger,
10
+ } from './socket.js';
11
+
12
+ /**
13
+ * The production {@link WhatsAppSocketFactory}: lazily imports Baileys (a heavy
14
+ * dependency tree — protobufjs, libsignal — that must NOT load at plugin
15
+ * discovery time, only when the channel actually starts; same lesson as the
16
+ * desktop-host eager-import boot crash), builds the swappable auth state, and
17
+ * adapts the socket down to the narrow contract the channel uses.
18
+ */
19
+ export const openBaileysSocket: WhatsAppSocketFactory = async ({ storage, logger }) => {
20
+ const baileys = (await import('@whiskeysockets/baileys')) as unknown as BaileysModule;
21
+ const makeWASocket = baileys.makeWASocket ?? baileys.default?.makeWASocket ?? baileys.default;
22
+ if (typeof makeWASocket !== 'function') {
23
+ throw new Error('@whiskeysockets/baileys: could not resolve makeWASocket export');
24
+ }
25
+ const proto = baileys.proto ?? baileys.default?.proto;
26
+ const bridge: BaileysAuthBridge = {
27
+ initAuthCreds: baileys.initAuthCreds,
28
+ BufferJSON: baileys.BufferJSON,
29
+ ...(proto?.Message?.AppStateSyncKeyData
30
+ ? {
31
+ reviveAppStateSyncKey: (value: unknown) =>
32
+ proto.Message!.AppStateSyncKeyData!.fromObject(value as Record<string, unknown>),
33
+ }
34
+ : {}),
35
+ };
36
+ const { state, saveCreds } = await createWhatsAppAuthState(storage, bridge);
37
+
38
+ const sock = makeWASocket({
39
+ auth: state as never,
40
+ // The channel renders the QR itself (terminal QR / desktop panel status file).
41
+ printQRInTerminal: false,
42
+ logger: silentBaileysLogger() as never,
43
+ // Don't mark the linked device "online": keeps phone notifications working
44
+ // and keeps this client's footprint minimal (it is a bot, not a presence).
45
+ markOnlineOnConnect: false,
46
+ // No history backfill — the channel only reacts to live inbound messages;
47
+ // syncing message history would be both wasteful and a privacy liability.
48
+ syncFullHistory: false,
49
+ shouldSyncHistoryMessage: () => false,
50
+ generateHighQualityLinkPreview: false,
51
+ browser: ['moxxy', 'Desktop', '1.0.0'],
52
+ });
53
+ sock.ev.on('creds.update', () => {
54
+ void saveCreds().catch((err: unknown) => {
55
+ logger?.warn?.('whatsapp: failed to persist creds update', { err: String(err) });
56
+ });
57
+ });
58
+
59
+ return adaptBaileysSocket(sock, baileys, logger);
60
+ };
61
+
62
+ function adaptBaileysSocket(
63
+ sock: BaileysSock,
64
+ baileys: BaileysModule,
65
+ logger?: WhatsAppSocketLogger,
66
+ ): WhatsAppSocket {
67
+ const downloadMediaMessage =
68
+ baileys.downloadMediaMessage ?? baileys.default?.downloadMediaMessage;
69
+ return {
70
+ userJid: () => sock.user?.id ?? null,
71
+ onConnectionUpdate: (cb) =>
72
+ sock.ev.on('connection.update', (u: unknown) => cb(u as WaConnectionUpdate)),
73
+ onMessages: (cb) =>
74
+ sock.ev.on('messages.upsert', (u: unknown) => cb(u as WaMessagesUpsert)),
75
+ sendText: async (jid, text) => {
76
+ const sent = (await sock.sendMessage(jid, { text })) as { key?: WaMessageKey } | undefined;
77
+ return sent?.key ? ({ key: sent.key } satisfies WaSentMessage) : null;
78
+ },
79
+ editText: async (jid, key, text) => {
80
+ await sock.sendMessage(jid, { text, edit: key });
81
+ },
82
+ downloadMedia: async (message) => {
83
+ if (typeof downloadMediaMessage !== 'function') {
84
+ throw new Error('@whiskeysockets/baileys: downloadMediaMessage export missing');
85
+ }
86
+ const buf = (await downloadMediaMessage(
87
+ message as never,
88
+ 'buffer',
89
+ {},
90
+ { logger: silentBaileysLogger() as never, reuploadRequest: sock.updateMediaMessage },
91
+ )) as Buffer;
92
+ return new Uint8Array(buf);
93
+ },
94
+ end: () => {
95
+ try {
96
+ sock.end(undefined);
97
+ } catch (err) {
98
+ logger?.debug?.('whatsapp: socket end threw', { err: String(err) });
99
+ }
100
+ },
101
+ };
102
+ }
103
+
104
+ /** Minimal Baileys ILogger that swallows everything (moxxy has its own logger). */
105
+ function silentBaileysLogger(): Record<string, unknown> {
106
+ const noop = (): void => undefined;
107
+ const self: Record<string, unknown> = {
108
+ level: 'silent',
109
+ trace: noop,
110
+ debug: noop,
111
+ info: noop,
112
+ warn: noop,
113
+ error: noop,
114
+ fatal: noop,
115
+ };
116
+ self.child = () => self;
117
+ return self;
118
+ }
119
+
120
+ // ---- Loosely-typed view of the dynamically imported module (the real types
121
+ // stay out so this file compiles without deep Baileys type coupling; the
122
+ // adapter boundary above is the single place the erasure happens). ----
123
+
124
+ interface BaileysSock {
125
+ readonly user?: { id: string } | undefined;
126
+ readonly ev: { on(event: string, cb: (arg: never) => void): void };
127
+ sendMessage(jid: string, content: Record<string, unknown>): Promise<unknown>;
128
+ updateMediaMessage: unknown;
129
+ end(err: Error | undefined): void;
130
+ }
131
+
132
+ interface BaileysModule {
133
+ makeWASocket?: (config: Record<string, unknown>) => BaileysSock;
134
+ initAuthCreds: () => Record<string, unknown>;
135
+ BufferJSON: BaileysAuthBridge['BufferJSON'];
136
+ downloadMediaMessage?: (
137
+ message: never,
138
+ type: 'buffer',
139
+ options: Record<string, unknown>,
140
+ ctx: Record<string, unknown>,
141
+ ) => Promise<Buffer>;
142
+ proto?: BaileysProto;
143
+ default?: {
144
+ makeWASocket?: (config: Record<string, unknown>) => BaileysSock;
145
+ downloadMediaMessage?: BaileysModule['downloadMediaMessage'];
146
+ proto?: BaileysProto;
147
+ } & ((config: Record<string, unknown>) => BaileysSock);
148
+ }
149
+
150
+ interface BaileysProto {
151
+ Message?: {
152
+ AppStateSyncKeyData?: { fromObject(value: Record<string, unknown>): unknown };
153
+ };
154
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ splitWhatsAppText,
4
+ WHATSAPP_MAX_MESSAGE_CHARS,
5
+ } from './turn-runner.js';
6
+
7
+ describe('splitWhatsAppText', () => {
8
+ it('returns a single chunk when under the cap', () => {
9
+ expect(splitWhatsAppText('hello')).toEqual(['hello']);
10
+ });
11
+
12
+ it('splits on newline boundaries when possible', () => {
13
+ const a = 'a'.repeat(30);
14
+ const b = 'b'.repeat(30);
15
+ const chunks = splitWhatsAppText(`${a}\n${b}`, 40);
16
+ expect(chunks).toEqual([a, b]);
17
+ });
18
+
19
+ it('never emits a chunk longer than maxLen', () => {
20
+ const text = 'x'.repeat(WHATSAPP_MAX_MESSAGE_CHARS * 2 + 123);
21
+ const chunks = splitWhatsAppText(text);
22
+ expect(chunks.length).toBeGreaterThan(1);
23
+ for (const c of chunks) expect(c.length).toBeLessThanOrEqual(WHATSAPP_MAX_MESSAGE_CHARS);
24
+ expect(chunks.join('')).toBe(text);
25
+ });
26
+
27
+ it('hard-splits a single word with no separators', () => {
28
+ const chunks = splitWhatsAppText('y'.repeat(90), 40);
29
+ expect(chunks.map((c) => c.length)).toEqual([40, 40, 10]);
30
+ });
31
+ });