@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
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waliwa - Auth state dengan File JSON storage
|
|
3
|
+
*
|
|
4
|
+
* RAM-efficient: gunakan debounced writes untuk avoid disk IO bottleneck,
|
|
5
|
+
* lazy-load sessions/senderKeys untuk avoid loading semua keys ke RAM.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { promises as fs } from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import type {
|
|
11
|
+
AuthenticationCreds,
|
|
12
|
+
AuthState,
|
|
13
|
+
SignalKeyStore,
|
|
14
|
+
KeyPair,
|
|
15
|
+
SignedKeyPair
|
|
16
|
+
} from '../types/index.js';
|
|
17
|
+
import {
|
|
18
|
+
generateCurveKeyPair,
|
|
19
|
+
getRandomBytes
|
|
20
|
+
} from '../core/crypto.js';
|
|
21
|
+
import { debounce } from '../utils/retry.js';
|
|
22
|
+
import type { WALogger } from '../types/index.js';
|
|
23
|
+
import { SilentLogger } from '../utils/logger.js';
|
|
24
|
+
|
|
25
|
+
// ============== Default Auth State ==============
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create fresh authentication credentials untuk first-time login
|
|
29
|
+
*/
|
|
30
|
+
export function createDefaultCreds(): AuthenticationCreds {
|
|
31
|
+
const noiseKey = generateCurveKeyPair();
|
|
32
|
+
const pairingKey = generateCurveKeyPair();
|
|
33
|
+
const identityId = generateCurveKeyPair();
|
|
34
|
+
|
|
35
|
+
// Signed pre-key: sign identity public key with private key
|
|
36
|
+
const signedPreKeyPair = generateCurveKeyPair();
|
|
37
|
+
const { sha256Hash } = require('../core/crypto.js');
|
|
38
|
+
const signature = new Uint8Array(64); // placeholder signature, will be regenerated on register
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
registered: false,
|
|
42
|
+
noiseKey,
|
|
43
|
+
pairingKey,
|
|
44
|
+
identityId,
|
|
45
|
+
signedIdentityKey: identityId,
|
|
46
|
+
signedPreKey: {
|
|
47
|
+
keyId: 1,
|
|
48
|
+
keyPair: signedPreKeyPair,
|
|
49
|
+
signature
|
|
50
|
+
},
|
|
51
|
+
registrationId: Math.floor(getRandomBytes(2).reduce((a, b) => (a << 8) | b, 0) / 65535 * 16383),
|
|
52
|
+
nextPreKeyId: 2,
|
|
53
|
+
firstUnuploadedPreKeyId: 2,
|
|
54
|
+
accountSettings: {
|
|
55
|
+
unarchiveChats: false
|
|
56
|
+
},
|
|
57
|
+
markOnlineLastSeen: true
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============== File-based Auth State ==============
|
|
62
|
+
|
|
63
|
+
export interface FileAuthStateOptions {
|
|
64
|
+
/** Path folder untuk simpan session files */
|
|
65
|
+
folder: string;
|
|
66
|
+
/** File prefix (default: "waliwa-session") */
|
|
67
|
+
prefix?: string;
|
|
68
|
+
/** Logger */
|
|
69
|
+
logger?: WALogger;
|
|
70
|
+
/** Debounce write ms (default: 100ms untuk RAM efficiency) */
|
|
71
|
+
writeDebounceMs?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* File-based auth state - simpan creds dan keys sebagai JSON files
|
|
76
|
+
* RAM-friendly: gunakan debounced writes dan lazy key loading
|
|
77
|
+
*/
|
|
78
|
+
export class FileAuthState implements AuthState {
|
|
79
|
+
creds: AuthenticationCreds;
|
|
80
|
+
keys: SignalKeyStore;
|
|
81
|
+
private folder: string;
|
|
82
|
+
private prefix: string;
|
|
83
|
+
private logger: WALogger;
|
|
84
|
+
private credsFile: string;
|
|
85
|
+
private keysFile: string;
|
|
86
|
+
private saveCredsDebounced: () => void;
|
|
87
|
+
private saveKeysDebounced: () => void;
|
|
88
|
+
private dirty: { creds: boolean; keys: boolean } = { creds: false, keys: false };
|
|
89
|
+
|
|
90
|
+
constructor(opts: FileAuthStateOptions) {
|
|
91
|
+
this.folder = opts.folder;
|
|
92
|
+
this.prefix = opts.prefix ?? 'waliwa-session';
|
|
93
|
+
this.logger = opts.logger ?? new SilentLogger();
|
|
94
|
+
this.credsFile = path.join(this.folder, `${this.prefix}-creds.json`);
|
|
95
|
+
this.keysFile = path.join(this.folder, `${this.prefix}-keys.json`);
|
|
96
|
+
|
|
97
|
+
// Load atau create creds
|
|
98
|
+
this.creds = this.loadCreds();
|
|
99
|
+
this.keys = this.loadKeys();
|
|
100
|
+
|
|
101
|
+
// Setup debounced saves
|
|
102
|
+
const debounceMs = opts.writeDebounceMs ?? 100;
|
|
103
|
+
this.saveCredsDebounced = debounce(() => this.flushCreds(), debounceMs);
|
|
104
|
+
this.saveKeysDebounced = debounce(() => this.flushKeys(), debounceMs);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============== Load ==============
|
|
108
|
+
|
|
109
|
+
private loadCreds(): AuthenticationCreds {
|
|
110
|
+
try {
|
|
111
|
+
const data = require('fs').readFileSync(this.credsFile, 'utf-8');
|
|
112
|
+
const parsed = JSON.parse(data, (key, value) => {
|
|
113
|
+
// Convert base64 strings back to Uint8Array untuk fields tertentu
|
|
114
|
+
if (typeof value === 'object' && value !== null && value.type === 'Uint8Array') {
|
|
115
|
+
return new Uint8Array(value.data);
|
|
116
|
+
}
|
|
117
|
+
if (typeof value === 'object' && value !== null && value.type === 'KeyPair') {
|
|
118
|
+
return {
|
|
119
|
+
private: new Uint8Array(value.private),
|
|
120
|
+
public: new Uint8Array(value.public)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
});
|
|
125
|
+
this.logger.debug('Loaded existing credentials');
|
|
126
|
+
return parsed;
|
|
127
|
+
} catch (err: any) {
|
|
128
|
+
if (err.code === 'ENOENT') {
|
|
129
|
+
this.logger.info('No existing credentials, creating new');
|
|
130
|
+
return createDefaultCreds();
|
|
131
|
+
}
|
|
132
|
+
this.logger.error(`Failed to load creds: ${err.message}`);
|
|
133
|
+
return createDefaultCreds();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private loadKeys(): SignalKeyStore {
|
|
138
|
+
try {
|
|
139
|
+
const data = require('fs').readFileSync(this.keysFile, 'utf-8');
|
|
140
|
+
const parsed = JSON.parse(data, (key, value) => {
|
|
141
|
+
if (typeof value === 'object' && value !== null && value.type === 'Uint8Array') {
|
|
142
|
+
return new Uint8Array(value.data);
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
preKeys: parsed.preKeys || {},
|
|
148
|
+
sessions: parsed.sessions || {},
|
|
149
|
+
identityKeys: parsed.identityKeys || {},
|
|
150
|
+
senderKeys: parsed.senderKeys || {},
|
|
151
|
+
appStateKeys: parsed.appStateKeys || {},
|
|
152
|
+
appStateSyncKeys: parsed.appStateSyncKeys || {}
|
|
153
|
+
};
|
|
154
|
+
} catch (err: any) {
|
|
155
|
+
if (err.code === 'ENOENT') {
|
|
156
|
+
this.logger.info('No existing keys, creating new empty keystore');
|
|
157
|
+
} else {
|
|
158
|
+
this.logger.error(`Failed to load keys: ${err.message}`);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
preKeys: {},
|
|
162
|
+
sessions: {},
|
|
163
|
+
identityKeys: {},
|
|
164
|
+
senderKeys: {},
|
|
165
|
+
appStateKeys: {},
|
|
166
|
+
appStateSyncKeys: {}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============== Save ==============
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Mark creds dirty untuk debounced save
|
|
175
|
+
*/
|
|
176
|
+
markCredsDirty(): void {
|
|
177
|
+
this.dirty.creds = true;
|
|
178
|
+
this.saveCredsDebounced();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
markKeysDirty(): void {
|
|
182
|
+
this.dirty.keys = true;
|
|
183
|
+
this.saveKeysDebounced();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async flushCreds(): Promise<void> {
|
|
187
|
+
if (!this.dirty.creds) return;
|
|
188
|
+
this.dirty.creds = false;
|
|
189
|
+
|
|
190
|
+
const data = JSON.stringify(this.creds, (key, value) => {
|
|
191
|
+
if (value instanceof Uint8Array) {
|
|
192
|
+
return { type: 'Uint8Array', data: Array.from(value) };
|
|
193
|
+
}
|
|
194
|
+
if (typeof value === 'object' && value !== null && 'private' in value && 'public' in value &&
|
|
195
|
+
value.private instanceof Uint8Array) {
|
|
196
|
+
return {
|
|
197
|
+
type: 'KeyPair',
|
|
198
|
+
private: Array.from(value.private),
|
|
199
|
+
public: Array.from(value.public)
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return value;
|
|
203
|
+
}, 0);
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
await fs.mkdir(this.folder, { recursive: true });
|
|
207
|
+
await fs.writeFile(this.credsFile, data, 'utf-8');
|
|
208
|
+
this.logger.debug('Credentials saved');
|
|
209
|
+
} catch (err: any) {
|
|
210
|
+
this.logger.error(`Failed to save creds: ${err.message}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async flushKeys(): Promise<void> {
|
|
215
|
+
if (!this.dirty.keys) return;
|
|
216
|
+
this.dirty.keys = false;
|
|
217
|
+
|
|
218
|
+
const data = JSON.stringify(this.keys, (key, value) => {
|
|
219
|
+
if (value instanceof Uint8Array) {
|
|
220
|
+
return { type: 'Uint8Array', data: Array.from(value) };
|
|
221
|
+
}
|
|
222
|
+
if (typeof value === 'object' && value !== null && 'private' in value && 'public' in value &&
|
|
223
|
+
value.private instanceof Uint8Array) {
|
|
224
|
+
return {
|
|
225
|
+
type: 'KeyPair',
|
|
226
|
+
private: Array.from(value.private),
|
|
227
|
+
public: Array.from(value.public)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return value;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await fs.mkdir(this.folder, { recursive: true });
|
|
235
|
+
await fs.writeFile(this.keysFile, data, 'utf-8');
|
|
236
|
+
this.logger.debug('Keys saved');
|
|
237
|
+
} catch (err: any) {
|
|
238
|
+
this.logger.error(`Failed to save keys: ${err.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Force flush pending saves
|
|
244
|
+
*/
|
|
245
|
+
async flush(): Promise<void> {
|
|
246
|
+
await Promise.all([this.flushCreds(), this.flushKeys()]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Clear all session data (logout)
|
|
251
|
+
*/
|
|
252
|
+
async clear(): Promise<void> {
|
|
253
|
+
try {
|
|
254
|
+
await fs.unlink(this.credsFile);
|
|
255
|
+
await fs.unlink(this.keysFile);
|
|
256
|
+
} catch (err: any) {
|
|
257
|
+
if (err.code !== 'ENOENT') {
|
|
258
|
+
this.logger.error(`Failed to clear session: ${err.message}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
this.creds = createDefaultCreds();
|
|
262
|
+
this.keys = {
|
|
263
|
+
preKeys: {},
|
|
264
|
+
sessions: {},
|
|
265
|
+
identityKeys: {},
|
|
266
|
+
senderKeys: {},
|
|
267
|
+
appStateKeys: {},
|
|
268
|
+
appStateSyncKeys: {}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ============== Key Management ==============
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Set pre-key dengan ID tertentu
|
|
276
|
+
*/
|
|
277
|
+
setPreKey(id: number, keyPair: KeyPair): void {
|
|
278
|
+
this.keys.preKeys[id] = keyPair;
|
|
279
|
+
if (id >= this.creds.nextPreKeyId) {
|
|
280
|
+
this.creds.nextPreKeyId = id + 1;
|
|
281
|
+
}
|
|
282
|
+
this.markKeysDirty();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get pre-key by ID
|
|
287
|
+
*/
|
|
288
|
+
getPreKey(id: number): KeyPair | undefined {
|
|
289
|
+
return this.keys.preKeys[id];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Generate pre-key
|
|
294
|
+
*/
|
|
295
|
+
generatePreKey(id: number): KeyPair {
|
|
296
|
+
const kp = generateCurveKeyPair();
|
|
297
|
+
this.setPreKey(id, kp);
|
|
298
|
+
return kp;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Set identity key untuk user
|
|
303
|
+
*/
|
|
304
|
+
setIdentityKey(user: string, key: Uint8Array): void {
|
|
305
|
+
this.keys.identityKeys[user] = { private: new Uint8Array(0), public: key };
|
|
306
|
+
this.markKeysDirty();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get identity key untuk user
|
|
311
|
+
*/
|
|
312
|
+
getIdentityKey(user: string): Uint8Array | undefined {
|
|
313
|
+
return this.keys.identityKeys[user]?.public;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Set app state sync key
|
|
318
|
+
*/
|
|
319
|
+
setAppStateSyncKey(id: string, key: Uint8Array): void {
|
|
320
|
+
this.keys.appStateSyncKeys[id] = key;
|
|
321
|
+
this.markKeysDirty();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get app state sync key
|
|
326
|
+
*/
|
|
327
|
+
getAppStateSyncKey(id: string): Uint8Array | undefined {
|
|
328
|
+
return this.keys.appStateSyncKeys[id];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Helper untuk init file-based auth state
|
|
334
|
+
*/
|
|
335
|
+
export async function useFileAuthState(
|
|
336
|
+
folder: string,
|
|
337
|
+
prefix?: string,
|
|
338
|
+
logger?: WALogger
|
|
339
|
+
): Promise<AuthState> {
|
|
340
|
+
const state = new FileAuthState({ folder, prefix, logger });
|
|
341
|
+
return state;
|
|
342
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waliwa - Pairing code implementation (Mobile API style)
|
|
3
|
+
*
|
|
4
|
+
* Alternatif login tanpa scan QR - user input 8-character pairing code
|
|
5
|
+
* di WhatsApp mobile app. Mengikuti companion device registration flow.
|
|
6
|
+
*
|
|
7
|
+
* Format pairing code: "AB12-CD34" (8 alphanumeric, dash di tengah)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
AuthenticationCreds,
|
|
12
|
+
PairingCodeResult,
|
|
13
|
+
WALogger,
|
|
14
|
+
JIDLike
|
|
15
|
+
} from '../types/index.js';
|
|
16
|
+
import { getRandomBytes, hkdfDerive } from '../core/crypto.js';
|
|
17
|
+
import { SilentLogger } from '../utils/logger.js';
|
|
18
|
+
|
|
19
|
+
// ============== Pairing Code Generator ==============
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate pairing code dari phone number + secret key
|
|
23
|
+
*
|
|
24
|
+
* Pairing code adalah 8 character alphanumeric yang derived dari:
|
|
25
|
+
* 1. Phone number user (target)
|
|
26
|
+
* 2. Server-side secret (sebagai HMAC key)
|
|
27
|
+
* 3. Random challenge untuk anti-replay
|
|
28
|
+
*
|
|
29
|
+
* User input code di WA mobile app, server verify dan pair device.
|
|
30
|
+
*/
|
|
31
|
+
export function generatePairingCode(
|
|
32
|
+
phoneNumber: string,
|
|
33
|
+
creds: AuthenticationCreds,
|
|
34
|
+
logger?: WALogger
|
|
35
|
+
): PairingCodeResult {
|
|
36
|
+
const log = logger ?? new SilentLogger();
|
|
37
|
+
|
|
38
|
+
// Sanitize phone number
|
|
39
|
+
const cleanPhone = phoneNumber.replace(/[^0-9]/g, '');
|
|
40
|
+
if (cleanPhone.length < 8) {
|
|
41
|
+
throw new Error(`Invalid phone number: ${phoneNumber}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Get pairing secret dari creds (atau generate)
|
|
45
|
+
const secretKey = creds.pairingKey?.private ?? getRandomBytes(32);
|
|
46
|
+
const publicKey = creds.pairingKey?.public ?? new Uint8Array(32);
|
|
47
|
+
|
|
48
|
+
// Compute pairing code via HKDF derive
|
|
49
|
+
// Code derived from phone number + public key fingerprint
|
|
50
|
+
const phoneBytes = new TextEncoder().encode(cleanPhone);
|
|
51
|
+
const derived = hkdfDerive(
|
|
52
|
+
secretKey,
|
|
53
|
+
phoneBytes,
|
|
54
|
+
new TextEncoder().encode('pairing-code-v1'),
|
|
55
|
+
4 // 4 bytes = 8 hex chars
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Convert 4 bytes ke 8-character alphanumeric
|
|
59
|
+
// Mapping: each nibble (4 bits) -> 1 character dari "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
60
|
+
// (avoid ambiguous chars: 0/O, 1/I)
|
|
61
|
+
const charSet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
62
|
+
let code = '';
|
|
63
|
+
for (let i = 0; i < 4; i++) {
|
|
64
|
+
const byte = derived[i];
|
|
65
|
+
code += charSet[byte >> 4]; // high nibble
|
|
66
|
+
code += charSet[byte & 0xf]; // low nibble
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Format: "XXXX-XXXX"
|
|
70
|
+
const formattedCode = `${code.substring(0, 4)}-${code.substring(4, 8)}`;
|
|
71
|
+
|
|
72
|
+
log.info(`Generated pairing code untuk ${cleanPhone}: ${formattedCode}`);
|
|
73
|
+
log.debug(`Pairing public key: ${Buffer.from(publicKey).toString('hex').substring(0, 32)}...`);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
code: formattedCode,
|
|
77
|
+
phoneNumber: cleanPhone,
|
|
78
|
+
expiresAt: Date.now() + 65 * 1000 // 65 detik expiry
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============== Pairing Code Validation ==============
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validate pairing code format
|
|
86
|
+
*/
|
|
87
|
+
export function isValidPairingCodeFormat(code: string): boolean {
|
|
88
|
+
return /^[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(code.toUpperCase());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Normalize user input pairing code
|
|
93
|
+
* "ab12cd34" -> "AB12-CD34"
|
|
94
|
+
* "AB12 CD34" -> "AB12-CD34"
|
|
95
|
+
*/
|
|
96
|
+
export function normalizePairingCode(input: string): string {
|
|
97
|
+
const cleaned = input.replace(/[^A-Za-z0-9]/g, '').toUpperCase();
|
|
98
|
+
if (cleaned.length !== 8) {
|
|
99
|
+
throw new Error(`Pairing code must be 8 alphanumeric chars, got: ${input}`);
|
|
100
|
+
}
|
|
101
|
+
return `${cleaned.substring(0, 4)}-${cleaned.substring(4, 8)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============== Pairing Request Builder ==============
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build pairing request binary node untuk kirim ke server
|
|
108
|
+
*
|
|
109
|
+
* <iq type="get" to="s.whatsapp.net">
|
|
110
|
+
* <pair code="AB12-CD34" phone="6281234567890" />
|
|
111
|
+
* </iq>
|
|
112
|
+
*/
|
|
113
|
+
export function buildPairingRequestNode(
|
|
114
|
+
code: string,
|
|
115
|
+
phoneNumber: string,
|
|
116
|
+
publicKey: Uint8Array
|
|
117
|
+
): any {
|
|
118
|
+
const cleanPhone = phoneNumber.replace(/[^0-9]/g, '');
|
|
119
|
+
return {
|
|
120
|
+
tag: 'iq',
|
|
121
|
+
attrs: {
|
|
122
|
+
type: 'set',
|
|
123
|
+
to: 's.whatsapp.net',
|
|
124
|
+
id: `pair-${Date.now()}`
|
|
125
|
+
},
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
tag: 'pair',
|
|
129
|
+
attrs: {
|
|
130
|
+
code: code.replace('-', ''),
|
|
131
|
+
phone: cleanPhone,
|
|
132
|
+
pubkey: Buffer.from(publicKey).toString('base64')
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============== Pairing Result Handler ==============
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Process pairing result dari server
|
|
143
|
+
* Jika success, extract identity keys dan update creds
|
|
144
|
+
*/
|
|
145
|
+
export interface PairingResult {
|
|
146
|
+
success: boolean;
|
|
147
|
+
/** JID dari paired account (e.g. "6281234567890@s.whatsapp.net") */
|
|
148
|
+
jid?: string;
|
|
149
|
+
/** Platform dari device yang paired */
|
|
150
|
+
platform?: string;
|
|
151
|
+
/** Business name jika business account */
|
|
152
|
+
businessName?: string;
|
|
153
|
+
/** Server-side identity key */
|
|
154
|
+
identityKey?: Uint8Array;
|
|
155
|
+
/** Error message jika gagal */
|
|
156
|
+
error?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Parse pairing result dari binary node response
|
|
161
|
+
*/
|
|
162
|
+
export function parsePairingResult(node: any): PairingResult {
|
|
163
|
+
if (!node || node.tag !== 'iq') {
|
|
164
|
+
return { success: false, error: 'Invalid response node' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const type = node.attrs?.type;
|
|
168
|
+
if (type === 'error') {
|
|
169
|
+
const errorNode = Array.isArray(node.content) && node.content.find((c: any) => c.tag === 'error');
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
error: errorNode?.attrs?.text || 'Pairing failed'
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const pairNode = Array.isArray(node.content) && node.content.find((c: any) => c.tag === 'pair');
|
|
177
|
+
if (!pairNode) {
|
|
178
|
+
return { success: false, error: 'Pair result not found' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
success: true,
|
|
183
|
+
jid: pairNode.attrs?.jid,
|
|
184
|
+
platform: pairNode.attrs?.platform,
|
|
185
|
+
businessName: pairNode.attrs?.business_name,
|
|
186
|
+
identityKey: pairNode.attrs?.identity
|
|
187
|
+
? new Uint8Array(Buffer.from(pairNode.attrs.identity, 'base64'))
|
|
188
|
+
: undefined
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ============== Phone Number Normalization ==============
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Normalize phone number - hapus karakter non-digit
|
|
196
|
+
*/
|
|
197
|
+
export function normalizePhoneNumber(phone: string): string {
|
|
198
|
+
return phone.replace(/[^0-9]/g, '');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Check apakah phone number valid (Indonesia: 10-15 digit)
|
|
203
|
+
*/
|
|
204
|
+
export function isValidPhoneNumber(phone: string): boolean {
|
|
205
|
+
const clean = normalizePhoneNumber(phone);
|
|
206
|
+
return clean.length >= 8 && clean.length <= 15;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ============== Code Expiry Tracker ==============
|
|
210
|
+
|
|
211
|
+
export class PairingCodeExpiry {
|
|
212
|
+
private expiresAt: number;
|
|
213
|
+
private onExpiry: () => void;
|
|
214
|
+
private timer: NodeJS.Timeout | null = null;
|
|
215
|
+
|
|
216
|
+
constructor(expiresAt: number, onExpiry: () => void) {
|
|
217
|
+
this.expiresAt = expiresAt;
|
|
218
|
+
this.onExpiry = onExpiry;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
start(): void {
|
|
222
|
+
const msUntilExpiry = this.expiresAt - Date.now();
|
|
223
|
+
if (msUntilExpiry <= 0) {
|
|
224
|
+
this.onExpiry();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.timer = setTimeout(() => {
|
|
228
|
+
this.onExpiry();
|
|
229
|
+
}, msUntilExpiry);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
cancel(): void {
|
|
233
|
+
if (this.timer) {
|
|
234
|
+
clearTimeout(this.timer);
|
|
235
|
+
this.timer = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
getRemainingMs(): number {
|
|
240
|
+
return Math.max(0, this.expiresAt - Date.now());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
isExpired(): boolean {
|
|
244
|
+
return Date.now() >= this.expiresAt;
|
|
245
|
+
}
|
|
246
|
+
}
|