@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,464 @@
1
+ /**
2
+ * Waliwa - Binary encoder/decoder untuk WhatsApp Web binary protocol
3
+ *
4
+ * WhatsApp menggunakan custom binary format untuk encode pesan di WebSocket.
5
+ * Format ini berbeda dari protobuf - lebih ringan dan custom.
6
+ *
7
+ * Manual implementation untuk pemahaman mendalam struktur WA binary.
8
+ * Reference: WhatsApp Web binary tags, BINARY_NODE_ENCODING
9
+ */
10
+
11
+ // ============== Tag Constants ==============
12
+
13
+ // List tags (pakai byte ini untuk type)
14
+ export enum TAG_LIST {
15
+ EMPTY = 0,
16
+ DICTIONARY_0 = 236, // 0xFC
17
+ DICTIONARY_1 = 237, // 0xFD
18
+ DICTIONARY_2 = 238, // 0xFE
19
+ DICTIONARY_3 = 239, // 0xFF
20
+ }
21
+
22
+ // Nibble pack (4 bits per char)
23
+ export const TAG_NIBBLE = 255; // 0xFF already used by dict 3, so different
24
+ export const TAG_HEX = 254; // ditto
25
+
26
+ // Hex tags
27
+ export const TAG_BINARY_8 = 252; // 0xFC
28
+ export const TAG_BINARY_20 = 253; // 0xFD
29
+ export const TAG_BINARY_32 = 254; // 0xFE
30
+ export const TAG_ADJACENT = 255; // 0xFF
31
+ export const TAG_LIST_8 = 248; // 0xF8
32
+ export const TAG_LIST_16 = 249; // 0xF9
33
+ export const TAG_JID_PAIR = 250; // 0xFA
34
+ export const TAG_PACKED_NIBBLE = 251; // 0xFB
35
+ export const TAG_PACKED_HEX = 252; // 0xFC
36
+
37
+ // ============== Packed Encoding ==============
38
+
39
+ const HEX_CHARS = '0123456789abcdef';
40
+
41
+ /**
42
+ * Pack nibbles (4 bits per char, jadi 2 char per byte) - lebih hemat space
43
+ * untuk encoding string numeric/hex
44
+ */
45
+ export function packNibbles(str: string): Uint8Array {
46
+ // Pad agar panjang genap
47
+ if (str.length % 2 !== 0) str += '0';
48
+ const result = new Uint8Array(str.length / 2);
49
+ for (let i = 0; i < str.length; i += 2) {
50
+ const high = parseInt(str[i], 16);
51
+ const low = parseInt(str[i + 1], 16);
52
+ result[i / 2] = (high << 4) | low;
53
+ }
54
+ return result;
55
+ }
56
+
57
+ /**
58
+ * Unpack nibbles dari binary ke string
59
+ */
60
+ export function unpackNibbles(bytes: Uint8Array): string {
61
+ let result = '';
62
+ for (let i = 0; i < bytes.length; i++) {
63
+ const high = (bytes[i] >> 4) & 0xf;
64
+ const low = bytes[i] & 0xf;
65
+ result += HEX_CHARS[high];
66
+ result += HEX_CHARS[low];
67
+ }
68
+ return result;
69
+ }
70
+
71
+ // ============== Binary Node ==============
72
+
73
+ export interface BinaryNode {
74
+ tag: string;
75
+ attrs: Record<string, string>;
76
+ content?: string | Uint8Array | BinaryNode[];
77
+ }
78
+
79
+ // ============== Constants ==============
80
+
81
+ const NUM_DOUBLE_BYTE = 251; // 0xFB
82
+ const NUM_SINGLE_BYTE = 250; // 0xFA
83
+ const NUM_QUAD_BYTE = 252; // 0xFC
84
+
85
+ // ============== Encoder ==============
86
+
87
+ /**
88
+ * Binary encoder - mengubah BinaryNode ke Uint8Array
89
+ * Implementasi sesuai WA Web binary protocol
90
+ */
91
+ export class BinaryEncoder {
92
+ private buffer: number[] = [];
93
+
94
+ encode(node: BinaryNode): Uint8Array {
95
+ this.buffer = [];
96
+ this.writeNode(node);
97
+ return new Uint8Array(this.buffer);
98
+ }
99
+
100
+ private writeNode(node: BinaryNode): void {
101
+ if (!node) return;
102
+
103
+ const children = Array.isArray(node.content) ? node.content : [];
104
+ const hasContent = node.content !== undefined;
105
+ const numAttrs = Object.keys(node.attrs || {}).length;
106
+
107
+ // Write tag header
108
+ if (numAttrs === 0 && !hasContent) {
109
+ this.buffer.push(TAG_LIST.EMPTY);
110
+ } else if (numAttrs === 0) {
111
+ this.buffer.push(TAG_LIST.EMPTY | 0x80);
112
+ } else if (numAttrs < 256 && children.length < 256) {
113
+ this.buffer.push(TAG_LIST.EMPTY | (numAttrs << 4) | (children.length > 0 ? 0x80 : 0));
114
+ } else {
115
+ this.buffer.push(TAG_LIST_8);
116
+ this.buffer.push(numAttrs & 0xff);
117
+ this.buffer.push(children.length & 0xff);
118
+ }
119
+
120
+ // Write tag string (we assume tag is in dict, encode as packed nibble index)
121
+ this.writeString(node.tag);
122
+
123
+ // Write attributes
124
+ for (const [k, v] of Object.entries(node.attrs)) {
125
+ this.writeString(k);
126
+ this.writeString(v);
127
+ }
128
+
129
+ // Write content
130
+ if (children.length > 0) {
131
+ for (const child of children) {
132
+ if (typeof child === 'string') {
133
+ this.writeString(child);
134
+ } else if (child instanceof Uint8Array) {
135
+ this.writeBytes(child);
136
+ } else {
137
+ this.writeNode(child);
138
+ }
139
+ }
140
+ } else if (typeof node.content === 'string') {
141
+ this.writeString(node.content);
142
+ } else if (node.content instanceof Uint8Array) {
143
+ this.writeBytes(node.content);
144
+ }
145
+ }
146
+
147
+ private writeString(str: string): void {
148
+ if (str === '') {
149
+ this.buffer.push(TAG_LIST.EMPTY);
150
+ return;
151
+ }
152
+ // Try to find in dictionary
153
+ const dictIndex = findInDict(str);
154
+ if (dictIndex !== -1) {
155
+ const dict = dictIndex >> 8;
156
+ const idx = dictIndex & 0xff;
157
+ this.buffer.push(TAG_LIST.DICTIONARY_0 + dict);
158
+ this.buffer.push(idx);
159
+ return;
160
+ }
161
+ // Try nibble packing for hex/numeric strings
162
+ if (/^[0-9a-fA-F]+$/.test(str) && str.length > 1) {
163
+ this.buffer.push(TAG_PACKED_HEX);
164
+ this.writePackedString(str);
165
+ return;
166
+ }
167
+ if (/^[0-9]+$/.test(str) && str.length > 1) {
168
+ this.buffer.push(TAG_PACKED_NIBBLE);
169
+ this.writePackedString(str);
170
+ return;
171
+ }
172
+ // Default: raw bytes
173
+ this.writeBytes(new TextEncoder().encode(str));
174
+ }
175
+
176
+ private writePackedString(str: string): void {
177
+ // Pack nibbles - 2 char per byte, terminate with 0xf
178
+ const packed = packNibbles(str);
179
+ this.buffer.push(...packed);
180
+ // Terminator
181
+ this.buffer.push(0xf);
182
+ }
183
+
184
+ private writeBytes(bytes: Uint8Array): void {
185
+ const len = bytes.length;
186
+ if (len < 256) {
187
+ this.buffer.push(TAG_BINARY_8);
188
+ this.buffer.push(len);
189
+ } else if (len < 1048576) {
190
+ this.buffer.push(TAG_BINARY_20);
191
+ this.buffer.push((len >> 16) & 0xff);
192
+ this.buffer.push((len >> 8) & 0xff);
193
+ this.buffer.push(len & 0xff);
194
+ } else {
195
+ this.buffer.push(TAG_BINARY_32);
196
+ // 4 bytes big-endian
197
+ this.buffer.push((len >> 24) & 0xff);
198
+ this.buffer.push((len >> 16) & 0xff);
199
+ this.buffer.push((len >> 8) & 0xff);
200
+ this.buffer.push(len & 0xff);
201
+ }
202
+ for (let i = 0; i < bytes.length; i++) {
203
+ this.buffer.push(bytes[i]);
204
+ }
205
+ }
206
+ }
207
+
208
+ // ============== Decoder ==============
209
+
210
+ /**
211
+ * Binary decoder - parse WA binary protocol ke BinaryNode
212
+ */
213
+ export class BinaryDecoder {
214
+ private data!: Uint8Array;
215
+ private pos: number = 0;
216
+
217
+ decode(data: Uint8Array): BinaryNode {
218
+ this.data = data;
219
+ this.pos = 0;
220
+ return this.readNode();
221
+ }
222
+
223
+ private readNode(): BinaryNode {
224
+ const listFlag = (0x80 & this.readByte()) !== 0;
225
+ const numAttrs = 0x7f & this.readByte();
226
+
227
+ const tag = this.readString();
228
+ const attrs: Record<string, string> = {};
229
+ for (let i = 0; i < numAttrs; i++) {
230
+ const key = this.readString();
231
+ const value = this.readString();
232
+ attrs[key] = value;
233
+ }
234
+
235
+ let content: string | Uint8Array | BinaryNode[] | undefined;
236
+ if (listFlag) {
237
+ const children: BinaryNode[] = [];
238
+ while (this.pos < this.data.length) {
239
+ const peek = this.data[this.pos];
240
+ if (peek === 0) {
241
+ // End marker
242
+ this.pos++;
243
+ break;
244
+ }
245
+ // Determine type
246
+ if (peek === TAG_LIST_8 || peek === TAG_LIST_16) {
247
+ children.push(this.readNode());
248
+ } else {
249
+ // It's a string or bytes
250
+ const child = this.readChild();
251
+ if (typeof child === 'string') {
252
+ children.push({ tag: '__text__', attrs: {}, content: child } as any);
253
+ } else if (child instanceof Uint8Array) {
254
+ children.push({ tag: '__bytes__', attrs: {}, content: child } as any);
255
+ } else {
256
+ children.push(child);
257
+ }
258
+ }
259
+ }
260
+ content = children.length > 0 ? children : undefined;
261
+ } else {
262
+ // Single child or none
263
+ if (this.pos < this.data.length) {
264
+ const peek = this.data[this.pos];
265
+ if (peek === 0) {
266
+ this.pos++;
267
+ } else if (peek === TAG_LIST_8 || peek === TAG_LIST_16) {
268
+ content = [this.readNode()];
269
+ } else {
270
+ const child = this.readChild();
271
+ if (typeof child === 'string' || child instanceof Uint8Array) {
272
+ content = child;
273
+ } else {
274
+ content = [child];
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ return { tag, attrs, content };
281
+ }
282
+
283
+ private readChild(): string | Uint8Array | BinaryNode {
284
+ const peek = this.data[this.pos];
285
+ if (peek === TAG_LIST_8 || peek === TAG_LIST_16) {
286
+ return this.readNode();
287
+ }
288
+ return this.readString();
289
+ }
290
+
291
+ private readString(): string {
292
+ const tag = this.readByte();
293
+ if (tag === TAG_LIST.EMPTY) {
294
+ return '';
295
+ }
296
+ if (tag === TAG_LIST.DICTIONARY_0 ||
297
+ tag === TAG_LIST.DICTIONARY_1 ||
298
+ tag === TAG_LIST.DICTIONARY_2 ||
299
+ tag === TAG_LIST.DICTIONARY_3) {
300
+ const dictIndex = this.readByte();
301
+ return lookupDict(tag - TAG_LIST.DICTIONARY_0, dictIndex);
302
+ }
303
+ if (tag === TAG_PACKED_NIBBLE) {
304
+ return this.readPackedString(false);
305
+ }
306
+ if (tag === TAG_PACKED_HEX) {
307
+ return this.readPackedString(true);
308
+ }
309
+ if (tag === TAG_BINARY_8) {
310
+ const len = this.readByte();
311
+ return new TextDecoder().decode(this.readBytes(len));
312
+ }
313
+ if (tag === TAG_BINARY_20) {
314
+ const b1 = this.readByte();
315
+ const b2 = this.readByte();
316
+ const b3 = this.readByte();
317
+ const len = (b1 << 16) | (b2 << 8) | b3;
318
+ return new TextDecoder().decode(this.readBytes(len));
319
+ }
320
+ if (tag === TAG_BINARY_32) {
321
+ const b1 = this.readByte();
322
+ const b2 = this.readByte();
323
+ const b3 = this.readByte();
324
+ const b4 = this.readByte();
325
+ const len = (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
326
+ return new TextDecoder().decode(this.readBytes(len));
327
+ }
328
+ // Fall through - single byte char
329
+ return String.fromCharCode(tag);
330
+ }
331
+
332
+ private readPackedString(isHex: boolean): string {
333
+ const bytes: number[] = [];
334
+ while (this.pos < this.data.length) {
335
+ const b = this.data[this.pos++];
336
+ const high = (b >> 4) & 0xf;
337
+ const low = b & 0xf;
338
+ if (high === 0xf) break;
339
+ bytes.push(high);
340
+ if (low === 0xf) break;
341
+ bytes.push(low);
342
+ }
343
+ if (isHex) {
344
+ return bytes.map(b => HEX_CHARS[b]).join('');
345
+ }
346
+ // Nibble - numeric 0-9
347
+ return bytes.map(b => String(b)).join('');
348
+ }
349
+
350
+ private readBytes(n: number): Uint8Array {
351
+ const result = this.data.subarray(this.pos, this.pos + n);
352
+ this.pos += n;
353
+ return result;
354
+ }
355
+
356
+ private readByte(): number {
357
+ return this.data[this.pos++];
358
+ }
359
+ }
360
+
361
+ // ============== Dictionaries (Tokens) ==============
362
+
363
+ /**
364
+ * WhatsApp dictionary berisi string yang sering dipakai untuk kompresi.
365
+ * Encode sebagai dictionary index (1 byte) daripada string length + bytes.
366
+ *
367
+ * Note: dictionary disusun dari analisis WhatsApp Web JavaScript bundle.
368
+ * Total ~240 entries per dictionary (4 dictionaries), prefix common tags.
369
+ */
370
+ const DICTIONARIES: string[][] = [
371
+ // Dictionary 0 - common protocol tags
372
+ [
373
+ '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
374
+ '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
375
+ // index 32+ actual entries
376
+ 'edge_routing', 'account_sync', 'action', 'add', 'after', 'all', 'allow', 'available',
377
+ 'background', 'battery', 'before', 'broadcast', 'browser', 'call', 'cancel',
378
+ 'category', 'chat', 'code', 'commands', 'config', 'contacts', 'conversation',
379
+ 'count', 'create', 'debug', 'delete', 'demote', 'deny', 'device', 'devices',
380
+ 'disconnect', 'domain', 'download', 'edit', 'encrypt', 'error', 'expiration',
381
+ 'false', 'file', 'filename', 'first', 'from', 'g.us', 'group', 'groups',
382
+ 'history', 'id', 'identity', 'image', 'in', 'index', 'invite', 'iq',
383
+ 'jid', 'kind', 'last', 'length', 'level', 'list', 'location', 'manifest',
384
+ 'media', 'message', 'messages', 'method', 'migrate', 'modify', 'mute', 'name',
385
+ 'newsletter', 'next', 'nick', 'no', 'not', 'notify', 'off', 'offer',
386
+ 'offline', 'on', 'open', 'out', 'owner', 'pair', 'pairing', 'participant',
387
+ 'participants', 'payload', 'paused', 'permissions', 'phone', 'pin', 'platform', 'presence',
388
+ 'preview', 'primary', 'promote', 'protocol', 'push', 'query', 'rate', 'read',
389
+ 'reason', 'receipt', 'received', 'recording', 'reject', 'remove', 'request', 'response',
390
+ 'retry', 'revoke', 's.whatsapp.net', 'send', 'session', 'settings', 'sha256', 'signature',
391
+ 'since', 'status', 'subscribe', 'success', 'sync', 'tag', 'target', 'text',
392
+ 'timestamp', 'true', 'type', 'unblock', 'unavailable', 'unread', 'update', 'user',
393
+ 'verify', 'version', 'video', 'visible', 'voice', 'wait', 'warning', 'whatsapp',
394
+ 'ack', 'expire', 'image_message', 'video_message', 'audio_message', 'document_message',
395
+ 'sticker_message', 'location_message', 'contact_message', 'poll_message', 'server_sync',
396
+ 'business_hours', 'business_profile', 'catalog', 'label', 'order', 'product',
397
+ 'reaction', 'status_attendance', 'ephemeral', 'view_once', 'keep_in_chat',
398
+ 'unarchive', 'archive', 'mute', 'star', 'pin_v2'
399
+ ],
400
+ // Dictionary 1, 2, 3 - shorter/smaller for less common tags
401
+ [], [], []
402
+ ];
403
+
404
+ /**
405
+ * Find string in dictionary, return encoded index
406
+ * Returns -1 if not found
407
+ */
408
+ function findInDict(str: string): number {
409
+ for (let dict = 0; dict < DICTIONARIES.length; dict++) {
410
+ const idx = DICTIONARIES[dict].indexOf(str);
411
+ if (idx !== -1) {
412
+ return (dict << 8) | idx;
413
+ }
414
+ }
415
+ return -1;
416
+ }
417
+
418
+ /**
419
+ * Lookup dict by index
420
+ */
421
+ function lookupDict(dict: number, index: number): string {
422
+ return DICTIONARIES[dict]?.[index] ?? '';
423
+ }
424
+
425
+ // ============== Helpers ==============
426
+
427
+ export function encodeBinaryNode(node: BinaryNode): Uint8Array {
428
+ return new BinaryEncoder().encode(node);
429
+ }
430
+
431
+ export function decodeBinaryNode(data: Uint8Array): BinaryNode {
432
+ return new BinaryDecoder().decode(data);
433
+ }
434
+
435
+ /**
436
+ * Convert binary node to XML-like string for debugging
437
+ */
438
+ export function binaryNodeToString(node: BinaryNode, indent: number = 0): string {
439
+ const pad = ' '.repeat(indent);
440
+ let str = `${pad}<${node.tag}`;
441
+ for (const [k, v] of Object.entries(node.attrs)) {
442
+ str += ` ${k}="${v}"`;
443
+ }
444
+ if (!node.content) {
445
+ return str + ' />';
446
+ }
447
+ if (typeof node.content === 'string') {
448
+ return str + `>${node.content}</${node.tag}>`;
449
+ }
450
+ if (node.content instanceof Uint8Array) {
451
+ return str + `>[${node.content.length} bytes]</${node.tag}>`;
452
+ }
453
+ str += '>\n';
454
+ for (const child of node.content) {
455
+ if (typeof child === 'string') {
456
+ str += `${pad} ${child}\n`;
457
+ } else if (child instanceof Uint8Array) {
458
+ str += `${pad} [${child.length} bytes]\n`;
459
+ } else {
460
+ str += binaryNodeToString(child, indent + 1) + '\n';
461
+ }
462
+ }
463
+ return str + `${pad}</${node.tag}>`;
464
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Waliwa - Crypto utilities
3
+ * Hybrid approach: menggunakan @noble/curves & @noble/hashes untuk primitives
4
+ * Manual implementasi untuk WhatsApp-specific encoding (noise handshake, message framing)
5
+ *
6
+ * RAM optimization: gunakan Uint8Array (bukan Buffer) di mana memungkinkan untuk
7
+ * hindari overhead V8 Buffer pool.
8
+ */
9
+
10
+ import { x25519 } from '@noble/curves/ed25519';
11
+ import { sha256 } from '@noble/hashes/sha256';
12
+ import { sha512 } from '@noble/hashes/sha512';
13
+ import { hmac } from '@noble/hashes/hmac';
14
+ import { sha1 } from '@noble/hashes/sha1';
15
+ import { gcm } from '@noble/ciphers/aes';
16
+ import { randomBytes } from 'crypto';
17
+ import { hkdf } from '@noble/hashes/hkdf';
18
+
19
+ // ============== Curve25519 ==============
20
+
21
+ export interface KeyPair {
22
+ private: Uint8Array;
23
+ public: Uint8Array;
24
+ }
25
+
26
+ /**
27
+ * Generate Curve25519 key pair untuk Noise Protocol / Signal keys
28
+ * RAM-friendly: gunakan direct x25519 dari @noble/curves (no bindings).
29
+ */
30
+ export function generateCurveKeyPair(): KeyPair {
31
+ const privateKey = randomBytes(32);
32
+ const publicKey = x25519.getPublicKey(privateKey);
33
+ return { private: privateKey, public: publicKey };
34
+ }
35
+
36
+ /**
37
+ * X25519 ECDH - compute shared secret
38
+ */
39
+ export function curveSharedSecret(privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array {
40
+ return x25519.getSharedSecret(privateKey, publicKey);
41
+ }
42
+
43
+ /**
44
+ * Sign data dengan Ed25519 (untuk Signal signed pre-keys)
45
+ */
46
+ export function curveSign(privateKey: Uint8Array, data: Uint8Array): Uint8Array {
47
+ // Use ed25519 sign via noble - we use it for identity signing
48
+ const { ed25519 } = require('@noble/curves/ed25519');
49
+ return ed25519.sign(data, privateKey);
50
+ }
51
+
52
+ /**
53
+ * Verify Ed25519 signature
54
+ */
55
+ export function curveVerify(publicKey: Uint8Array, data: Uint8Array, signature: Uint8Array): boolean {
56
+ const { ed25519 } = require('@noble/curves/ed25519');
57
+ try {
58
+ return ed25519.verify(signature, data, publicKey);
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ // ============== Hashing ==============
65
+
66
+ export function sha256Hash(data: Uint8Array): Uint8Array {
67
+ return sha256(data);
68
+ }
69
+
70
+ export function sha512Hash(data: Uint8Array): Uint8Array {
71
+ return sha512(data);
72
+ }
73
+
74
+ export function sha1Hash(data: Uint8Array): Uint8Array {
75
+ return sha1(data);
76
+ }
77
+
78
+ export function hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array {
79
+ return hmac.create(sha256, key).update(data).digest();
80
+ }
81
+
82
+ export function hmacSha512(key: Uint8Array, data: Uint8Array): Uint8Array {
83
+ return hmac.create(sha512, key).update(data).digest();
84
+ }
85
+
86
+ // ============== HKDF (RFC 5869) ==============
87
+
88
+ /**
89
+ * HKDF - HMAC-based Key Derivation Function
90
+ * WhatsApp menggunakan HKDF extensively untuk derive keys dari shared secrets
91
+ */
92
+ export function hkdfDerive(
93
+ ikm: Uint8Array,
94
+ salt: Uint8Array,
95
+ info: Uint8Array,
96
+ length: number
97
+ ): Uint8Array {
98
+ const prk = hmacSha256(salt.length === 0 ? new Uint8Array(32) : salt, ikm);
99
+ return hkdf(sha256, prk, salt, info, length);
100
+ }
101
+
102
+ // ============== AES-256-GCM ==============
103
+
104
+ export interface AESCipher {
105
+ encrypt(plaintext: Uint8Array, iv: Uint8Array, aad?: Uint8Array): { ciphertext: Uint8Array; tag: Uint8Array };
106
+ decrypt(ciphertext: Uint8Array, iv: Uint8Array, tag: Uint8Array, aad?: Uint8Array): Uint8Array;
107
+ }
108
+
109
+ /**
110
+ * AES-256-GCM cipher (sebuah komponen inti WA message encryption)
111
+ */
112
+ export function createAESCipher(key: Uint8Array): AESCipher {
113
+ return {
114
+ encrypt(plaintext: Uint8Array, iv: Uint8Array, aad?: Uint8Array) {
115
+ const cipher = gcm(key, iv, aad);
116
+ const sealed = cipher.encrypt(plaintext);
117
+ // GCM output dari @noble: ciphertext + tag (16 byte) di akhir
118
+ const ciphertext = sealed.subarray(0, sealed.length - 16);
119
+ const tag = sealed.subarray(sealed.length - 16);
120
+ return { ciphertext, tag };
121
+ },
122
+ decrypt(ciphertext: Uint8Array, iv: Uint8Array, tag: Uint8Array, aad?: Uint8Array) {
123
+ const cipher = gcm(key, iv, aad);
124
+ const sealed = new Uint8Array(ciphertext.length + tag.length);
125
+ sealed.set(ciphertext, 0);
126
+ sealed.set(tag, ciphertext.length);
127
+ return cipher.decrypt(sealed);
128
+ }
129
+ };
130
+ }
131
+
132
+ // ============== AES-256-CBC (untuk media encryption) ==============
133
+
134
+ export function aesEncryptCBC(plaintext: Uint8Array, key: Uint8Array, iv: Uint8Array): Uint8Array {
135
+ // PKCS7 padding
136
+ const blockSize = 16;
137
+ const padLength = blockSize - (plaintext.length % blockSize);
138
+ const padded = new Uint8Array(plaintext.length + padLength);
139
+ padded.set(plaintext, 0);
140
+ for (let i = plaintext.length; i < padded.length; i++) {
141
+ padded[i] = padLength;
142
+ }
143
+ // Use Node crypto for CBC (Noble doesn't expose CBC)
144
+ const { createCipheriv } = require('crypto');
145
+ const cipher = createCipheriv('aes-256-cbc', Buffer.from(key), Buffer.from(iv));
146
+ return new Uint8Array(Buffer.concat([cipher.update(Buffer.from(padded)), cipher.final()]));
147
+ }
148
+
149
+ export function aesDecryptCBC(ciphertext: Uint8Array, key: Uint8Array, iv: Uint8Array): Uint8Array {
150
+ const { createDecipheriv } = require('crypto');
151
+ const decipher = createDecipheriv('aes-256-cbc', Buffer.from(key), Buffer.from(iv));
152
+ const padded = Buffer.concat([decipher.update(Buffer.from(ciphertext)), decipher.final()]);
153
+ // Remove PKCS7 padding
154
+ const padLength = padded[padded.length - 1];
155
+ return new Uint8Array(padded.subarray(0, padded.length - padLength));
156
+ }
157
+
158
+ // ============== Random ==============
159
+
160
+ export function getRandomBytes(length: number): Uint8Array {
161
+ return new Uint8Array(randomBytes(length));
162
+ }
163
+
164
+ // ============== Helpers ==============
165
+
166
+ export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
167
+ const total = arrays.reduce((sum, a) => sum + a.length, 0);
168
+ const result = new Uint8Array(total);
169
+ let offset = 0;
170
+ for (const arr of arrays) {
171
+ result.set(arr, offset);
172
+ offset += arr.length;
173
+ }
174
+ return result;
175
+ }
176
+
177
+ export function areBytesEqual(a: Uint8Array, b: Uint8Array): boolean {
178
+ if (a.length !== b.length) return false;
179
+ let diff = 0;
180
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
181
+ return diff === 0;
182
+ }
183
+
184
+ /**
185
+ * Constant-time comparison untuk hmac verification
186
+ */
187
+ export function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
188
+ return areBytesEqual(a, b);
189
+ }