@kangwifi-pro/waliwa 1.0.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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +600 -0
  3. package/dist/advanced/features.d.ts +211 -0
  4. package/dist/advanced/features.d.ts.map +1 -0
  5. package/dist/advanced/features.js +671 -0
  6. package/dist/advanced/features.js.map +1 -0
  7. package/dist/auth/auth-state.d.ts +89 -0
  8. package/dist/auth/auth-state.d.ts.map +1 -0
  9. package/dist/auth/auth-state.js +330 -0
  10. package/dist/auth/auth-state.js.map +1 -0
  11. package/dist/auth/pairing-code.d.ts +78 -0
  12. package/dist/auth/pairing-code.d.ts.map +1 -0
  13. package/dist/auth/pairing-code.js +191 -0
  14. package/dist/auth/pairing-code.js.map +1 -0
  15. package/dist/auth/qr-code.d.ts +60 -0
  16. package/dist/auth/qr-code.d.ts.map +1 -0
  17. package/dist/auth/qr-code.js +151 -0
  18. package/dist/auth/qr-code.js.map +1 -0
  19. package/dist/calls/handler.d.ts +61 -0
  20. package/dist/calls/handler.d.ts.map +1 -0
  21. package/dist/calls/handler.js +117 -0
  22. package/dist/calls/handler.js.map +1 -0
  23. package/dist/core/binary.d.ts +74 -0
  24. package/dist/core/binary.d.ts.map +1 -0
  25. package/dist/core/binary.js +448 -0
  26. package/dist/core/binary.js.map +1 -0
  27. package/dist/core/crypto.d.ts +60 -0
  28. package/dist/core/crypto.d.ts.map +1 -0
  29. package/dist/core/crypto.js +170 -0
  30. package/dist/core/crypto.js.map +1 -0
  31. package/dist/core/noise.d.ts +106 -0
  32. package/dist/core/noise.d.ts.map +1 -0
  33. package/dist/core/noise.js +307 -0
  34. package/dist/core/noise.js.map +1 -0
  35. package/dist/events/emitter.d.ts +44 -0
  36. package/dist/events/emitter.d.ts.map +1 -0
  37. package/dist/events/emitter.js +79 -0
  38. package/dist/events/emitter.js.map +1 -0
  39. package/dist/features/index.d.ts +342 -0
  40. package/dist/features/index.d.ts.map +1 -0
  41. package/dist/features/index.js +755 -0
  42. package/dist/features/index.js.map +1 -0
  43. package/dist/fixes/index.d.ts +333 -0
  44. package/dist/fixes/index.d.ts.map +1 -0
  45. package/dist/fixes/index.js +762 -0
  46. package/dist/fixes/index.js.map +1 -0
  47. package/dist/groups/management.d.ts +86 -0
  48. package/dist/groups/management.d.ts.map +1 -0
  49. package/dist/groups/management.js +443 -0
  50. package/dist/groups/management.js.map +1 -0
  51. package/dist/index.d.ts +65 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +236 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/messages/media.d.ts +93 -0
  56. package/dist/messages/media.d.ts.map +1 -0
  57. package/dist/messages/media.js +252 -0
  58. package/dist/messages/media.js.map +1 -0
  59. package/dist/messages/message-encoder.d.ts +70 -0
  60. package/dist/messages/message-encoder.d.ts.map +1 -0
  61. package/dist/messages/message-encoder.js +453 -0
  62. package/dist/messages/message-encoder.js.map +1 -0
  63. package/dist/messages/send.d.ts +101 -0
  64. package/dist/messages/send.d.ts.map +1 -0
  65. package/dist/messages/send.js +409 -0
  66. package/dist/messages/send.js.map +1 -0
  67. package/dist/recovery/index.d.ts +125 -0
  68. package/dist/recovery/index.d.ts.map +1 -0
  69. package/dist/recovery/index.js +584 -0
  70. package/dist/recovery/index.js.map +1 -0
  71. package/dist/skdm/index.d.ts +220 -0
  72. package/dist/skdm/index.d.ts.map +1 -0
  73. package/dist/skdm/index.js +600 -0
  74. package/dist/skdm/index.js.map +1 -0
  75. package/dist/socket/hybrid.d.ts +118 -0
  76. package/dist/socket/hybrid.d.ts.map +1 -0
  77. package/dist/socket/hybrid.js +352 -0
  78. package/dist/socket/hybrid.js.map +1 -0
  79. package/dist/socket/wa-socket.d.ts +300 -0
  80. package/dist/socket/wa-socket.d.ts.map +1 -0
  81. package/dist/socket/wa-socket.js +1094 -0
  82. package/dist/socket/wa-socket.js.map +1 -0
  83. package/dist/socket/ws-socket.d.ts +96 -0
  84. package/dist/socket/ws-socket.d.ts.map +1 -0
  85. package/dist/socket/ws-socket.js +302 -0
  86. package/dist/socket/ws-socket.js.map +1 -0
  87. package/dist/types/index.d.ts +444 -0
  88. package/dist/types/index.d.ts.map +1 -0
  89. package/dist/types/index.js +7 -0
  90. package/dist/types/index.js.map +1 -0
  91. package/dist/utils/jid.d.ts +46 -0
  92. package/dist/utils/jid.d.ts.map +1 -0
  93. package/dist/utils/jid.js +152 -0
  94. package/dist/utils/jid.js.map +1 -0
  95. package/dist/utils/logger.d.ts +45 -0
  96. package/dist/utils/logger.d.ts.map +1 -0
  97. package/dist/utils/logger.js +79 -0
  98. package/dist/utils/logger.js.map +1 -0
  99. package/dist/utils/retry.d.ts +43 -0
  100. package/dist/utils/retry.d.ts.map +1 -0
  101. package/dist/utils/retry.js +175 -0
  102. package/dist/utils/retry.js.map +1 -0
  103. package/docs/API.md +745 -0
  104. package/docs/ARCHITECTURE.md +307 -0
  105. package/docs/BAILEYS_FIXES.md +360 -0
  106. package/docs/FEATURES.md +532 -0
  107. package/docs/PROTOCOL.md +489 -0
  108. package/docs/RECOVERY.md +409 -0
  109. package/docs/SKDM.md +233 -0
  110. package/examples/ai-bot/index.ts +479 -0
  111. package/examples/ai-bot/package.json +17 -0
  112. package/examples/echo-bot/index.ts +171 -0
  113. package/examples/echo-bot/package.json +18 -0
  114. package/examples/group-bot/index.ts +557 -0
  115. package/examples/group-bot/package.json +17 -0
  116. package/examples/rest-gateway/index.ts +499 -0
  117. package/examples/rest-gateway/package.json +19 -0
  118. package/package.json +75 -0
  119. package/src/advanced/features.ts +817 -0
  120. package/src/auth/auth-state.ts +342 -0
  121. package/src/auth/pairing-code.ts +246 -0
  122. package/src/auth/qr-code.ts +191 -0
  123. package/src/calls/handler.ts +153 -0
  124. package/src/core/binary.ts +464 -0
  125. package/src/core/crypto.ts +189 -0
  126. package/src/core/noise.ts +406 -0
  127. package/src/events/emitter.ts +88 -0
  128. package/src/features/index.ts +921 -0
  129. package/src/fixes/index.ts +882 -0
  130. package/src/groups/management.ts +497 -0
  131. package/src/index.ts +274 -0
  132. package/src/messages/media.ts +372 -0
  133. package/src/messages/message-encoder.ts +520 -0
  134. package/src/messages/send.ts +521 -0
  135. package/src/recovery/index.ts +704 -0
  136. package/src/skdm/index.ts +693 -0
  137. package/src/socket/hybrid.ts +414 -0
  138. package/src/socket/wa-socket.ts +1347 -0
  139. package/src/socket/ws-socket.ts +355 -0
  140. package/src/types/index.ts +457 -0
  141. package/src/utils/jid.ts +156 -0
  142. package/src/utils/logger.ts +84 -0
  143. package/src/utils/retry.ts +205 -0
  144. package/tsconfig.json +30 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Waliwa - Mobile API socket layer
3
+ *
4
+ * Companion device pairing via phone number + code (no QR scan needed).
5
+ * Mengikuti WhatsApp Mobile API registration flow:
6
+ * 1. Request pairing code untuk phone number
7
+ * 2. User input code di WA mobile app
8
+ * 3. Server pair device, return identity keys
9
+ * 4. Switch ke WA Web protocol untuk ongoing communication
10
+ *
11
+ * Hybrid approach: pairing via Mobile API, communication via WA Web WS
12
+ */
13
+
14
+ import { EventEmitter } from 'events';
15
+ import { WAWebSocket } from './ws-socket.js';
16
+ import { NoiseHandshake, TrafficCipher } from '../core/noise.js';
17
+ import { generateCurveKeyPair, getRandomBytes, hkdfDerive } from '../core/crypto.js';
18
+ import { encodeBinaryNode, decodeBinaryNode, BinaryNode } from '../core/binary.js';
19
+ import { generatePairingCode, parsePairingResult, PairingCodeExpiry } from '../auth/pairing-code.js';
20
+ import { SilentLogger } from '../utils/logger.js';
21
+ import type {
22
+ AuthenticationCreds,
23
+ PairingCodeResult,
24
+ WALogger,
25
+ WaliwaConfig,
26
+ ConnectionUpdate
27
+ } from '../types/index.js';
28
+
29
+ // ============== Mobile Socket ==============
30
+
31
+ export interface MobileSocketOptions {
32
+ phoneNumber: string;
33
+ creds: AuthenticationCreds;
34
+ logger?: WALogger;
35
+ /** Browser identification */
36
+ browser?: [string, string, string];
37
+ /** WA WebSocket URL setelah pairing */
38
+ waWsUrl?: string;
39
+ connectTimeoutMs?: number;
40
+ ramLevel?: 1 | 2 | 3;
41
+ }
42
+
43
+ /**
44
+ * Mobile API socket - handles companion device pairing via phone number
45
+ *
46
+ * Flow:
47
+ * 1. connect() - init WA WebSocket
48
+ * 2. requestPairing() - generate pairing code untuk user
49
+ * 3. wait for user to input code di mobile app
50
+ * 4. server sends pairing confirmation
51
+ * 5. switch ke normal WA Web communication
52
+ */
53
+ export class MobileAPISocket extends EventEmitter {
54
+ private phoneNumber: string;
55
+ private creds: AuthenticationCreds;
56
+ private logger: WALogger;
57
+ private browser: [string, string, string];
58
+ private waWsUrl: string;
59
+ private connectTimeoutMs: number;
60
+ private ramLevel: 1 | 2 | 3;
61
+
62
+ private ws: WAWebSocket | null = null;
63
+ private pairingExpiry: PairingCodeExpiry | null = null;
64
+ private isPairing: boolean = false;
65
+
66
+ constructor(opts: MobileSocketOptions) {
67
+ super();
68
+ this.phoneNumber = opts.phoneNumber;
69
+ this.creds = opts.creds;
70
+ this.logger = opts.logger ?? new SilentLogger();
71
+ this.browser = opts.browser ?? ['Waliwa', 'WA', '1.0.0'];
72
+ this.waWsUrl = opts.waWsUrl ?? 'wss://web.whatsapp.com/ws/chat';
73
+ this.connectTimeoutMs = opts.connectTimeoutMs ?? 30000;
74
+ this.ramLevel = opts.ramLevel ?? 2;
75
+ }
76
+
77
+ /**
78
+ * Connect dan start pairing flow
79
+ */
80
+ async connect(): Promise<PairingCodeResult> {
81
+ this.logger.info(`Starting Mobile API pairing untuk phone: ${this.phoneNumber}`);
82
+
83
+ // Init WA WebSocket dengan Noise handshake
84
+ this.ws = new WAWebSocket({
85
+ url: this.waWsUrl,
86
+ logger: this.logger,
87
+ browser: this.browser,
88
+ connectTimeoutMs: this.connectTimeoutMs,
89
+ noiseKey: this.creds.noiseKey,
90
+ ramLevel: this.ramLevel
91
+ });
92
+
93
+ // Listen for incoming nodes
94
+ this.ws.on('message', (data: Uint8Array) => this.handleIncomingData(data));
95
+ this.ws.on('close', (code: number, reason: string) => {
96
+ this.logger.warn(`Mobile socket closed: ${code} ${reason}`);
97
+ this.emit('close', code, reason);
98
+ });
99
+ this.ws.on('error', (err: Error) => {
100
+ this.logger.error(`Mobile socket error: ${err.message}`);
101
+ this.emit('error', err);
102
+ });
103
+
104
+ await this.ws.connect();
105
+ this.logger.info('WebSocket connected, generating pairing code');
106
+
107
+ // Generate pairing code
108
+ const pairingResult = generatePairingCode(this.phoneNumber, this.creds, this.logger);
109
+
110
+ // Setup expiry
111
+ this.pairingExpiry = new PairingCodeExpiry(
112
+ pairingResult.expiresAt,
113
+ () => {
114
+ this.logger.warn('Pairing code expired');
115
+ this.emit('pairing:expired');
116
+ }
117
+ );
118
+ this.pairingExpiry.start();
119
+
120
+ // Send pairing request ke server
121
+ await this.sendPairingRequest(pairingResult);
122
+
123
+ this.isPairing = true;
124
+ return pairingResult;
125
+ }
126
+
127
+ /**
128
+ * Send pairing request ke server
129
+ */
130
+ private async sendPairingRequest(pairing: PairingCodeResult): Promise<void> {
131
+ if (!this.creds.pairingKey) {
132
+ throw new Error('No pairing key in creds');
133
+ }
134
+
135
+ const pairNode: BinaryNode = {
136
+ tag: 'iq',
137
+ attrs: {
138
+ type: 'set',
139
+ to: 's.whatsapp.net',
140
+ id: `pair-${Date.now()}`
141
+ },
142
+ content: [
143
+ {
144
+ tag: 'pair',
145
+ attrs: {
146
+ code: pairing.code.replace('-', ''),
147
+ phone: pairing.phoneNumber,
148
+ pubkey: Buffer.from(this.creds.pairingKey.public).toString('base64'),
149
+ platform: this.browser[0]
150
+ }
151
+ }
152
+ ]
153
+ };
154
+
155
+ this.ws!.send(encodeBinaryNode(pairNode));
156
+ this.logger.debug(`Pairing request sent for ${pairing.phoneNumber}`);
157
+ }
158
+
159
+ /**
160
+ * Handle incoming data
161
+ */
162
+ private handleIncomingData(data: Uint8Array): void {
163
+ try {
164
+ const node = decodeBinaryNode(data);
165
+ if (!node) return;
166
+
167
+ // Pairing response
168
+ if (node.tag === 'iq' && node.attrs?.id?.startsWith('pair-')) {
169
+ const result = parsePairingResult(node);
170
+ if (result.success) {
171
+ this.logger.info(`Pairing successful: paired with ${result.jid}`);
172
+ // Update creds
173
+ this.creds.registered = true;
174
+ if (result.jid) {
175
+ this.creds.me = result.jid;
176
+ }
177
+ if (result.identityKey) {
178
+ // Store server-side identity
179
+ this.logger.debug('Got server identity key');
180
+ }
181
+ if (this.pairingExpiry) {
182
+ this.pairingExpiry.cancel();
183
+ this.pairingExpiry = null;
184
+ }
185
+ this.isPairing = false;
186
+ this.emit('paired', result);
187
+ this.emit('connection.update', {
188
+ connection: 'open',
189
+ isNewLogin: true
190
+ } as ConnectionUpdate);
191
+ } else {
192
+ this.logger.error(`Pairing failed: ${result.error}`);
193
+ this.emit('pairing:error', new Error(result.error));
194
+ }
195
+ return;
196
+ }
197
+
198
+ // Re-emit other nodes untuk processing selanjutnya
199
+ this.emit('message', node);
200
+ } catch (err: any) {
201
+ this.logger.error(`Failed to process data: ${err.message}`);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Send raw data via WebSocket
207
+ */
208
+ send(data: Uint8Array): void {
209
+ if (!this.ws || !this.ws.isConnected()) {
210
+ throw new Error('Socket not connected');
211
+ }
212
+ this.ws.send(data);
213
+ }
214
+
215
+ /**
216
+ * Get underlying WA WebSocket (untuk pas-pairing switch ke WA Web mode)
217
+ */
218
+ getWebSocket(): WAWebSocket | null {
219
+ return this.ws;
220
+ }
221
+
222
+ /**
223
+ * Close socket
224
+ */
225
+ async close(): Promise<void> {
226
+ if (this.pairingExpiry) {
227
+ this.pairingExpiry.cancel();
228
+ this.pairingExpiry = null;
229
+ }
230
+ if (this.ws) {
231
+ this.ws.close();
232
+ this.ws = null;
233
+ }
234
+ this.isPairing = false;
235
+ }
236
+
237
+ isPairingInProgress(): boolean {
238
+ return this.isPairing;
239
+ }
240
+ }
241
+
242
+ // ============== Hybrid Socket Manager ==============
243
+
244
+ export interface HybridSocketOptions extends WaliwaConfig {
245
+ /** Primary mode: 'web' (QR) atau 'mobile' (pairing code) */
246
+ primaryMode?: 'web' | 'mobile';
247
+ /** Phone number (required jika primaryMode='mobile') */
248
+ phoneNumber?: string;
249
+ /** Auto-fallback ke other mode jika primary gagal */
250
+ autoFallback?: boolean;
251
+ }
252
+
253
+ /**
254
+ * Hybrid socket manager - otomatis switch antara WA Web (QR) dan Mobile API (pairing)
255
+ *
256
+ * - Primary mode: coba connect dengan mode utama
257
+ * - Fallback: jika gagal setelah maxRetries, switch ke mode lain
258
+ * - Mode switching transparent ke caller
259
+ */
260
+ export class HybridSocketManager extends EventEmitter {
261
+ private opts: Required<HybridSocketOptions>;
262
+ private currentMode: 'web' | 'mobile';
263
+ private currentSocket: WAWebSocket | MobileAPISocket | null = null;
264
+ private retryCount: number = 0;
265
+
266
+ constructor(opts: HybridSocketOptions) {
267
+ super();
268
+ this.opts = {
269
+ ...opts,
270
+ primaryMode: opts.primaryMode ?? (opts.mobile ? 'mobile' : 'web'),
271
+ autoFallback: opts.autoFallback ?? true,
272
+ phoneNumber: opts.phoneNumber ?? '',
273
+ wsURL: opts.wsURL ?? 'wss://web.whatsapp.com/ws/chat',
274
+ connectTimeoutMs: opts.connectTimeoutMs ?? 20000,
275
+ keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 20000,
276
+ ramOptimizationLevel: opts.ramOptimizationLevel ?? 2,
277
+ browser: opts.browser ?? ['Waliwa', 'WA', '1.0.0'],
278
+ printQRInTerminal: opts.printQRInTerminal ?? false,
279
+ mobile: opts.mobile ?? false,
280
+ syncFullHistory: opts.syncFullHistory ?? false,
281
+ maxMsgRetryCount: opts.maxMsgRetryCount ?? 5,
282
+ maxReconnectAttempts: opts.maxReconnectAttempts ?? 10,
283
+ reconnectInterval: opts.reconnectInterval ?? 2000,
284
+ markOnlineOnConnect: opts.markOnlineOnConnect ?? true,
285
+ fetchAgent: opts.fetchAgent
286
+ } as any;
287
+ this.currentMode = this.opts.primaryMode;
288
+ }
289
+
290
+ /**
291
+ * Connect dengan hybrid mode
292
+ */
293
+ async connect(): Promise<any> {
294
+ if (this.currentMode === 'mobile') {
295
+ if (!this.opts.phoneNumber) {
296
+ if (this.opts.autoFallback) {
297
+ this.logger().info('No phone number provided, falling back to QR mode');
298
+ this.currentMode = 'web';
299
+ return this.connect();
300
+ }
301
+ throw new Error('phoneNumber required untuk Mobile API mode');
302
+ }
303
+
304
+ const mobileSocket = new MobileAPISocket({
305
+ phoneNumber: this.opts.phoneNumber!,
306
+ creds: this.opts.authState!.creds,
307
+ logger: this.logger(),
308
+ browser: this.opts.browser!,
309
+ waWsUrl: this.opts.wsURL!,
310
+ connectTimeoutMs: this.opts.connectTimeoutMs!,
311
+ ramLevel: this.opts.ramOptimizationLevel!
312
+ });
313
+
314
+ mobileSocket.on('paired', (result) => this.emit('paired', result));
315
+ mobileSocket.on('pairing:error', (err) => this.handleModeError(err));
316
+ mobileSocket.on('pairing:expired', () => this.handleModeError(new Error('Pairing expired')));
317
+ mobileSocket.on('connection.update', (update) => this.emit('connection.update', update));
318
+ mobileSocket.on('message', (node) => this.emit('message', node));
319
+
320
+ this.currentSocket = mobileSocket;
321
+ return mobileSocket.connect();
322
+ } else {
323
+ // Web mode
324
+ const wsSocket = new WAWebSocket({
325
+ url: this.opts.wsURL!,
326
+ logger: this.logger(),
327
+ browser: this.opts.browser!,
328
+ connectTimeoutMs: this.opts.connectTimeoutMs!,
329
+ keepAliveIntervalMs: this.opts.keepAliveIntervalMs!,
330
+ noiseKey: this.opts.authState!.creds.noiseKey,
331
+ ramLevel: this.opts.ramOptimizationLevel!
332
+ });
333
+
334
+ wsSocket.on('message', (data: Uint8Array) => this.emit('message', data));
335
+ wsSocket.on('close', (code: number, reason: string) => {
336
+ this.emit('close', code, reason);
337
+ });
338
+ wsSocket.on('error', (err: Error) => this.handleModeError(err));
339
+
340
+ this.currentSocket = wsSocket;
341
+ return wsSocket.connect();
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Handle error dari current socket - mungkin switch mode
347
+ */
348
+ private handleModeError(err: Error): void {
349
+ this.retryCount++;
350
+ this.logger().error(`Socket error (${this.currentMode} mode): ${err.message}`);
351
+
352
+ if (this.opts.autoFallback && this.retryCount >= 3) {
353
+ // Switch mode
354
+ const newMode = this.currentMode === 'web' ? 'mobile' : 'web';
355
+ this.logger().warn(`Switching from ${this.currentMode} to ${newMode} mode after ${this.retryCount} retries`);
356
+ this.currentMode = newMode;
357
+ this.retryCount = 0;
358
+
359
+ // Close current dan reconnect
360
+ if (this.currentSocket) {
361
+ if (this.currentSocket instanceof MobileAPISocket) {
362
+ this.currentSocket.close();
363
+ } else {
364
+ this.currentSocket.close();
365
+ }
366
+ this.currentSocket = null;
367
+ }
368
+
369
+ // Reconnect dengan mode baru
370
+ this.connect().catch(reconnectErr => {
371
+ this.logger().error(`Reconnect failed: ${reconnectErr.message}`);
372
+ });
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Send data via current socket
378
+ */
379
+ send(data: Uint8Array): void {
380
+ if (!this.currentSocket) {
381
+ throw new Error('No active socket');
382
+ }
383
+ if (this.currentSocket instanceof MobileAPISocket) {
384
+ this.currentSocket.send(data);
385
+ } else {
386
+ this.currentSocket.send(data);
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Close socket
392
+ */
393
+ async close(): Promise<void> {
394
+ if (this.currentSocket) {
395
+ if (this.currentSocket instanceof MobileAPISocket) {
396
+ await this.currentSocket.close();
397
+ } else {
398
+ this.currentSocket.close();
399
+ }
400
+ this.currentSocket = null;
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Get current mode
406
+ */
407
+ getCurrentMode(): 'web' | 'mobile' {
408
+ return this.currentMode;
409
+ }
410
+
411
+ private logger(): WALogger {
412
+ return this.opts.logger ?? new SilentLogger();
413
+ }
414
+ }