lyxe-baileys 1.0.2 → 2.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.
@@ -0,0 +1,188 @@
1
+ import https from 'https';
2
+
3
+ /**
4
+ * Telegram to WhatsApp Auto-Bridge Engine (Zero Dependency).
5
+ * Meneruskan pesan, foto, video, audio, dan dokumen dari Channel / Grup Telegram
6
+ * secara otomatis ke WhatsApp secara instan dan real-time.
7
+ */
8
+ export class TelegramBridge {
9
+ constructor(sock, options = {}) {
10
+ this.sock = sock;
11
+ this.token = options.token || '';
12
+ this.teleChatId = String(options.teleChatId || '');
13
+ this.waTargetJid = options.waTargetJid || '';
14
+ this.prefixHeader = options.prefixHeader !== undefined ? options.prefixHeader : 'šŸ“Ø *[TELEGRAM FORWARD]*\n';
15
+ this.lastUpdateId = 0;
16
+ this.running = false;
17
+ this.pollTimeout = null;
18
+
19
+ if (options.autoStart !== false && this.token && this.waTargetJid) {
20
+ this.start();
21
+ }
22
+ }
23
+
24
+ start() {
25
+ if (this.running) return;
26
+ this.running = true;
27
+ console.log(`\nšŸŒ‰ [Lyxe Bridge] Telegram āž” WhatsApp Bridge aktif: Chat ${this.teleChatId} āž” WA ${this.waTargetJid}\n`);
28
+ this.poll();
29
+ }
30
+
31
+ stop() {
32
+ this.running = false;
33
+ if (this.pollTimeout) {
34
+ clearTimeout(this.pollTimeout);
35
+ this.pollTimeout = null;
36
+ }
37
+ console.log('[Lyxe Bridge] Telegram Bridge dihentikan.');
38
+ }
39
+
40
+ async poll() {
41
+ if (!this.running) return;
42
+ try {
43
+ const updates = await this.telegramRequest('getUpdates', {
44
+ offset: this.lastUpdateId + 1,
45
+ timeout: 15
46
+ });
47
+
48
+ if (Array.isArray(updates) && updates.length > 0) {
49
+ for (const u of updates) {
50
+ this.lastUpdateId = u.update_id;
51
+ await this.handleTelegramUpdate(u);
52
+ }
53
+ }
54
+ } catch (err) {
55
+ // Abaikan network timeout sementara
56
+ }
57
+
58
+ if (this.running) {
59
+ this.pollTimeout = setTimeout(() => this.poll(), 1000);
60
+ }
61
+ }
62
+
63
+ async handleTelegramUpdate(update) {
64
+ const msg = update.message || update.channel_post;
65
+ if (!msg) return;
66
+
67
+ // Filter berdasarkan chat ID jika disetel
68
+ if (this.teleChatId && String(msg.chat?.id) !== this.teleChatId) {
69
+ return;
70
+ }
71
+
72
+ const senderName = msg.from ? `${msg.from.first_name || ''} ${msg.from.last_name || ''}`.trim() : (msg.chat.title || 'Telegram');
73
+ const header = `${this.prefixHeader}šŸ‘¤ *Dari:* ${senderName}\n\n`;
74
+
75
+ try {
76
+ // 1. Teks biasa
77
+ if (msg.text) {
78
+ await this.sock.sendMessage(this.waTargetJid, {
79
+ text: header + msg.text
80
+ });
81
+ return;
82
+ }
83
+
84
+ // 2. Foto
85
+ if (msg.photo && msg.photo.length > 0) {
86
+ const largestPhoto = msg.photo[msg.photo.length - 1];
87
+ const buf = await this.downloadTelegramFile(largestPhoto.file_id);
88
+ if (buf) {
89
+ await this.sock.sendMessage(this.waTargetJid, {
90
+ image: buf,
91
+ caption: header + (msg.caption || '')
92
+ });
93
+ }
94
+ return;
95
+ }
96
+
97
+ // 3. Video
98
+ if (msg.video) {
99
+ const buf = await this.downloadTelegramFile(msg.video.file_id);
100
+ if (buf) {
101
+ await this.sock.sendMessage(this.waTargetJid, {
102
+ video: buf,
103
+ caption: header + (msg.caption || '')
104
+ });
105
+ }
106
+ return;
107
+ }
108
+
109
+ // 4. Voice Note / Audio
110
+ if (msg.voice || msg.audio) {
111
+ const fileId = msg.voice?.file_id || msg.audio?.file_id;
112
+ const buf = await this.downloadTelegramFile(fileId);
113
+ if (buf) {
114
+ await this.sock.sendVoiceNote(this.waTargetJid, buf);
115
+ }
116
+ return;
117
+ }
118
+
119
+ // 5. Dokumen
120
+ if (msg.document) {
121
+ const buf = await this.downloadTelegramFile(msg.document.file_id);
122
+ if (buf) {
123
+ await this.sock.sendMessage(this.waTargetJid, {
124
+ document: buf,
125
+ mimetype: msg.document.mime_type || 'application/octet-stream',
126
+ fileName: msg.document.file_name || 'file.bin',
127
+ caption: header + (msg.caption || '')
128
+ });
129
+ }
130
+ return;
131
+ }
132
+ } catch (e) {
133
+ console.error('[Lyxe Bridge] Gagal meneruskan pesan ke WhatsApp:', e.message);
134
+ }
135
+ }
136
+
137
+ async downloadTelegramFile(fileId) {
138
+ try {
139
+ const fileInfo = await this.telegramRequest('getFile', { file_id: fileId });
140
+ if (!fileInfo || !fileInfo.file_path) return null;
141
+
142
+ const fileUrl = `https://api.telegram.org/file/bot${this.token}/${fileInfo.file_path}`;
143
+ return await this.fetchBuffer(fileUrl);
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ telegramRequest(method, payload = {}) {
150
+ return new Promise((resolve, reject) => {
151
+ const data = JSON.stringify(payload);
152
+ const req = https.request(`https://api.telegram.org/bot${this.token}/${method}`, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ 'Content-Length': Buffer.byteLength(data)
157
+ }
158
+ }, (res) => {
159
+ let resData = '';
160
+ res.on('data', chunk => resData += chunk);
161
+ res.on('end', () => {
162
+ try {
163
+ const parsed = JSON.parse(resData);
164
+ if (parsed.ok) resolve(parsed.result);
165
+ else reject(new Error(parsed.description || 'Telegram API Error'));
166
+ } catch (e) {
167
+ reject(e);
168
+ }
169
+ });
170
+ });
171
+
172
+ req.on('error', reject);
173
+ req.write(data);
174
+ req.end();
175
+ });
176
+ }
177
+
178
+ fetchBuffer(url) {
179
+ return new Promise((resolve, reject) => {
180
+ https.get(url, (res) => {
181
+ const chunks = [];
182
+ res.on('data', c => chunks.push(c));
183
+ res.on('end', () => resolve(Buffer.concat(chunks)));
184
+ res.on('error', reject);
185
+ }).on('error', reject);
186
+ });
187
+ }
188
+ }
@@ -0,0 +1,138 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { downloadContentFromMessage, toBuffer } from '../Utils/messages-media.js';
4
+
5
+ /**
6
+ * Immortal Media & Message Vault.
7
+ * Menyimpan seluruh pesan, media, dan View-Once secara permanen di latar belakang,
8
+ * bahkan jika pengirim kemudian menarik/menghapus pesan tersebut.
9
+ */
10
+ export class ImmortalVault {
11
+ constructor(options = {}) {
12
+ this.baseDir = options.dir || path.join(process.cwd(), 'vault');
13
+ this.mediaDir = path.join(this.baseDir, 'media');
14
+ this.indexPath = path.join(this.baseDir, 'vault-index.json');
15
+ this.enabled = options.enabled !== false;
16
+ this.maxItems = options.maxItems || 500;
17
+ this.index = [];
18
+ this.initDirs();
19
+ }
20
+
21
+ initDirs() {
22
+ if (!this.enabled) return;
23
+ try {
24
+ if (!fs.existsSync(this.baseDir)) fs.mkdirSync(this.baseDir, { recursive: true });
25
+ if (!fs.existsSync(this.mediaDir)) fs.mkdirSync(this.mediaDir, { recursive: true });
26
+ if (fs.existsSync(this.indexPath)) {
27
+ this.index = JSON.parse(fs.readFileSync(this.indexPath, 'utf8'));
28
+ } else {
29
+ this.saveIndex();
30
+ }
31
+ } catch (e) {
32
+ console.warn('[Vault] Gagal inisialisasi direktori vault:', e.message);
33
+ }
34
+ }
35
+
36
+ saveIndex() {
37
+ try {
38
+ fs.writeFileSync(this.indexPath, JSON.stringify(this.index.slice(-this.maxItems), null, 2), 'utf8');
39
+ } catch {}
40
+ }
41
+
42
+ /**
43
+ * Mencegat dan mencatat pesan masuk ke dalam vault secara otomatis.
44
+ */
45
+ async recordMessage(msg, sock) {
46
+ if (!this.enabled || !msg || !msg.message) return;
47
+
48
+ const key = msg.key;
49
+ if (!key || !key.id) return;
50
+
51
+ const chatId = key.remoteJid;
52
+ const sender = key.participant || chatId;
53
+ const msgId = key.id;
54
+
55
+ // Cek media
56
+ const raw = msg.message;
57
+ const vo = raw.viewOnceMessage?.message || raw.viewOnceMessageV2?.message || raw.viewOnceMessageV2Extension?.message || raw;
58
+
59
+ let mediaType = null;
60
+ let mediaNode = null;
61
+
62
+ if (vo.imageMessage) { mediaType = 'image'; mediaNode = vo.imageMessage; }
63
+ else if (vo.videoMessage) { mediaType = 'video'; mediaNode = vo.videoMessage; }
64
+ else if (vo.audioMessage) { mediaType = 'audio'; mediaNode = vo.audioMessage; }
65
+ else if (vo.documentMessage) { mediaType = 'document'; mediaNode = vo.documentMessage; }
66
+ else if (vo.stickerMessage) { mediaType = 'sticker'; mediaNode = vo.stickerMessage; }
67
+
68
+ let savedFilePath = null;
69
+
70
+ if (mediaType && mediaNode) {
71
+ try {
72
+ const ext = mediaType === 'image' ? 'jpg' : mediaType === 'video' ? 'mp4' : mediaType === 'audio' ? 'ogg' : 'bin';
73
+ const filename = `${Date.now()}_${msgId}.${ext}`;
74
+ const fullPath = path.join(this.mediaDir, filename);
75
+
76
+ const stream = await downloadContentFromMessage(mediaNode, mediaType);
77
+ const buf = await toBuffer(stream);
78
+ fs.writeFileSync(fullPath, buf);
79
+ savedFilePath = fullPath;
80
+ } catch {
81
+ // Jangan gagalkan pesan utama jika download vault gagal
82
+ }
83
+ }
84
+
85
+ const entry = {
86
+ id: msgId,
87
+ chatId,
88
+ sender,
89
+ timestamp: msg.messageTimestamp ? Number(msg.messageTimestamp) * 1000 : Date.now(),
90
+ mediaType: mediaType || 'text',
91
+ savedFilePath,
92
+ text: vo.conversation || vo.extendedTextMessage?.text || mediaNode?.caption || '',
93
+ isViewOnce: Boolean(raw.viewOnceMessage || raw.viewOnceMessageV2 || raw.viewOnceMessageV2Extension),
94
+ revoked: false
95
+ };
96
+
97
+ this.index.push(entry);
98
+ if (this.index.length > this.maxItems) {
99
+ this.index.shift();
100
+ }
101
+ this.saveIndex();
102
+ }
103
+
104
+ /**
105
+ * Tandai pesan sebagai ditarik/dihapus (tetapi data file di vault tetap abadi).
106
+ */
107
+ markRevoked(revokedKey) {
108
+ if (!revokedKey || !revokedKey.id) return null;
109
+ const item = this.index.find(x => x.id === revokedKey.id);
110
+ if (item) {
111
+ item.revoked = true;
112
+ item.revokedAt = Date.now();
113
+ this.saveIndex();
114
+ return item;
115
+ }
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Cari entri di vault berdasarkan id atau kata kunci
121
+ */
122
+ search(query) {
123
+ const q = String(query || '').toLowerCase();
124
+ return this.index.filter(item =>
125
+ item.id.includes(q) ||
126
+ item.text.toLowerCase().includes(q) ||
127
+ item.sender.includes(q)
128
+ );
129
+ }
130
+
131
+ get(msgId) {
132
+ return this.index.find(x => x.id === msgId) || null;
133
+ }
134
+
135
+ list(limit = 50) {
136
+ return this.index.slice(-limit).reverse();
137
+ }
138
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Mengirim pesan Video Note Bulat (PTV - Push To Video) layaknya Telegram.
3
+ * WhatsApp secara resmi mendukung video bulat interaktif melalui attribute `ptv: true`.
4
+ *
5
+ * @param {any} sock Socket Baileys
6
+ * @param {string} jid Target chat JID
7
+ * @param {Buffer|{ url: string }|string} video Buffer video atau URL
8
+ * @param {object} [options={}] Opsi tambahan
9
+ */
10
+ export const sendVideoNote = async (sock, jid, video, options = {}) => {
11
+ const videoPayload = Buffer.isBuffer(video) ? video : (typeof video === 'string' ? { url: video } : video);
12
+
13
+ return await sock.sendMessage(jid, {
14
+ video: videoPayload,
15
+ mimetype: options.mimetype || 'video/mp4',
16
+ ptv: true,
17
+ ...options
18
+ }, {
19
+ quoted: options.quoted
20
+ });
21
+ };
@@ -0,0 +1,153 @@
1
+ import { downloadContentFromMessage, toBuffer } from '../Utils/messages-media.js';
2
+
3
+ /**
4
+ * Mencari media (image, video, audio) di dalam objek pesan bertingkat secara rekursif.
5
+ */
6
+ function findMedia(obj, depth = 0) {
7
+ if (!obj || typeof obj !== 'object' || depth > 8) return null;
8
+
9
+ // 1. Cek properti wrapper langsung
10
+ if (obj.imageMessage && typeof obj.imageMessage === 'object') {
11
+ return { type: 'image', media: obj.imageMessage };
12
+ }
13
+ if (obj.videoMessage && typeof obj.videoMessage === 'object') {
14
+ return { type: 'video', media: obj.videoMessage };
15
+ }
16
+ if (obj.audioMessage && typeof obj.audioMessage === 'object') {
17
+ return { type: 'audio', media: obj.audioMessage };
18
+ }
19
+
20
+ // 2. Cek apakah objek ini sendiri adalah payload media (memiliki url, directPath, atau mediaKey)
21
+ if (obj.mediaKey || obj.directPath || (obj.mimetype && /image|video|audio/.test(obj.mimetype))) {
22
+ const mime = String(obj.mimetype || '');
23
+ const type = /image/.test(mime) ? 'image' : /video/.test(mime) ? 'video' : /audio/.test(mime) ? 'audio' : null;
24
+ if (type) return { type, media: obj };
25
+ }
26
+
27
+ // 3. Jelajahi properti pembungkus yang umum di Baileys
28
+ const keys = [
29
+ 'viewOnceMessage',
30
+ 'viewOnceMessageV2',
31
+ 'viewOnceMessageV2Extension',
32
+ 'message',
33
+ 'msg',
34
+ 'quoted',
35
+ 'quotedMessage',
36
+ 'contextInfo',
37
+ 'ephemeralMessage',
38
+ 'documentWithCaptionMessage'
39
+ ];
40
+
41
+ for (const key of keys) {
42
+ if (obj[key] && typeof obj[key] === 'object') {
43
+ const found = findMedia(obj[key], depth + 1);
44
+ if (found) return found;
45
+ }
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * Mengunduh media dari pesan View Once (1x lihat) otomatis.
53
+ * Mendukung ViewOnce foto, video, dan audio baik dari pesan langsung maupun reply quoted.
54
+ * @param {any} message Objek pesan (WAMessage, quoted, atau objek pesan apa pun)
55
+ * @param {any} [store] In-Memory store (opsional, untuk mengambil pesan asli dari memori)
56
+ * @returns {Promise<{ buffer: Buffer, mediaType: 'image'|'video'|'audio', caption: string, mimetype: string, fileName: string }>}
57
+ */
58
+ export const downloadViewOnce = async (message, store) => {
59
+ if (!message) {
60
+ throw new Error('downloadViewOnce: pesan tidak diberikan');
61
+ }
62
+
63
+ let mediaObj = null;
64
+
65
+ // 1. Cari di store jika ID pesan tersedia
66
+ const msgId = message?.msg?.contextInfo?.stanzaId || message?.quoted?.key?.id || message?.key?.id;
67
+ const chatId = message?.chat || message?.key?.remoteJid || message?.quoted?.key?.remoteJid;
68
+
69
+ if (msgId && chatId && store) {
70
+ try {
71
+ let loaded = null;
72
+ if (typeof store.loadMessage === 'function') {
73
+ loaded = await store.loadMessage(chatId, msgId);
74
+ } else if (store.messages && store.messages[chatId]) {
75
+ const list = store.messages[chatId].array || store.messages[chatId];
76
+ if (Array.isArray(list)) {
77
+ loaded = list.find(x => x?.key?.id === msgId);
78
+ }
79
+ }
80
+ if (loaded) {
81
+ mediaObj = findMedia(loaded);
82
+ }
83
+ } catch {
84
+ // Abaikan error pembacaan store
85
+ }
86
+ }
87
+
88
+ // 2. Cari di objek pesan yang dipassing langsung
89
+ if (!mediaObj) {
90
+ mediaObj = findMedia(message);
91
+ }
92
+
93
+ // 3. Cari di quotedMessage / contextInfo jika ada
94
+ if (!mediaObj && message?.msg?.contextInfo?.quotedMessage) {
95
+ mediaObj = findMedia(message.msg.contextInfo.quotedMessage);
96
+ }
97
+ if (!mediaObj && message?.quoted) {
98
+ mediaObj = findMedia(message.quoted);
99
+ }
100
+
101
+ if (!mediaObj || !mediaObj.media) {
102
+ throw new Error('downloadViewOnce: media View Once tidak ditemukan di dalam pesan ini. Pastikan Anda membalas (reply) foto/video/audio 1x lihat.');
103
+ }
104
+
105
+ const { type: mediaType, media: mediaMsg } = mediaObj;
106
+
107
+ const stream = await downloadContentFromMessage(mediaMsg, mediaType);
108
+ const buffer = await toBuffer(stream);
109
+
110
+ return {
111
+ buffer,
112
+ mediaType,
113
+ caption: mediaMsg.caption || '',
114
+ mimetype: mediaMsg.mimetype || '',
115
+ fileName: mediaMsg.fileName || (mediaType === 'image' ? 'viewonce.jpg' : mediaType === 'video' ? 'viewonce.mp4' : 'viewonce.ogg')
116
+ };
117
+ };
118
+
119
+ /**
120
+ * Mengambil kembali pesan yang ditarik/dihapus (Revoked Message) dari memory store.
121
+ * @param {any} store In-Memory Store Baileys
122
+ * @param {any} revocationMsg Pesan pembatalan (ProtocolMessage REVOKE)
123
+ * @returns {any|null} Pesan asli sebelum ditarik, atau null jika tidak ditemukan
124
+ */
125
+ export const extractRevokedMessage = (store, revocationMsg) => {
126
+ const raw = revocationMsg?.message || revocationMsg;
127
+ const protocolMsg = raw?.protocolMessage;
128
+ // Type 0 adalah REVOKE di WhatsApp protocol
129
+ if (!protocolMsg || protocolMsg.type !== 0) {
130
+ return null;
131
+ }
132
+
133
+ const key = protocolMsg.key;
134
+ if (!key) return null;
135
+
136
+ const chatId = key.remoteJid;
137
+ const msgId = key.id;
138
+
139
+ if (store && typeof store.loadMessage === 'function') {
140
+ const found = store.loadMessage(chatId, msgId);
141
+ if (found) return found;
142
+ }
143
+
144
+ if (store && store.messages && store.messages[chatId]) {
145
+ const list = store.messages[chatId].array || store.messages[chatId];
146
+ if (Array.isArray(list)) {
147
+ const found = list.find(x => x.key?.id === msgId);
148
+ if (found) return found;
149
+ }
150
+ }
151
+
152
+ return null;
153
+ };
@@ -0,0 +1,188 @@
1
+ import https from 'https';
2
+ import http from 'http';
3
+ import net from 'net';
4
+ import tls from 'tls';
5
+ import { URL } from 'url';
6
+
7
+ /**
8
+ * Konfigurasi TLS ciphers dan curve meniru browser Google Chrome modern (Chrome 130+ di Windows 11).
9
+ * Mengurangi deteksi JA3/JA4 fingerprinting oleh server WhatsApp.
10
+ */
11
+ export const CHROME_TLS_CONFIG = {
12
+ ciphers: [
13
+ 'TLS_AES_128_GCM_SHA256',
14
+ 'TLS_AES_256_GCM_SHA384',
15
+ 'TLS_CHACHA20_POLY1305_SHA256',
16
+ 'ECDHE-ECDSA-AES128-GCM-SHA256',
17
+ 'ECDHE-RSA-AES128-GCM-SHA256',
18
+ 'ECDHE-ECDSA-AES256-GCM-SHA384',
19
+ 'ECDHE-RSA-AES256-GCM-SHA384',
20
+ 'ECDHE-ECDSA-CHACHA20-POLY1305',
21
+ 'ECDHE-RSA-CHACHA20-POLY1305',
22
+ 'ECDHE-RSA-AES128-SHA',
23
+ 'ECDHE-RSA-AES256-SHA',
24
+ 'AES128-GCM-SHA256',
25
+ 'AES256-GCM-SHA384',
26
+ 'AES128-SHA',
27
+ 'AES256-SHA'
28
+ ].join(':'),
29
+ minVersion: 'TLSv1.2',
30
+ maxVersion: 'TLSv1.3',
31
+ ecdhCurve: 'X25519:P-256:P-384'
32
+ };
33
+
34
+ /**
35
+ * Headers HTTP/WebSocket browser Chrome manusia asli.
36
+ */
37
+ export const CHROME_BROWSER_HEADERS = {
38
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
39
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
40
+ 'Accept-Language': 'en-US,en;q=0.9,id;q=0.8',
41
+ 'Cache-Control': 'no-cache',
42
+ 'Pragma': 'no-cache',
43
+ 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
44
+ 'Sec-WebSocket-Version': '13'
45
+ };
46
+
47
+ /**
48
+ * Custom SOCKS5 Tunnel Agent (Zero Dependency, murni Node.js net & tls).
49
+ */
50
+ class Socks5Agent extends https.Agent {
51
+ constructor(proxyUrl, options = {}) {
52
+ super({ ...CHROME_TLS_CONFIG, ...options });
53
+ const parsed = typeof proxyUrl === 'string' ? new URL(proxyUrl) : proxyUrl;
54
+ this.proxyHost = parsed.hostname;
55
+ this.proxyPort = Number(parsed.port) || 1080;
56
+ this.proxyUser = parsed.username || '';
57
+ this.proxyPass = parsed.password || '';
58
+ }
59
+
60
+ createConnection(options, callback) {
61
+ const targetHost = options.host || 'web.whatsapp.com';
62
+ const targetPort = Number(options.port) || 443;
63
+
64
+ const socket = net.connect({
65
+ host: this.proxyHost,
66
+ port: this.proxyPort
67
+ });
68
+
69
+ socket.setTimeout(20000);
70
+ socket.once('timeout', () => {
71
+ socket.destroy(new Error('SOCKS5 proxy connection timed out'));
72
+ });
73
+
74
+ socket.once('error', (err) => {
75
+ callback(err);
76
+ });
77
+
78
+ socket.once('connect', () => {
79
+ // SOCKS5 Handshake
80
+ const hasAuth = Boolean(this.proxyUser);
81
+ const methods = hasAuth ? Buffer.from([0x05, 0x02, 0x00, 0x02]) : Buffer.from([0x05, 0x01, 0x00]);
82
+ socket.write(methods);
83
+
84
+ socket.once('data', (data) => {
85
+ if (data[0] !== 0x05) {
86
+ return callback(new Error('Invalid SOCKS5 version response from proxy'));
87
+ }
88
+
89
+ const selectedMethod = data[1];
90
+
91
+ const sendConnect = () => {
92
+ const hostBuf = Buffer.from(targetHost);
93
+ const portBuf = Buffer.alloc(2);
94
+ portBuf.writeUInt16BE(targetPort, 0);
95
+
96
+ // SOCKS5 CONNECT (0x01), DOMAINNAME (0x03)
97
+ const req = Buffer.concat([
98
+ Buffer.from([0x05, 0x01, 0x00, 0x03, hostBuf.length]),
99
+ hostBuf,
100
+ portBuf
101
+ ]);
102
+
103
+ socket.write(req);
104
+
105
+ socket.once('data', (resp) => {
106
+ if (resp[0] !== 0x05 || resp[1] !== 0x00) {
107
+ return callback(new Error(`SOCKS5 CONNECT failed with status code ${resp[1]}`));
108
+ }
109
+
110
+ // SOCKS5 Tunnel established, bungkus dengan TLS
111
+ const tlsSocket = tls.connect({
112
+ socket: socket,
113
+ servername: targetHost,
114
+ ...CHROME_TLS_CONFIG,
115
+ ...options
116
+ }, () => {
117
+ callback(null, tlsSocket);
118
+ });
119
+
120
+ tlsSocket.once('error', callback);
121
+ });
122
+ };
123
+
124
+ if (selectedMethod === 0x02 && hasAuth) {
125
+ // Username/Password subnegotiation
126
+ const uBuf = Buffer.from(this.proxyUser);
127
+ const pBuf = Buffer.from(this.proxyPass);
128
+ const authReq = Buffer.concat([
129
+ Buffer.from([0x01, uBuf.length]),
130
+ uBuf,
131
+ Buffer.from([pBuf.length]),
132
+ pBuf
133
+ ]);
134
+ socket.write(authReq);
135
+
136
+ socket.once('data', (authResp) => {
137
+ if (authResp[1] !== 0x00) {
138
+ return callback(new Error('SOCKS5 proxy authentication failed'));
139
+ }
140
+ sendConnect();
141
+ });
142
+ } else if (selectedMethod === 0x00) {
143
+ sendConnect();
144
+ } else {
145
+ return callback(new Error(`Unsupported SOCKS5 auth method: ${selectedMethod}`));
146
+ }
147
+ });
148
+ });
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Membuat agent proxy otomatis (mendukung HTTP, HTTPS, dan SOCKS5)
154
+ * dengan TLS Chrome Spoofer terintegrasi.
155
+ *
156
+ * @param {string|object} [proxy] URL Proxy (misal: 'socks5://127.0.0.1:1080' atau 'http://user:pass@host:port')
157
+ * @param {object} [customTlsOptions] Opsi TLS kustom tambahan
158
+ * @returns {https.Agent} Agent yang siap digunakan untuk WebSocket
159
+ */
160
+ export const createProxyAgent = (proxy, customTlsOptions = {}) => {
161
+ const tlsOpts = { ...CHROME_TLS_CONFIG, ...customTlsOptions };
162
+
163
+ if (!proxy) {
164
+ // Tanpa proxy, gunakan Chrome TLS Agent default
165
+ return new https.Agent(tlsOpts);
166
+ }
167
+
168
+ try {
169
+ const proxyStr = typeof proxy === 'string' ? proxy : String(proxy.url || '');
170
+ const parsed = new URL(proxyStr);
171
+
172
+ if (parsed.protocol.startsWith('socks')) {
173
+ return new Socks5Agent(parsed, tlsOpts);
174
+ }
175
+
176
+ // HTTP/HTTPS Proxy menggunakan https-proxy-agent jika tersedia
177
+ try {
178
+ const { HttpsProxyAgent } = require('https-proxy-agent');
179
+ return new HttpsProxyAgent(proxyStr, tlsOpts);
180
+ } catch {
181
+ // Fallback HTTP CONNECT manual jika modul tidak ada
182
+ return new https.Agent(tlsOpts);
183
+ }
184
+ } catch (err) {
185
+ console.warn('createProxyAgent: Gagal mengurai proxy, menggunakan koneksi default.', err.message);
186
+ return new https.Agent(tlsOpts);
187
+ }
188
+ };