@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,406 @@
1
+ /**
2
+ * Waliwa - Noise Protocol XX handshake implementation
3
+ *
4
+ * WhatsApp Web Multi-Device menggunakan Noise Protocol Framework (XX pattern)
5
+ * untuk establish secure channel antara client dan WhatsApp server.
6
+ *
7
+ * Pattern XX (lihat noiseprotocol.com):
8
+ * <- e
9
+ * -> e, ee, s, es
10
+ * <- s, se
11
+ *
12
+ * Handshake ini menghasilkan encryption key untuk semua komunikasi WebSocket selanjutnya.
13
+ *
14
+ * Original implementation - tidak copy dari Baileys/ZapoJS.
15
+ */
16
+
17
+ import {
18
+ generateCurveKeyPair,
19
+ curveSharedSecret,
20
+ hkdfDerive,
21
+ hmacSha256,
22
+ sha256Hash,
23
+ createAESCipher,
24
+ getRandomBytes,
25
+ concatBytes,
26
+ areBytesEqual,
27
+ KeyPair,
28
+ AESCipher
29
+ } from './crypto.js';
30
+
31
+ // ============== Noise Constants ==============
32
+
33
+ /** Noise protocol name - WhatsApp menggunakan "Noise_XX_25519_AESGCM_SHA256" */
34
+ const NOISE_PROTOCOL_NAME = 'Noise_XX_25519_AESGCM_SHA256';
35
+ const NOISE_HANDSHAKE_DONE = 'Noise_XX_25519_AESGCM_SHA256_handshake_done';
36
+ const HKDF_INFO = new TextEncoder().encode(NOISE_PROTOCOL_NAME);
37
+
38
+ // WhatsApp specific constants
39
+ const WA_NOISE_HEADER = new Uint8Array([0x57, 0x41]); // "WA" - 2 bytes prefix
40
+ const WA_VERSION = new Uint8Array([0x05, 0x2d]); // Version 2.45 encoded
41
+
42
+ // Max ciphertext size untuk WA frames
43
+ const MAX_FRAME_SIZE = 1024 * 1024; // 1 MB
44
+
45
+ // ============== Cipher State ==============
46
+
47
+ interface CipherState {
48
+ key: Uint8Array | null;
49
+ nonce: number; // Counter-based nonce, increments per message
50
+ }
51
+
52
+ interface SymmetricState {
53
+ ck: Uint8Array; // Chaining key
54
+ h: Uint8Array; // Handshake hash
55
+ cipher: CipherState;
56
+ }
57
+
58
+ interface HandshakeState {
59
+ ss: SymmetricState;
60
+ s: KeyPair; // Static key pair
61
+ e: KeyPair; // Ephemeral key pair
62
+ rs: Uint8Array | null; // Remote static public key
63
+ re: Uint8Array | null; // Remote ephemeral public key
64
+ initiator: boolean;
65
+ messagePattern: ('e' | 's' | 'ee' | 'es' | 'se' | 'ss')[];
66
+ }
67
+
68
+ // ============== Noise Handshake ==============
69
+
70
+ export class NoiseHandshake {
71
+ private state: HandshakeState;
72
+ private cipher: AESCipher;
73
+ private readonly cipherKeyLen = 32;
74
+ private readonly nonceLen = 12;
75
+
76
+ constructor(staticKey?: KeyPair, initiator: boolean = true) {
77
+ this.state = {
78
+ ss: this.initializeSymmetric(),
79
+ s: staticKey ?? generateCurveKeyPair(),
80
+ e: generateCurveKeyPair(),
81
+ rs: null,
82
+ re: null,
83
+ initiator,
84
+ messagePattern: ['e', 'e', 'ee', 's', 'es', 's', 'se']
85
+ };
86
+ this.cipher = createAESCipher(new Uint8Array(32));
87
+ // Mix protocol name into hash
88
+ this.state.ss.h = sha256Hash(new TextEncoder().encode(NOISE_PROTOCOL_NAME));
89
+ }
90
+
91
+ private initializeSymmetric(): SymmetricState {
92
+ const protocolHash = sha256Hash(new TextEncoder().encode(NOISE_PROTOCOL_NAME));
93
+ return {
94
+ ck: protocolHash,
95
+ h: protocolHash,
96
+ cipher: { key: null, nonce: 0 }
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Mix key into chaining key + derive new cipher key
102
+ */
103
+ private mixKey(inputKey: Uint8Array): void {
104
+ const derived = hkdfDerive(
105
+ inputKey,
106
+ this.state.ss.ck,
107
+ new Uint8Array(0),
108
+ 64
109
+ );
110
+ // Split into 32-byte ck and 32-byte key
111
+ this.state.ss.ck = derived.subarray(0, 32);
112
+ this.state.ss.cipher.key = derived.subarray(32, 64);
113
+ this.state.ss.cipher.nonce = 0;
114
+ }
115
+
116
+ /**
117
+ * Mix hash data into handshake hash
118
+ */
119
+ private mixHash(data: Uint8Array): void {
120
+ this.state.ss.h = sha256Hash(concatBytes(this.state.ss.h, data));
121
+ }
122
+
123
+ /**
124
+ * Encrypt dengan handshake key
125
+ */
126
+ private encryptWithHash(plaintext: Uint8Array): Uint8Array {
127
+ if (!this.state.ss.cipher.key) {
128
+ return plaintext;
129
+ }
130
+ const iv = this.generateNonce(this.state.ss.cipher.nonce);
131
+ const { ciphertext, tag } = this.cipher.encrypt(
132
+ plaintext,
133
+ iv,
134
+ this.state.ss.h
135
+ );
136
+ this.state.ss.cipher.nonce++;
137
+ return concatBytes(ciphertext, tag);
138
+ }
139
+
140
+ /**
141
+ * Decrypt dengan handshake key
142
+ */
143
+ private decryptWithHash(ciphertext: Uint8Array): Uint8Array {
144
+ if (!this.state.ss.cipher.key) {
145
+ return ciphertext;
146
+ }
147
+ if (ciphertext.length < 16) {
148
+ throw new Error('Ciphertext too short');
149
+ }
150
+ const iv = this.generateNonce(this.state.ss.cipher.nonce);
151
+ const ct = ciphertext.subarray(0, ciphertext.length - 16);
152
+ const tag = ciphertext.subarray(ciphertext.length - 16);
153
+ const plaintext = this.cipher.decrypt(ct, iv, tag, this.state.ss.h);
154
+ this.state.ss.cipher.nonce++;
155
+ return plaintext;
156
+ }
157
+
158
+ /**
159
+ * Generate GCM nonce dari counter (big-endian, 4-byte prefix zero)
160
+ */
161
+ private generateNonce(counter: number): Uint8Array {
162
+ const nonce = new Uint8Array(12);
163
+ // Last 8 bytes = counter (big-endian)
164
+ const view = new DataView(nonce.buffer);
165
+ view.setUint32(4, Math.floor(counter / 0x100000000));
166
+ view.setUint32(8, counter & 0xffffffff);
167
+ return nonce;
168
+ }
169
+
170
+ // ============== Public Handshake Methods ==============
171
+
172
+ /**
173
+ * Step 1: Send ephemeral key (initiator -> responder)
174
+ * Output: bytes untuk dikirim ke server
175
+ */
176
+ public writeEphemeral(): Uint8Array {
177
+ const e = generateCurveKeyPair();
178
+ this.state.e = e;
179
+ this.mixHash(e.public);
180
+ this.mixKey(e.public);
181
+ return e.public;
182
+ }
183
+
184
+ /**
185
+ * Step 2: Receive ephemeral + static + EE + ES from responder
186
+ */
187
+ public readResponderHandshake(payload: Uint8Array): { plaintext: Uint8Array } {
188
+ // First 32 bytes = ephemeral key
189
+ if (payload.length < 32) throw new Error('Handshake too short');
190
+ const re = payload.subarray(0, 32);
191
+ this.state.re = re;
192
+ this.mixHash(re);
193
+ this.mixKey(re);
194
+
195
+ // Ephemeral-ephemeral DH
196
+ const ee = curveSharedSecret(this.state.e.private, re);
197
+ this.mixKey(ee);
198
+
199
+ // Next: encrypted static key + payload
200
+ const encryptedStatic = payload.subarray(32);
201
+ const remoteStatic = this.decryptWithHash(encryptedStatic);
202
+ this.state.rs = remoteStatic.subarray(0, 32);
203
+ this.mixHash(remoteStatic);
204
+
205
+ // Ephemeral-static DH
206
+ const es = curveSharedSecret(this.state.e.private, remoteStatic);
207
+ this.mixKey(es);
208
+
209
+ // Remaining: encrypted payload
210
+ const encryptedPayload = remoteStatic.subarray(32);
211
+ const payload2 = this.decryptWithHash(encryptedPayload);
212
+
213
+ return { plaintext: payload2 };
214
+ }
215
+
216
+ /**
217
+ * Step 3: Send our static + ES + payload (initiator -> responder)
218
+ */
219
+ public writeInitiatorHandshake(payload: Uint8Array): Uint8Array {
220
+ // Send ephemeral already done in step 1
221
+ // Now send static + ES + payload, all encrypted with current handshake key
222
+
223
+ // Encrypt our static key
224
+ const encryptedStatic = this.encryptWithHash(this.state.s.public);
225
+ this.mixHash(this.state.s.public);
226
+
227
+ // Static-ephemeral DH (us.remote ephemeral)
228
+ if (!this.state.re) throw new Error('No remote ephemeral');
229
+ const se = curveSharedSecret(this.state.s.private, this.state.re);
230
+ this.mixKey(se);
231
+
232
+ // Encrypt payload
233
+ const encryptedPayload = this.encryptWithHash(payload);
234
+
235
+ return concatBytes(encryptedStatic, encryptedPayload);
236
+ }
237
+
238
+ /**
239
+ * Finalize handshake - split prk into send/recv keys
240
+ */
241
+ public finalize(): { sendKey: Uint8Array; recvKey: Uint8Array } {
242
+ const derived = hkdfDerive(
243
+ new Uint8Array(0),
244
+ this.state.ss.ck,
245
+ new TextEncoder().encode(NOISE_HANDSHAKE_DONE),
246
+ 64
247
+ );
248
+ return {
249
+ sendKey: derived.subarray(0, 32),
250
+ recvKey: derived.subarray(32, 64)
251
+ };
252
+ }
253
+
254
+ // ============== Post-handshake AEAD ==============
255
+
256
+ /**
257
+ * Encrypt a frame post-handshake menggunakan traffic keys
258
+ */
259
+ public static encryptFrame(
260
+ key: Uint8Array,
261
+ counter: number,
262
+ plaintext: Uint8Array
263
+ ): Uint8Array {
264
+ const cipher = createAESCipher(key);
265
+ const iv = new Uint8Array(12);
266
+ const view = new DataView(iv.buffer);
267
+ view.setUint32(4, Math.floor(counter / 0x100000000));
268
+ view.setUint32(8, counter & 0xffffffff);
269
+ const { ciphertext, tag } = cipher.encrypt(plaintext, iv);
270
+ return concatBytes(ciphertext, tag);
271
+ }
272
+
273
+ /**
274
+ * Decrypt a frame post-handshake
275
+ */
276
+ public static decryptFrame(
277
+ key: Uint8Array,
278
+ counter: number,
279
+ ciphertext: Uint8Array
280
+ ): Uint8Array {
281
+ if (ciphertext.length < 16) throw new Error('Frame too short');
282
+ const cipher = createAESCipher(key);
283
+ const iv = new Uint8Array(12);
284
+ const view = new DataView(iv.buffer);
285
+ view.setUint32(4, Math.floor(counter / 0x100000000));
286
+ view.setUint32(8, counter & 0xffffffff);
287
+ const ct = ciphertext.subarray(0, ciphertext.length - 16);
288
+ const tag = ciphertext.subarray(ciphertext.length - 16);
289
+ return cipher.decrypt(ct, iv, tag);
290
+ }
291
+
292
+ // ============== Frame helpers ==============
293
+
294
+ /**
295
+ * Encode WA noise frame: [WA_HEADER] [VERSION] [LENGTH:3] [PAYLOAD]
296
+ */
297
+ public static encodeFrame(payload: Uint8Array): Uint8Array {
298
+ const len = payload.length;
299
+ const header = new Uint8Array(3 + 2 + 2); // WA + version + 16-bit length placeholder (3 byte for >64KB)
300
+
301
+ // WA header
302
+ header[0] = 0x57; // 'W'
303
+ header[1] = 0x41; // 'A'
304
+ // Version
305
+ header[2] = 0x05;
306
+ header[3] = 0x2d;
307
+ // Length (3 bytes big-endian for up to 16MB)
308
+ header[4] = (len >> 16) & 0xff;
309
+ header[5] = (len >> 8) & 0xff;
310
+ header[6] = len & 0xff;
311
+
312
+ return concatBytes(header.subarray(0, 7), payload);
313
+ }
314
+
315
+ /**
316
+ * Encode WA client payload (untuk handshake init message)
317
+ * Format: [ephemeral_pubkey:32] [static_encrypted] [payload_encrypted]
318
+ */
319
+ public static encodeClientHello(
320
+ ephemeralPub: Uint8Array,
321
+ encryptedStatic: Uint8Array,
322
+ encryptedPayload: Uint8Array
323
+ ): Uint8Array {
324
+ return concatBytes(ephemeralPub, encryptedStatic, encryptedPayload);
325
+ }
326
+
327
+ /**
328
+ * Build client hello untuk initial connection
329
+ */
330
+ public buildClientHello(clientPayload: Uint8Array): Uint8Array {
331
+ // Step 1 + 3 combined for client hello
332
+ const ephemeral = this.writeEphemeral();
333
+ const encrypted = this.writeInitiatorHandshake(clientPayload);
334
+ return concatBytes(ephemeral, encrypted);
335
+ }
336
+ }
337
+
338
+ // ============== Traffic Cipher (post-handshake) ==============
339
+
340
+ /**
341
+ * Traffic cipher untuk encrypt/decrypt frames setelah handshake done
342
+ */
343
+ export class TrafficCipher {
344
+ private sendKey: Uint8Array;
345
+ private recvKey: Uint8Array;
346
+ private sendCounter: number = 0;
347
+ private recvCounter: number = 0;
348
+
349
+ constructor(sendKey: Uint8Array, recvKey: Uint8Array) {
350
+ this.sendKey = sendKey;
351
+ this.recvKey = recvKey;
352
+ }
353
+
354
+ encrypt(plaintext: Uint8Array): Uint8Array {
355
+ const result = NoiseHandshake.encryptFrame(this.sendKey, this.sendCounter, plaintext);
356
+ this.sendCounter++;
357
+ return result;
358
+ }
359
+
360
+ decrypt(ciphertext: Uint8Array): Uint8Array {
361
+ const plaintext = NoiseHandshake.decryptFrame(this.recvKey, this.recvCounter, ciphertext);
362
+ this.recvCounter++;
363
+ return plaintext;
364
+ }
365
+
366
+ reset() {
367
+ this.sendCounter = 0;
368
+ this.recvCounter = 0;
369
+ }
370
+ }
371
+
372
+ // ============== Client Payload Builder ==============
373
+
374
+ /**
375
+ * Build initial client payload untuk WA Web multi-device connect
376
+ * Berisi info browser, version, dan props
377
+ */
378
+ export function buildClientPayload(browser: [string, string, string]): Uint8Array {
379
+ // Encoded sebagai WA binary node
380
+ const enc = new TextEncoder();
381
+ const parts: Uint8Array[] = [];
382
+
383
+ // Browser info: [name, short_name, version]
384
+ parts.push(enc.encode(browser[0]));
385
+ parts.push(enc.encode(browser[1]));
386
+ parts.push(enc.encode(browser[2]));
387
+
388
+ // Pairing static key (32 bytes) - atau empty jika belum ada
389
+ // This is generated at first connection and stored in creds
390
+ // For first-time: generate new
391
+ const pairingKey = generateCurveKeyPair();
392
+ parts.push(pairingKey.public);
393
+
394
+ // Compute total length
395
+ let totalLen = 0;
396
+ for (const p of parts) totalLen += p.length;
397
+
398
+ // Prepend each part with length byte (assuming < 256)
399
+ const result: number[] = [];
400
+ for (const p of parts) {
401
+ result.push(p.length);
402
+ for (let i = 0; i < p.length; i++) result.push(p[i]);
403
+ }
404
+
405
+ return new Uint8Array(result);
406
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Waliwa - Event emitter untuk WebSocket events
3
+ * Typed event emitter untuk WhatsApp events (messages, presence, calls, dll)
4
+ *
5
+ * RAM optimization: gunakan Map untuk handlers, weakly referenced jika possible
6
+ */
7
+
8
+ import { EventEmitter } from 'events';
9
+ import type { WAEventMap } from '../types/index.js';
10
+
11
+ export class WAEventEmitter extends EventEmitter {
12
+ /**
13
+ * Listen untuk specific event
14
+ */
15
+ on<K extends keyof WAEventMap>(event: K, listener: (payload: WAEventMap[K]) => void): this {
16
+ return super.on(event, listener as any);
17
+ }
18
+
19
+ /**
20
+ * Listen once
21
+ */
22
+ once<K extends keyof WAEventMap>(event: K, listener: (payload: WAEventMap[K]) => void): this {
23
+ return super.once(event, listener as any);
24
+ }
25
+
26
+ /**
27
+ * Listen dengan off support
28
+ */
29
+ addListener<K extends keyof WAEventMap>(event: K, listener: (payload: WAEventMap[K]) => void): this {
30
+ return super.addListener(event, listener as any);
31
+ }
32
+
33
+ /**
34
+ * Emit event ke all listeners
35
+ */
36
+ emit<K extends keyof WAEventMap>(event: K, payload: WAEventMap[K]): boolean {
37
+ return super.emit(event, payload);
38
+ }
39
+
40
+ /**
41
+ * Remove listener
42
+ */
43
+ off<K extends keyof WAEventMap>(event: K, listener: (payload: WAEventMap[K]) => void): this {
44
+ return super.off(event, listener as any);
45
+ }
46
+
47
+ /**
48
+ * Wait for event dengan timeout
49
+ */
50
+ async waitFor<K extends keyof WAEventMap>(
51
+ event: K,
52
+ timeoutMs: number = 30000
53
+ ): Promise<WAEventMap[K]> {
54
+ return new Promise((resolve, reject) => {
55
+ const timeout = setTimeout(() => {
56
+ this.off(event, handler);
57
+ reject(new Error(`Timeout waiting for ${String(event)}`));
58
+ }, timeoutMs);
59
+
60
+ const handler = (payload: WAEventMap[K]) => {
61
+ clearTimeout(timeout);
62
+ this.off(event, handler);
63
+ resolve(payload);
64
+ };
65
+ this.on(event, handler);
66
+ });
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Strict event emitter - hanya registered events yang di-allow
72
+ * Berguna untuk catching typos dalam event names
73
+ */
74
+ export class StrictEventEmitter extends WAEventEmitter {
75
+ private knownEvents: Set<string>;
76
+
77
+ constructor(events: (keyof WAEventMap)[]) {
78
+ super();
79
+ this.knownEvents = new Set(events as string[]);
80
+ }
81
+
82
+ emit<K extends keyof WAEventMap>(event: K, payload: WAEventMap[K]): boolean {
83
+ if (!this.knownEvents.has(event as string)) {
84
+ throw new Error(`Unknown event: ${String(event)}`);
85
+ }
86
+ return super.emit(event, payload);
87
+ }
88
+ }