@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,355 @@
1
+ /**
2
+ * Waliwa - WebSocket layer untuk WhatsApp Web Multi-Device
3
+ *
4
+ * Connect ke wss://web.whatsapp.com/ws/chat dengan Noise protocol encrypted frames.
5
+ * RAM optimization: gunakan buffer pooling, hindari Buffer.concat berlebihan,
6
+ * drain queue ASAP.
7
+ */
8
+
9
+ import WebSocket from 'ws';
10
+ import { EventEmitter } from 'events';
11
+ import {
12
+ NoiseHandshake,
13
+ TrafficCipher
14
+ } from '../core/noise.js';
15
+ import { concatBytes, getRandomBytes } from '../core/crypto.js';
16
+ import type { WALogger } from '../types/index.js';
17
+ import { SilentLogger } from '../utils/logger.js';
18
+
19
+ export type WAWebSocketEvent =
20
+ | 'open'
21
+ | 'message'
22
+ | 'close'
23
+ | 'error'
24
+ | 'handshake:done'
25
+ | 'handshake:error';
26
+
27
+ export interface WAWebSocketOptions {
28
+ url: string;
29
+ logger?: WALogger;
30
+ /** Browser identification for client payload */
31
+ browser?: [string, string, string];
32
+ /** Connect timeout ms */
33
+ connectTimeoutMs?: number;
34
+ /** Keep alive interval ms */
35
+ keepAliveIntervalMs?: number;
36
+ /** Static key pair untuk Noise (persistent across sessions) */
37
+ noiseKey?: { private: Uint8Array; public: Uint8Array };
38
+ /** RAM optimization level (1-3) */
39
+ ramLevel?: 1 | 2 | 3;
40
+ }
41
+
42
+ const DEFAULT_WA_URL = 'wss://web.whatsapp.com/ws/chat';
43
+ const DEFAULT_BROWSER: [string, string, string] = ['Waliwa', 'WA', '1.0.0'];
44
+
45
+ // ============== Frame Buffer Pool (RAM optimization) ==============
46
+
47
+ /**
48
+ * Buffer pool untuk reuse buffer daripada alokasi ulang.
49
+ * RAM-friendly pattern untuk high-throughput message processing.
50
+ */
51
+ class BufferPool {
52
+ private pool: Uint8Array[] = [];
53
+ private readonly maxSize: number;
54
+ private readonly bufferSize: number;
55
+
56
+ constructor(bufferSize: number = 4096, maxPoolSize: number = 16) {
57
+ this.bufferSize = bufferSize;
58
+ this.maxSize = maxPoolSize;
59
+ }
60
+
61
+ acquire(): Uint8Array {
62
+ const buf = this.pool.pop();
63
+ return buf ?? new Uint8Array(this.bufferSize);
64
+ }
65
+
66
+ release(buf: Uint8Array): void {
67
+ if (this.pool.length < this.maxSize) {
68
+ this.pool.push(buf);
69
+ }
70
+ }
71
+ }
72
+
73
+ // ============== WA WebSocket ==============
74
+
75
+ export class WAWebSocket extends EventEmitter {
76
+ private ws: WebSocket | null = null;
77
+ private url: string;
78
+ private logger: WALogger;
79
+ private browser: [string, string, string];
80
+ private connectTimeoutMs: number;
81
+ private keepAliveIntervalMs: number;
82
+ private noiseKey?: { private: Uint8Array; public: Uint8Array };
83
+ private ramLevel: 1 | 2 | 3;
84
+
85
+ private trafficCipher: TrafficCipher | null = null;
86
+ private handshake: NoiseHandshake | null = null;
87
+ private keepAliveTimer: NodeJS.Timeout | null = null;
88
+ private connectTimer: NodeJS.Timeout | null = null;
89
+ private isOpen: boolean = false;
90
+ private isHandshakeDone: boolean = false;
91
+ private recvBuffer: number[] = [];
92
+ private bufferPool: BufferPool;
93
+
94
+ constructor(opts: WAWebSocketOptions) {
95
+ super();
96
+ this.url = opts.url || DEFAULT_WA_URL;
97
+ this.logger = opts.logger ?? new SilentLogger();
98
+ this.browser = opts.browser ?? DEFAULT_BROWSER;
99
+ this.connectTimeoutMs = opts.connectTimeoutMs ?? 20000;
100
+ this.keepAliveIntervalMs = opts.keepAliveIntervalMs ?? 20000;
101
+ this.noiseKey = opts.noiseKey;
102
+ this.ramLevel = opts.ramLevel ?? 2;
103
+ this.bufferPool = new BufferPool(8192, this.ramLevel === 3 ? 32 : 16);
104
+ }
105
+
106
+ /**
107
+ * Connect ke WhatsApp WebSocket server
108
+ */
109
+ async connect(): Promise<void> {
110
+ return new Promise((resolve, reject) => {
111
+ this.logger.info(`Connecting to ${this.url}`);
112
+ this.ws = new WebSocket(this.url, {
113
+ origin: 'https://web.whatsapp.com',
114
+ headers: {
115
+ 'Origin': 'https://web.whatsapp.com',
116
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
117
+ },
118
+ perMessageDeflate: false, // Hindari CPU overhead untuk RAM efficiency
119
+ maxPayload: 16 * 1024 * 1024 // 16MB max payload
120
+ });
121
+
122
+ // Connect timeout
123
+ this.connectTimer = setTimeout(() => {
124
+ if (!this.isOpen) {
125
+ this.ws?.terminate();
126
+ reject(new Error('WebSocket connect timeout'));
127
+ }
128
+ }, this.connectTimeoutMs);
129
+
130
+ this.ws.on('open', () => {
131
+ this.logger.info('WebSocket connected, performing Noise handshake');
132
+ this.isOpen = true;
133
+ if (this.connectTimer) {
134
+ clearTimeout(this.connectTimer);
135
+ this.connectTimer = null;
136
+ }
137
+ this.startHandshake().then(() => {
138
+ this.isHandshakeDone = true;
139
+ this.emit('handshake:done');
140
+ this.startKeepAlive();
141
+ resolve();
142
+ }).catch(err => {
143
+ this.logger.error(`Handshake failed: ${err.message}`);
144
+ this.emit('handshake:error', err);
145
+ reject(err);
146
+ });
147
+ });
148
+
149
+ this.ws.on('message', (data: Buffer) => this.handleMessage(data));
150
+ this.ws.on('close', (code: number, reason: Buffer) => {
151
+ this.logger.warn(`WebSocket closed: ${code} ${reason.toString()}`);
152
+ this.isOpen = false;
153
+ this.isHandshakeDone = false;
154
+ this.stopKeepAlive();
155
+ this.emit('close', code, reason.toString());
156
+ });
157
+ this.ws.on('error', (err: Error) => {
158
+ this.logger.error(`WebSocket error: ${err.message}`);
159
+ this.emit('error', err);
160
+ if (!this.isOpen) {
161
+ if (this.connectTimer) {
162
+ clearTimeout(this.connectTimer);
163
+ this.connectTimer = null;
164
+ }
165
+ reject(err);
166
+ }
167
+ });
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Start Noise Protocol XX handshake
173
+ */
174
+ private async startHandshake(): Promise<void> {
175
+ this.handshake = new NoiseHandshake(this.noiseKey, true);
176
+
177
+ // Build client hello: ephemeral + encrypted static + encrypted payload
178
+ const clientPayload = this.buildClientPayload();
179
+ const clientHello = this.handshake.buildClientHello(clientPayload);
180
+
181
+ // Wrap dengan WA frame header
182
+ const frame = NoiseHandshake.encodeFrame(clientHello);
183
+ this.ws!.send(Buffer.from(frame));
184
+
185
+ // Wait for server response
186
+ return new Promise((resolve, reject) => {
187
+ const timeout = setTimeout(() => {
188
+ reject(new Error('Handshake response timeout'));
189
+ }, 15000);
190
+
191
+ const onceHandler = (data: Buffer) => {
192
+ clearTimeout(timeout);
193
+ this.ws!.off('message', onceHandler);
194
+ try {
195
+ this.processHandshakeResponse(data);
196
+ resolve();
197
+ } catch (err) {
198
+ reject(err);
199
+ }
200
+ };
201
+ this.ws!.once('message', onceHandler);
202
+ });
203
+ }
204
+
205
+ /**
206
+ * Process handshake response dari server
207
+ */
208
+ private processHandshakeResponse(data: Buffer): void {
209
+ if (!this.handshake) throw new Error('No handshake state');
210
+
211
+ // Strip WA header (7 bytes: WA + version + length)
212
+ const payload = data.subarray(7);
213
+ this.handshake.readResponderHandshake(payload);
214
+
215
+ // Finalize - dapatkan traffic keys
216
+ const { sendKey, recvKey } = this.handshake.finalize();
217
+ this.trafficCipher = new TrafficCipher(sendKey, recvKey);
218
+ this.logger.debug('Noise handshake completed, traffic keys derived');
219
+ }
220
+
221
+ /**
222
+ * Build client payload - browser info + pairing static key
223
+ */
224
+ private buildClientPayload(): Uint8Array {
225
+ const enc = new TextEncoder();
226
+ const parts: Uint8Array[] = [];
227
+
228
+ parts.push(enc.encode(this.browser[0]));
229
+ parts.push(enc.encode(this.browser[1]));
230
+ parts.push(enc.encode(this.browser[2]));
231
+
232
+ // Generate pairing static key (atau reuse dari creds)
233
+ const pairingKey = this.noiseKey ?? this.generatePairingKey();
234
+ parts.push(pairingKey.public);
235
+
236
+ // Encode as length-prefixed
237
+ const result: number[] = [];
238
+ for (const p of parts) {
239
+ result.push(p.length);
240
+ for (let i = 0; i < p.length; i++) result.push(p[i]);
241
+ }
242
+ return new Uint8Array(result);
243
+ }
244
+
245
+ private generatePairingKey(): { private: Uint8Array; public: Uint8Array } {
246
+ const { generateCurveKeyPair } = require('../core/crypto.js');
247
+ return generateCurveKeyPair();
248
+ }
249
+
250
+ /**
251
+ * Handle incoming message - decrypt jika handshake done
252
+ */
253
+ private handleMessage(data: Buffer): void {
254
+ if (!this.isHandshakeDone) {
255
+ // Should be handled by handshake
256
+ return;
257
+ }
258
+ try {
259
+ // Strip WA header (7 bytes) dan decrypt
260
+ const payload = data.subarray(7);
261
+ const plaintext = this.trafficCipher!.decrypt(payload);
262
+ this.emit('message', plaintext);
263
+ } catch (err: any) {
264
+ this.logger.error(`Message decrypt failed: ${err.message}`);
265
+ this.emit('error', err);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Send encrypted frame ke server
271
+ */
272
+ send(plaintext: Uint8Array): void {
273
+ if (!this.isHandshakeDone || !this.trafficCipher) {
274
+ throw new Error('Cannot send before handshake complete');
275
+ }
276
+ const ciphertext = this.trafficCipher.encrypt(plaintext);
277
+ const frame = NoiseHandshake.encodeFrame(ciphertext);
278
+ this.ws!.send(Buffer.from(frame));
279
+ }
280
+
281
+ /**
282
+ * Start keep-alive ping
283
+ */
284
+ private startKeepAlive(): void {
285
+ this.stopKeepAlive();
286
+ this.keepAliveTimer = setInterval(() => {
287
+ if (this.isOpen && this.isHandshakeDone) {
288
+ // Send ping sebagai binary node "iq" type "get" query "ping"
289
+ const pingFrame = this.buildPingFrame();
290
+ try {
291
+ this.send(pingFrame);
292
+ } catch (err: any) {
293
+ this.logger.warn(`Keep-alive ping failed: ${err.message}`);
294
+ }
295
+ }
296
+ }, this.keepAliveIntervalMs);
297
+ }
298
+
299
+ private stopKeepAlive(): void {
300
+ if (this.keepAliveTimer) {
301
+ clearInterval(this.keepAliveTimer);
302
+ this.keepAliveTimer = null;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Build ping frame: <iq type="get"><ping/></iq>
308
+ */
309
+ private buildPingFrame(): Uint8Array {
310
+ // Simple binary ping: tag=iq, attrs={type:get}, content=ping node
311
+ const { encodeBinaryNode } = require('../core/binary.js');
312
+ const node = {
313
+ tag: 'iq',
314
+ attrs: { type: 'get', id: this.generateId() },
315
+ content: [{ tag: 'ping', attrs: {} }]
316
+ };
317
+ return encodeBinaryNode(node);
318
+ }
319
+
320
+ private generateId(): string {
321
+ return Date.now().toString(36) + Math.random().toString(36).substr(2, 8);
322
+ }
323
+
324
+ /**
325
+ * Close WebSocket connection
326
+ */
327
+ close(code: number = 1000, reason: string = 'normal'): void {
328
+ this.stopKeepAlive();
329
+ if (this.connectTimer) {
330
+ clearTimeout(this.connectTimer);
331
+ this.connectTimer = null;
332
+ }
333
+ this.isOpen = false;
334
+ this.isHandshakeDone = false;
335
+ if (this.ws) {
336
+ this.ws.close(code, reason);
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Get connection state
342
+ */
343
+ isConnected(): boolean {
344
+ return this.isOpen && this.isHandshakeDone;
345
+ }
346
+
347
+ /**
348
+ * Get traffic cipher (untuk debug)
349
+ */
350
+ getTrafficCipher(): TrafficCipher | null {
351
+ return this.trafficCipher;
352
+ }
353
+ }
354
+
355
+ export default WAWebSocket;