@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.
- package/LICENSE +21 -0
- package/README.md +600 -0
- package/dist/advanced/features.d.ts +211 -0
- package/dist/advanced/features.d.ts.map +1 -0
- package/dist/advanced/features.js +671 -0
- package/dist/advanced/features.js.map +1 -0
- package/dist/auth/auth-state.d.ts +89 -0
- package/dist/auth/auth-state.d.ts.map +1 -0
- package/dist/auth/auth-state.js +330 -0
- package/dist/auth/auth-state.js.map +1 -0
- package/dist/auth/pairing-code.d.ts +78 -0
- package/dist/auth/pairing-code.d.ts.map +1 -0
- package/dist/auth/pairing-code.js +191 -0
- package/dist/auth/pairing-code.js.map +1 -0
- package/dist/auth/qr-code.d.ts +60 -0
- package/dist/auth/qr-code.d.ts.map +1 -0
- package/dist/auth/qr-code.js +151 -0
- package/dist/auth/qr-code.js.map +1 -0
- package/dist/calls/handler.d.ts +61 -0
- package/dist/calls/handler.d.ts.map +1 -0
- package/dist/calls/handler.js +117 -0
- package/dist/calls/handler.js.map +1 -0
- package/dist/core/binary.d.ts +74 -0
- package/dist/core/binary.d.ts.map +1 -0
- package/dist/core/binary.js +448 -0
- package/dist/core/binary.js.map +1 -0
- package/dist/core/crypto.d.ts +60 -0
- package/dist/core/crypto.d.ts.map +1 -0
- package/dist/core/crypto.js +170 -0
- package/dist/core/crypto.js.map +1 -0
- package/dist/core/noise.d.ts +106 -0
- package/dist/core/noise.d.ts.map +1 -0
- package/dist/core/noise.js +307 -0
- package/dist/core/noise.js.map +1 -0
- package/dist/events/emitter.d.ts +44 -0
- package/dist/events/emitter.d.ts.map +1 -0
- package/dist/events/emitter.js +79 -0
- package/dist/events/emitter.js.map +1 -0
- package/dist/features/index.d.ts +342 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +755 -0
- package/dist/features/index.js.map +1 -0
- package/dist/fixes/index.d.ts +333 -0
- package/dist/fixes/index.d.ts.map +1 -0
- package/dist/fixes/index.js +762 -0
- package/dist/fixes/index.js.map +1 -0
- package/dist/groups/management.d.ts +86 -0
- package/dist/groups/management.d.ts.map +1 -0
- package/dist/groups/management.js +443 -0
- package/dist/groups/management.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +236 -0
- package/dist/index.js.map +1 -0
- package/dist/messages/media.d.ts +93 -0
- package/dist/messages/media.d.ts.map +1 -0
- package/dist/messages/media.js +252 -0
- package/dist/messages/media.js.map +1 -0
- package/dist/messages/message-encoder.d.ts +70 -0
- package/dist/messages/message-encoder.d.ts.map +1 -0
- package/dist/messages/message-encoder.js +453 -0
- package/dist/messages/message-encoder.js.map +1 -0
- package/dist/messages/send.d.ts +101 -0
- package/dist/messages/send.d.ts.map +1 -0
- package/dist/messages/send.js +409 -0
- package/dist/messages/send.js.map +1 -0
- package/dist/recovery/index.d.ts +125 -0
- package/dist/recovery/index.d.ts.map +1 -0
- package/dist/recovery/index.js +584 -0
- package/dist/recovery/index.js.map +1 -0
- package/dist/skdm/index.d.ts +220 -0
- package/dist/skdm/index.d.ts.map +1 -0
- package/dist/skdm/index.js +600 -0
- package/dist/skdm/index.js.map +1 -0
- package/dist/socket/hybrid.d.ts +118 -0
- package/dist/socket/hybrid.d.ts.map +1 -0
- package/dist/socket/hybrid.js +352 -0
- package/dist/socket/hybrid.js.map +1 -0
- package/dist/socket/wa-socket.d.ts +300 -0
- package/dist/socket/wa-socket.d.ts.map +1 -0
- package/dist/socket/wa-socket.js +1094 -0
- package/dist/socket/wa-socket.js.map +1 -0
- package/dist/socket/ws-socket.d.ts +96 -0
- package/dist/socket/ws-socket.d.ts.map +1 -0
- package/dist/socket/ws-socket.js +302 -0
- package/dist/socket/ws-socket.js.map +1 -0
- package/dist/types/index.d.ts +444 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/jid.d.ts +46 -0
- package/dist/utils/jid.d.ts.map +1 -0
- package/dist/utils/jid.js +152 -0
- package/dist/utils/jid.js.map +1 -0
- package/dist/utils/logger.d.ts +45 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +79 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/retry.d.ts +43 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +175 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/API.md +745 -0
- package/docs/ARCHITECTURE.md +307 -0
- package/docs/BAILEYS_FIXES.md +360 -0
- package/docs/FEATURES.md +532 -0
- package/docs/PROTOCOL.md +489 -0
- package/docs/RECOVERY.md +409 -0
- package/docs/SKDM.md +233 -0
- package/examples/ai-bot/index.ts +479 -0
- package/examples/ai-bot/package.json +17 -0
- package/examples/echo-bot/index.ts +171 -0
- package/examples/echo-bot/package.json +18 -0
- package/examples/group-bot/index.ts +557 -0
- package/examples/group-bot/package.json +17 -0
- package/examples/rest-gateway/index.ts +499 -0
- package/examples/rest-gateway/package.json +19 -0
- package/package.json +75 -0
- package/src/advanced/features.ts +817 -0
- package/src/auth/auth-state.ts +342 -0
- package/src/auth/pairing-code.ts +246 -0
- package/src/auth/qr-code.ts +191 -0
- package/src/calls/handler.ts +153 -0
- package/src/core/binary.ts +464 -0
- package/src/core/crypto.ts +189 -0
- package/src/core/noise.ts +406 -0
- package/src/events/emitter.ts +88 -0
- package/src/features/index.ts +921 -0
- package/src/fixes/index.ts +882 -0
- package/src/groups/management.ts +497 -0
- package/src/index.ts +274 -0
- package/src/messages/media.ts +372 -0
- package/src/messages/message-encoder.ts +520 -0
- package/src/messages/send.ts +521 -0
- package/src/recovery/index.ts +704 -0
- package/src/skdm/index.ts +693 -0
- package/src/socket/hybrid.ts +414 -0
- package/src/socket/wa-socket.ts +1347 -0
- package/src/socket/ws-socket.ts +355 -0
- package/src/types/index.ts +457 -0
- package/src/utils/jid.ts +156 -0
- package/src/utils/logger.ts +84 -0
- package/src/utils/retry.ts +205 -0
- package/tsconfig.json +30 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waliwa - Lightweight WhatsApp library dengan hybrid protocol
|
|
3
|
+
*
|
|
4
|
+
* Lebih lengkap dari Baileys, lebih ringan dari ZapoJS, lebih hemat RAM.
|
|
5
|
+
* Menggunakan WhatsApp Web Multi-Device + Mobile API pairing.
|
|
6
|
+
*
|
|
7
|
+
* @example Basic usage
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { makeWASocket, useFileAuthState } from 'waliwa';
|
|
10
|
+
*
|
|
11
|
+
* const { state, saveCreds } = await useFileAuthState('./auth');
|
|
12
|
+
* const sock = makeWASocket({ authState: state, printQRInTerminal: true });
|
|
13
|
+
*
|
|
14
|
+
* sock.ev.on('connection.update', ({ connection }) => {
|
|
15
|
+
* if (connection === 'open') console.log('Connected!');
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* sock.ev.on('message.upsert', async ({ messages }) => {
|
|
19
|
+
* for (const msg of messages) {
|
|
20
|
+
* if (!msg.key.fromMe && msg.message?.text) {
|
|
21
|
+
* await sock.sendMessage(msg.key.remoteJid, { text: 'Hello!' });
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// ============== Main Socket ==============
|
|
29
|
+
export { makeWASocket, WASocketImpl, WASocket } from './socket/wa-socket.js';
|
|
30
|
+
export type { WASocket as WASocketType } from './socket/wa-socket.js';
|
|
31
|
+
|
|
32
|
+
// ============== Hybrid Socket ==============
|
|
33
|
+
export { HybridSocketManager, MobileAPISocket } from './socket/hybrid.js';
|
|
34
|
+
|
|
35
|
+
// ============== Auth State ==============
|
|
36
|
+
export { FileAuthState, useFileAuthState, createDefaultCreds } from './auth/auth-state.js';
|
|
37
|
+
|
|
38
|
+
// ============== QR Code ==============
|
|
39
|
+
export {
|
|
40
|
+
generateQRCodeData,
|
|
41
|
+
buildQRString,
|
|
42
|
+
parseQRString,
|
|
43
|
+
QRRefreshManager
|
|
44
|
+
} from './auth/qr-code.js';
|
|
45
|
+
|
|
46
|
+
// ============== Pairing Code ==============
|
|
47
|
+
export {
|
|
48
|
+
generatePairingCode,
|
|
49
|
+
normalizePairingCode,
|
|
50
|
+
isValidPairingCodeFormat,
|
|
51
|
+
normalizePhoneNumber,
|
|
52
|
+
buildPairingRequestNode,
|
|
53
|
+
parsePairingResult,
|
|
54
|
+
PairingCodeExpiry
|
|
55
|
+
} from './auth/pairing-code.js';
|
|
56
|
+
|
|
57
|
+
// ============== Core Crypto ==============
|
|
58
|
+
export {
|
|
59
|
+
generateCurveKeyPair,
|
|
60
|
+
curveSharedSecret,
|
|
61
|
+
curveSign,
|
|
62
|
+
curveVerify,
|
|
63
|
+
sha256Hash,
|
|
64
|
+
sha512Hash,
|
|
65
|
+
sha1Hash,
|
|
66
|
+
hmacSha256,
|
|
67
|
+
hmacSha512,
|
|
68
|
+
hkdfDerive,
|
|
69
|
+
createAESCipher,
|
|
70
|
+
aesEncryptCBC,
|
|
71
|
+
aesDecryptCBC,
|
|
72
|
+
getRandomBytes,
|
|
73
|
+
concatBytes,
|
|
74
|
+
areBytesEqual,
|
|
75
|
+
constantTimeEqual,
|
|
76
|
+
type KeyPair,
|
|
77
|
+
type AESCipher
|
|
78
|
+
} from './core/crypto.js';
|
|
79
|
+
|
|
80
|
+
// ============== Noise Protocol ==============
|
|
81
|
+
export {
|
|
82
|
+
NoiseHandshake,
|
|
83
|
+
TrafficCipher,
|
|
84
|
+
buildClientPayload
|
|
85
|
+
} from './core/noise.js';
|
|
86
|
+
|
|
87
|
+
// ============== Binary Protocol ==============
|
|
88
|
+
export {
|
|
89
|
+
BinaryEncoder,
|
|
90
|
+
BinaryDecoder,
|
|
91
|
+
encodeBinaryNode,
|
|
92
|
+
decodeBinaryNode,
|
|
93
|
+
binaryNodeToString,
|
|
94
|
+
packNibbles,
|
|
95
|
+
unpackNibbles,
|
|
96
|
+
TAG_LIST,
|
|
97
|
+
type BinaryNode
|
|
98
|
+
} from './core/binary.js';
|
|
99
|
+
|
|
100
|
+
// ============== WebSocket Layer ==============
|
|
101
|
+
export { WAWebSocket } from './socket/ws-socket.js';
|
|
102
|
+
export type { WAWebSocketOptions, WAWebSocketEvent } from './socket/ws-socket.js';
|
|
103
|
+
|
|
104
|
+
// ============== Messages ==============
|
|
105
|
+
export {
|
|
106
|
+
MessageSender,
|
|
107
|
+
MessageReceiver,
|
|
108
|
+
PendingMessageTracker,
|
|
109
|
+
buildReadReceiptNode,
|
|
110
|
+
buildChatPresenceNode
|
|
111
|
+
} from './messages/send.js';
|
|
112
|
+
export {
|
|
113
|
+
encodeMessageNode,
|
|
114
|
+
decodeMessageNode,
|
|
115
|
+
generateMessageId,
|
|
116
|
+
generateStanzaId,
|
|
117
|
+
extractMessageText,
|
|
118
|
+
hasMedia,
|
|
119
|
+
getMediaInfo,
|
|
120
|
+
type ReceiptType,
|
|
121
|
+
type MessageStatusUpdateType
|
|
122
|
+
} from './messages/message-encoder.js';
|
|
123
|
+
export {
|
|
124
|
+
MediaUploader,
|
|
125
|
+
MediaDownloader,
|
|
126
|
+
encryptMedia,
|
|
127
|
+
decryptMedia,
|
|
128
|
+
deriveMediaKeys,
|
|
129
|
+
streamToBuffer,
|
|
130
|
+
bufferToStream,
|
|
131
|
+
type EncryptedMedia
|
|
132
|
+
} from './messages/media.js';
|
|
133
|
+
|
|
134
|
+
// ============== Groups ==============
|
|
135
|
+
export {
|
|
136
|
+
GroupManager,
|
|
137
|
+
buildInviteLink,
|
|
138
|
+
parseInviteLink
|
|
139
|
+
} from './groups/management.js';
|
|
140
|
+
|
|
141
|
+
// ============== Calls ==============
|
|
142
|
+
export { CallHandler } from './calls/handler.js';
|
|
143
|
+
export type { VoiceTranscriptionFn, VoiceTranscriptionResult } from './calls/handler.js';
|
|
144
|
+
|
|
145
|
+
// ============== Advanced Features ==============
|
|
146
|
+
export {
|
|
147
|
+
PollManager,
|
|
148
|
+
NewsletterManager,
|
|
149
|
+
StatusManager,
|
|
150
|
+
BusinessProfileManager,
|
|
151
|
+
CatalogManager,
|
|
152
|
+
LabelsManager,
|
|
153
|
+
type PollOptions,
|
|
154
|
+
type PollVote,
|
|
155
|
+
type NewsletterMetadata,
|
|
156
|
+
type StatusOptions,
|
|
157
|
+
type BusinessProfileOptions,
|
|
158
|
+
type BusinessHours,
|
|
159
|
+
type CatalogItem,
|
|
160
|
+
type Label
|
|
161
|
+
} from './advanced/features.js';
|
|
162
|
+
|
|
163
|
+
// ============== Event Emitter ==============
|
|
164
|
+
export { WAEventEmitter, StrictEventEmitter } from './events/emitter.js';
|
|
165
|
+
|
|
166
|
+
// ============== Utils ==============
|
|
167
|
+
export {
|
|
168
|
+
parseJid,
|
|
169
|
+
jidToString,
|
|
170
|
+
jidUser,
|
|
171
|
+
jidServer,
|
|
172
|
+
isGroupJid,
|
|
173
|
+
isUserJid,
|
|
174
|
+
isBroadcastJid,
|
|
175
|
+
isCallJid,
|
|
176
|
+
isStatusJid,
|
|
177
|
+
isNewsletter,
|
|
178
|
+
isLidJid,
|
|
179
|
+
isValidJid,
|
|
180
|
+
jidEqual,
|
|
181
|
+
jidToPhoneNumber,
|
|
182
|
+
phoneNumberToJid,
|
|
183
|
+
userJid,
|
|
184
|
+
groupJid,
|
|
185
|
+
broadcastJid
|
|
186
|
+
} from './utils/jid.js';
|
|
187
|
+
|
|
188
|
+
export {
|
|
189
|
+
DefaultLogger,
|
|
190
|
+
SilentLogger,
|
|
191
|
+
defaultLogger
|
|
192
|
+
} from './utils/logger.js';
|
|
193
|
+
|
|
194
|
+
export {
|
|
195
|
+
retry,
|
|
196
|
+
sleep,
|
|
197
|
+
RateLimiter,
|
|
198
|
+
PromiseQueue,
|
|
199
|
+
debounce,
|
|
200
|
+
throttle
|
|
201
|
+
} from './utils/retry.js';
|
|
202
|
+
|
|
203
|
+
// ============== SKDM (Store Key Data Manager) ==============
|
|
204
|
+
export {
|
|
205
|
+
SKDM,
|
|
206
|
+
PreKeyManager,
|
|
207
|
+
SessionManager,
|
|
208
|
+
IdentityKeyManager,
|
|
209
|
+
AppStateKeyManager,
|
|
210
|
+
LRUCache,
|
|
211
|
+
AtomicFileWriter,
|
|
212
|
+
type SKDMConfig,
|
|
213
|
+
type SKDMStats
|
|
214
|
+
} from './skdm/index.js';
|
|
215
|
+
|
|
216
|
+
// ============== Baileys Fixes ==============
|
|
217
|
+
export {
|
|
218
|
+
WeakEventEmitter,
|
|
219
|
+
ConnectionHeartbeat,
|
|
220
|
+
IdempotencyCache,
|
|
221
|
+
GroupEventCoalescer,
|
|
222
|
+
HistorySyncManager,
|
|
223
|
+
DecryptionRetryQueue,
|
|
224
|
+
MessageQueue,
|
|
225
|
+
ChunkedMediaUploader,
|
|
226
|
+
SmarterReconnector
|
|
227
|
+
} from './fixes/index.js';
|
|
228
|
+
|
|
229
|
+
// ============== New Features ==============
|
|
230
|
+
export {
|
|
231
|
+
HealthMonitor,
|
|
232
|
+
MultiAccountManager,
|
|
233
|
+
WebhookManager,
|
|
234
|
+
CircuitBreaker,
|
|
235
|
+
RateLimitTracker,
|
|
236
|
+
MessagePersistence,
|
|
237
|
+
type HealthStatus,
|
|
238
|
+
type AccountConfig,
|
|
239
|
+
type AccountStatus,
|
|
240
|
+
type WebhookConfig
|
|
241
|
+
} from './features/index.js';
|
|
242
|
+
|
|
243
|
+
// ============== Multi-Branch SKDM Recovery ==============
|
|
244
|
+
export {
|
|
245
|
+
RecoveryManager,
|
|
246
|
+
mapCodeToReason,
|
|
247
|
+
getActionDescription,
|
|
248
|
+
getReasonDescription,
|
|
249
|
+
DEFAULT_RECOVERY_BRANCHES,
|
|
250
|
+
DEFAULT_RECOVERY_CONFIG,
|
|
251
|
+
type DisconnectReason,
|
|
252
|
+
type RecoveryAction,
|
|
253
|
+
type RecoveryBranch,
|
|
254
|
+
type RecoveryContext,
|
|
255
|
+
type RecoveryConfig
|
|
256
|
+
} from './recovery/index.js';
|
|
257
|
+
|
|
258
|
+
// ============== All Types ==============
|
|
259
|
+
export * from './types/index.js';
|
|
260
|
+
|
|
261
|
+
// ============== Version ==============
|
|
262
|
+
export const VERSION = '1.0.0';
|
|
263
|
+
export const LIBRARY_NAME = 'waliwa';
|
|
264
|
+
|
|
265
|
+
// ============== Default Config ==============
|
|
266
|
+
export const DEFAULT_CONFIG = {
|
|
267
|
+
wsURL: 'wss://web.whatsapp.com/ws/chat',
|
|
268
|
+
browser: ['Waliwa', 'WA', '1.0.0'] as [string, string, string],
|
|
269
|
+
connectTimeoutMs: 20000,
|
|
270
|
+
keepAliveIntervalMs: 20000,
|
|
271
|
+
maxReconnectAttempts: 10,
|
|
272
|
+
reconnectInterval: 2000,
|
|
273
|
+
ramOptimizationLevel: 2 as 1 | 2 | 3
|
|
274
|
+
};
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waliwa - Media upload/download
|
|
3
|
+
* Handle upload media ke WhatsApp CDN dan decrypt download
|
|
4
|
+
*
|
|
5
|
+
* Media encryption: AES-256-CBC + HMAC-SHA256, then upload ke WA CDN
|
|
6
|
+
* Download: reverse process - download encrypted blob, decrypt, return plain
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import axios from 'axios';
|
|
10
|
+
import { Readable } from 'stream';
|
|
11
|
+
import {
|
|
12
|
+
sha256Hash,
|
|
13
|
+
hmacSha256,
|
|
14
|
+
aesEncryptCBC,
|
|
15
|
+
aesDecryptCBC,
|
|
16
|
+
hkdfDerive,
|
|
17
|
+
getRandomBytes,
|
|
18
|
+
concatBytes,
|
|
19
|
+
areBytesEqual
|
|
20
|
+
} from '../core/crypto.js';
|
|
21
|
+
import type {
|
|
22
|
+
MediaUploadResult,
|
|
23
|
+
MediaDownloadResult,
|
|
24
|
+
WALogger
|
|
25
|
+
} from '../types/index.js';
|
|
26
|
+
import { SilentLogger } from '../utils/logger.js';
|
|
27
|
+
|
|
28
|
+
// ============== Constants ==============
|
|
29
|
+
|
|
30
|
+
const WA_MEDIA_UPLOAD_URL = 'https://mmg.whatsapp.net/whatsapp/mmedia/mms';
|
|
31
|
+
const WA_MEDIA_DOWNLOAD_HOSTS = [
|
|
32
|
+
'mmg.whatsapp.net',
|
|
33
|
+
'mmg2.whatsapp.net',
|
|
34
|
+
'mmg3.whatsapp.net'
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const MEDIA_KEY_TYPES: Record<string, number> = {
|
|
38
|
+
image: 1,
|
|
39
|
+
video: 2,
|
|
40
|
+
audio: 3,
|
|
41
|
+
document: 4,
|
|
42
|
+
sticker: 5,
|
|
43
|
+
'thumbnail-image': 6,
|
|
44
|
+
'thumbnail-video': 7,
|
|
45
|
+
'thumbnail-document': 8,
|
|
46
|
+
'md-msg-hist': 9,
|
|
47
|
+
'app-state': 10,
|
|
48
|
+
'link-thumbnail': 11,
|
|
49
|
+
'media-msg': 12
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const CHUNK_SIZE = 64 * 1024; // 64KB chunks untuk upload
|
|
53
|
+
|
|
54
|
+
// ============== Media Key Derivation ==============
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Derive media encryption keys dari random mediaKey + mediaType
|
|
58
|
+
*
|
|
59
|
+
* Spec WA: HKDF-SHA256 dengan salt=length + mediaType-appended
|
|
60
|
+
* Output: 4 keys - iv, cipherKey, macKey, refKey
|
|
61
|
+
*/
|
|
62
|
+
export function deriveMediaKeys(
|
|
63
|
+
mediaKey: Uint8Array,
|
|
64
|
+
mediaType: string
|
|
65
|
+
): {
|
|
66
|
+
iv: Uint8Array;
|
|
67
|
+
cipherKey: Uint8Array;
|
|
68
|
+
macKey: Uint8Array;
|
|
69
|
+
refKey: Uint8Array;
|
|
70
|
+
} {
|
|
71
|
+
const mediaTypeNum = MEDIA_KEY_TYPES[mediaType] ?? 1;
|
|
72
|
+
const salt = new Uint8Array([mediaTypeNum]);
|
|
73
|
+
const info = new TextEncoder().encode(`WhatsApp ${mediaType} Keys`);
|
|
74
|
+
|
|
75
|
+
const derived = hkdfDerive(mediaKey, salt, info, 112); // 112 bytes total
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
iv: derived.subarray(0, 16),
|
|
79
|
+
cipherKey: derived.subarray(16, 48),
|
|
80
|
+
macKey: derived.subarray(48, 80),
|
|
81
|
+
refKey: derived.subarray(80, 112)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============== Media Encryption ==============
|
|
86
|
+
|
|
87
|
+
export interface EncryptedMedia {
|
|
88
|
+
/** Encrypted media data */
|
|
89
|
+
ciphertext: Uint8Array;
|
|
90
|
+
/** MAC (HMAC-SHA256 dari encrypted data + IV) */
|
|
91
|
+
mac: Uint8Array;
|
|
92
|
+
/** Random mediaKey (32 bytes) */
|
|
93
|
+
mediaKey: Uint8Array;
|
|
94
|
+
/** SHA256 hash dari plaintext */
|
|
95
|
+
fileSha256: Uint8Array;
|
|
96
|
+
/** SHA256 hash dari encrypted data */
|
|
97
|
+
encFileSha256: Uint8Array;
|
|
98
|
+
/** Original file length */
|
|
99
|
+
fileLength: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Encrypt media untuk upload
|
|
104
|
+
* Spec: AES-256-CBC + HMAC-SHA256 untuk integrity
|
|
105
|
+
*/
|
|
106
|
+
export async function encryptMedia(
|
|
107
|
+
plaintext: Buffer | Uint8Array,
|
|
108
|
+
mediaType: string
|
|
109
|
+
): Promise<EncryptedMedia> {
|
|
110
|
+
// 1. Generate random mediaKey
|
|
111
|
+
const mediaKey = getRandomBytes(32);
|
|
112
|
+
|
|
113
|
+
// 2. Derive keys
|
|
114
|
+
const { iv, cipherKey, macKey } = deriveMediaKeys(mediaKey, mediaType);
|
|
115
|
+
|
|
116
|
+
// 3. Compute plaintext hash
|
|
117
|
+
const fileSha256 = sha256Hash(plaintext);
|
|
118
|
+
|
|
119
|
+
// 4. Encrypt dengan AES-256-CBC
|
|
120
|
+
const ciphertext = aesEncryptCBC(plaintext, cipherKey, iv);
|
|
121
|
+
|
|
122
|
+
// 5. Compute MAC: HMAC-SHA256(macKey, iv + ciphertext)
|
|
123
|
+
const macInput = concatBytes(iv, ciphertext);
|
|
124
|
+
const mac = hmacSha256(macKey, macInput);
|
|
125
|
+
|
|
126
|
+
// 6. Compute encrypted file hash
|
|
127
|
+
const encFileSha256 = sha256Hash(concatBytes(ciphertext, mac));
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
ciphertext,
|
|
131
|
+
mac,
|
|
132
|
+
mediaKey,
|
|
133
|
+
fileSha256,
|
|
134
|
+
encFileSha256,
|
|
135
|
+
fileLength: plaintext.length
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============== Media Decryption ==============
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Decrypt media hasil download
|
|
143
|
+
*/
|
|
144
|
+
export async function decryptMedia(
|
|
145
|
+
encryptedData: Uint8Array,
|
|
146
|
+
mediaKey: Uint8Array,
|
|
147
|
+
mediaType: string
|
|
148
|
+
): Promise<Uint8Array> {
|
|
149
|
+
if (encryptedData.length < 16) {
|
|
150
|
+
throw new Error('Encrypted media too short');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { iv, cipherKey, macKey } = deriveMediaKeys(mediaKey, mediaType);
|
|
154
|
+
|
|
155
|
+
// Last 10 bytes = MAC (untuk CBC mode WA spec)
|
|
156
|
+
// Note: WhatsApp uses 10-byte MAC instead of full 32
|
|
157
|
+
const mac = encryptedData.subarray(encryptedData.length - 10);
|
|
158
|
+
const ciphertext = encryptedData.subarray(0, encryptedData.length - 10);
|
|
159
|
+
|
|
160
|
+
// Verify MAC
|
|
161
|
+
const expectedMac = hmacSha256(macKey, concatBytes(iv, ciphertext));
|
|
162
|
+
const expectedMacShort = expectedMac.subarray(0, 10);
|
|
163
|
+
if (!areBytesEqual(mac, expectedMacShort)) {
|
|
164
|
+
throw new Error('Media MAC verification failed');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Decrypt
|
|
168
|
+
const plaintext = aesDecryptCBC(ciphertext, cipherKey, iv);
|
|
169
|
+
return plaintext;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============== Media Uploader ==============
|
|
173
|
+
|
|
174
|
+
export interface MediaUploaderOptions {
|
|
175
|
+
logger?: WALogger;
|
|
176
|
+
/** Custom HTTP agent */
|
|
177
|
+
httpAgent?: any;
|
|
178
|
+
/** Timeout ms */
|
|
179
|
+
timeoutMs?: number;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export class MediaUploader {
|
|
183
|
+
private logger: WALogger;
|
|
184
|
+
private httpAgent?: any;
|
|
185
|
+
private timeoutMs: number;
|
|
186
|
+
|
|
187
|
+
constructor(opts: MediaUploaderOptions = {}) {
|
|
188
|
+
this.logger = opts.logger ?? new SilentLogger();
|
|
189
|
+
this.httpAgent = opts.httpAgent;
|
|
190
|
+
this.timeoutMs = opts.timeoutMs ?? 60000;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Upload media ke WhatsApp CDN
|
|
195
|
+
*/
|
|
196
|
+
async upload(
|
|
197
|
+
media: Buffer | Uint8Array,
|
|
198
|
+
mediaType: string,
|
|
199
|
+
mimetype: string,
|
|
200
|
+
fileName?: string
|
|
201
|
+
): Promise<MediaUploadResult> {
|
|
202
|
+
this.logger.debug(`Encrypting ${media.length} bytes of ${mediaType} (${mimetype})`);
|
|
203
|
+
|
|
204
|
+
// Encrypt
|
|
205
|
+
const encrypted = await encryptMedia(media, mediaType);
|
|
206
|
+
|
|
207
|
+
// Build upload URL
|
|
208
|
+
const uploadUrl = `${WA_MEDIA_UPLOAD_URL}/${mediaType}`;
|
|
209
|
+
const params: any = {
|
|
210
|
+
hash: Buffer.from(encrypted.encFileSha256).toString('base64'),
|
|
211
|
+
type: mimetype,
|
|
212
|
+
size: encrypted.fileLength,
|
|
213
|
+
name: fileName
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
this.logger.debug(`Uploading to ${uploadUrl}`);
|
|
217
|
+
|
|
218
|
+
// Upload via POST
|
|
219
|
+
const response = await axios.post(uploadUrl, Buffer.from(encrypted.ciphertext), {
|
|
220
|
+
params,
|
|
221
|
+
headers: {
|
|
222
|
+
'Content-Type': 'application/octet-stream',
|
|
223
|
+
'Origin': 'https://web.whatsapp.com',
|
|
224
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
225
|
+
},
|
|
226
|
+
timeout: this.timeoutMs,
|
|
227
|
+
maxContentLength: Infinity,
|
|
228
|
+
maxBodyLength: Infinity
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (response.status !== 200 && response.status !== 201) {
|
|
232
|
+
throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const data = response.data;
|
|
236
|
+
if (!data || !data.url) {
|
|
237
|
+
throw new Error('Invalid upload response: missing URL');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.logger.debug(`Upload successful: ${data.url}`);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
url: data.url,
|
|
244
|
+
directPath: data.direct_path || data.directPath || '',
|
|
245
|
+
mediaKey: encrypted.mediaKey,
|
|
246
|
+
encFileHash: encrypted.encFileSha256,
|
|
247
|
+
fileEncSHA256: encrypted.encFileSha256,
|
|
248
|
+
fileSHA256: encrypted.fileSha256,
|
|
249
|
+
fileLength: encrypted.fileLength,
|
|
250
|
+
mimetype
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Upload stream media (untuk large files)
|
|
256
|
+
*/
|
|
257
|
+
async uploadStream(
|
|
258
|
+
stream: Readable,
|
|
259
|
+
mediaType: string,
|
|
260
|
+
mimetype: string,
|
|
261
|
+
fileLength: number
|
|
262
|
+
): Promise<MediaUploadResult> {
|
|
263
|
+
// Collect stream ke buffer (untuk simplicity)
|
|
264
|
+
const chunks: Buffer[] = [];
|
|
265
|
+
for await (const chunk of stream) {
|
|
266
|
+
chunks.push(Buffer.from(chunk));
|
|
267
|
+
}
|
|
268
|
+
const buffer = Buffer.concat(chunks);
|
|
269
|
+
return this.upload(buffer, mediaType, mimetype);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============== Media Downloader ==============
|
|
274
|
+
|
|
275
|
+
export interface MediaDownloaderOptions {
|
|
276
|
+
logger?: WALogger;
|
|
277
|
+
httpAgent?: any;
|
|
278
|
+
timeoutMs?: number;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export class MediaDownloader {
|
|
282
|
+
private logger: WALogger;
|
|
283
|
+
private httpAgent?: any;
|
|
284
|
+
private timeoutMs: number;
|
|
285
|
+
|
|
286
|
+
constructor(opts: MediaDownloaderOptions = {}) {
|
|
287
|
+
this.logger = opts.logger ?? new SilentLogger();
|
|
288
|
+
this.httpAgent = opts.httpAgent;
|
|
289
|
+
this.timeoutMs = opts.timeoutMs ?? 60000;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Download media dari URL dan decrypt
|
|
294
|
+
*/
|
|
295
|
+
async download(
|
|
296
|
+
url: string,
|
|
297
|
+
mediaKey: Uint8Array,
|
|
298
|
+
mediaType: string,
|
|
299
|
+
mimetype: string,
|
|
300
|
+
fileName?: string
|
|
301
|
+
): Promise<MediaDownloadResult> {
|
|
302
|
+
this.logger.debug(`Downloading media from ${url}`);
|
|
303
|
+
|
|
304
|
+
// Determine host
|
|
305
|
+
let downloadUrl = url;
|
|
306
|
+
if (!url.startsWith('http')) {
|
|
307
|
+
// Relative path, prepend host
|
|
308
|
+
const host = WA_MEDIA_DOWNLOAD_HOSTS[0];
|
|
309
|
+
downloadUrl = `https://${host}${url.startsWith('/') ? '' : '/'}${url}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const response = await axios.get(downloadUrl, {
|
|
313
|
+
responseType: 'arraybuffer',
|
|
314
|
+
timeout: this.timeoutMs,
|
|
315
|
+
headers: {
|
|
316
|
+
'Origin': 'https://web.whatsapp.com',
|
|
317
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (response.status !== 200) {
|
|
322
|
+
throw new Error(`Download failed: ${response.status}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const encryptedData = new Uint8Array(response.data);
|
|
326
|
+
|
|
327
|
+
// Decrypt
|
|
328
|
+
const plaintext = await decryptMedia(encryptedData, mediaKey, mediaType);
|
|
329
|
+
|
|
330
|
+
this.logger.debug(`Decrypted ${plaintext.length} bytes`);
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
buffer: Buffer.from(plaintext),
|
|
334
|
+
mimetype,
|
|
335
|
+
fileLength: plaintext.length,
|
|
336
|
+
fileName
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Download dari direct path (alternative to URL)
|
|
342
|
+
*/
|
|
343
|
+
async downloadFromDirectPath(
|
|
344
|
+
directPath: string,
|
|
345
|
+
mediaKey: Uint8Array,
|
|
346
|
+
mediaType: string,
|
|
347
|
+
mimetype: string,
|
|
348
|
+
fileName?: string
|
|
349
|
+
): Promise<MediaDownloadResult> {
|
|
350
|
+
return this.download(directPath, mediaKey, mediaType, mimetype, fileName);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ============== Helper ==============
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Convert Readable stream ke Buffer
|
|
358
|
+
*/
|
|
359
|
+
export async function streamToBuffer(stream: Readable): Promise<Buffer> {
|
|
360
|
+
const chunks: Buffer[] = [];
|
|
361
|
+
for await (const chunk of stream) {
|
|
362
|
+
chunks.push(Buffer.from(chunk));
|
|
363
|
+
}
|
|
364
|
+
return Buffer.concat(chunks);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Convert Buffer ke Readable stream
|
|
369
|
+
*/
|
|
370
|
+
export function bufferToStream(buffer: Buffer): Readable {
|
|
371
|
+
return Readable.from(buffer);
|
|
372
|
+
}
|