@chahakshah/terabox-api 2.5.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/api.js ADDED
@@ -0,0 +1,2465 @@
1
+ import { FormData, Client, buildConnector, request } from 'undici';
2
+ import { Cookie, CookieJar } from 'tough-cookie';
3
+ import { filesize } from 'filesize';
4
+
5
+ import child_process from 'node:child_process';
6
+ import crypto from 'node:crypto';
7
+ import tls from 'node:tls';
8
+
9
+ /**
10
+ * Main module for api interacting with TeraBox
11
+ * @module api
12
+ */
13
+
14
+ /**
15
+ * Constructs a remote file path by combining a directory and filename, ensuring proper slash formatting
16
+ * @param {string} sdir - The directory path (with or without trailing slash)
17
+ * @param {string} sfile - The filename to append to the directory path
18
+ * @returns {string} The combined full path with exactly one slash between directory and filename
19
+ * @example
20
+ * makeRemoteFPath('documents', 'file.txt') // returns 'documents/file.txt'
21
+ * makeRemoteFPath('documents/', 'file.txt') // returns 'documents/file.txt'
22
+ * @ignore
23
+ */
24
+ function makeRemoteFPath(sdir, sfile){
25
+ const tdir = sdir.match(/\/$/) ? sdir : sdir + '/';
26
+ return tdir + sfile;
27
+ }
28
+
29
+ /**
30
+ * A utility class for handling application/x-www-form-urlencoded data
31
+ * Wraps URLSearchParams with additional convenience methods and encoding behavior
32
+ * @class
33
+ */
34
+ class FormUrlEncoded {
35
+ /**
36
+ * Creates a new FormUrlEncoded instance
37
+ * @param {Object.<string, string>} [params] - Optional initial parameters as key-value pairs
38
+ * @example
39
+ * const form = new FormUrlEncoded({ foo: 'bar', baz: 'qux' });
40
+ */
41
+ constructor(params) {
42
+ this.data = new URLSearchParams();
43
+ if(typeof params === 'object' && params !== null){
44
+ for (const [key, value] of Object.entries(params)) {
45
+ this.data.append(key, value);
46
+ }
47
+ }
48
+ }
49
+ /**
50
+ * Sets or replaces a parameter value
51
+ * @param {string} param - The parameter name
52
+ * @param {string} value - The parameter value
53
+ * @returns {void}
54
+ */
55
+ set(param, value){
56
+ this.data.set(param, value);
57
+ }
58
+ /**
59
+ * Appends a new value to an existing parameter
60
+ * @param {string} param - The parameter name
61
+ * @param {string} value - The parameter value
62
+ * @returns {void}
63
+ */
64
+ append(param, value){
65
+ this.data.append(param, value);
66
+ }
67
+ /**
68
+ * Removes a parameter
69
+ * @param {string} param - The parameter name to remove
70
+ * @returns {void}
71
+ */
72
+ delete(param){
73
+ this.data.delete(param);
74
+ }
75
+ /**
76
+ * Returns the encoded string representation (space encoded as %20)
77
+ * Suitable for application/x-www-form-urlencoded content
78
+ * @returns {string} The encoded form data
79
+ * @example
80
+ * form.str(); // returns "foo=bar&baz=qux"
81
+ */
82
+ str(){
83
+ return this.data.toString().replace(/\+/g, '%20');
84
+ }
85
+ /**
86
+ * Returns the underlying URLSearchParams object
87
+ * @returns {URLSearchParams} The native URLSearchParams instance
88
+ */
89
+ url(){
90
+ return this.data;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Generates a signed download token using a modified RC4-like algorithm
96
+ *
97
+ * This function implements a stream cipher similar to RC4 that:
98
+ * <br>1. Initializes a permutation array using the secret key (s1)
99
+ * <br>2. Generates a pseudorandom keystream
100
+ * <br>3. XORs the input data (s2) with the keystream
101
+ * <br>4. Returns the result as a Base64-encoded string
102
+ *
103
+ * @param {string} s1 - The secret key used for signing (should be at least 1 character)
104
+ * @param {string} s2 - The input data to be signed
105
+ * @returns {string} Base64-encoded signature
106
+ * @example
107
+ * const signature = signDownload('secret-key', 'data-to-sign');
108
+ * // Returns something like: "X3p8YFJjUA=="
109
+ */
110
+ function signDownload(s1, s2) {
111
+ // Initialize permutation array (p) and key array (a)
112
+ const p = new Uint8Array(256);
113
+ const a = new Uint8Array(256);
114
+ const result = [];
115
+
116
+ // Key-scheduling algorithm (KSA)
117
+ // Initialize the permutation array with the secret key
118
+ Array.from({ length: 256 }, (_, i) => {
119
+ a[i] = s1.charCodeAt(i % s1.length);
120
+ p[i] = i;
121
+ });
122
+
123
+ // Scramble the permutation array using the key
124
+ let j = 0;
125
+ Array.from({ length: 256 }, (_, i) => {
126
+ j = (j + p[i] + a[i]) % 256;
127
+ [p[i], p[j]] = [p[j], p[i]]; // swap
128
+ });
129
+
130
+ // Pseudo-random generation algorithm (PRGA)
131
+ // Generate keystream and XOR with input data
132
+ let i = 0; j = 0;
133
+ Array.from({ length: s2.length }, (_, q) => {
134
+ i = (i + 1) % 256;
135
+ j = (j + p[i]) % 256;
136
+ [p[i], p[j]] = [p[j], p[i]]; // swap
137
+ const k = p[(p[i] + p[j]) % 256];
138
+ result.push(s2.charCodeAt(q) ^ k);
139
+ });
140
+
141
+ // Return the result as Base64
142
+ return Buffer.from(result).toString('base64');
143
+ }
144
+
145
+ /**
146
+ * Validates whether a string is a properly formatted MD5 hash
147
+ * <br>
148
+ * <br>Checks if the input:
149
+ * <br>1. Is exactly 32 characters long
150
+ * <br>2. Contains only hexadecimal characters (a-f, 0-9)
151
+ * <br>3. Is in lowercase
152
+ * <br>
153
+ * <br>Note: This only validates the format, not the cryptographic correctness of the hash.
154
+ *
155
+ * @param {*} md5 - The value to check (typically a string)
156
+ * @returns {boolean} True if the input is a valid MD5 format, false otherwise
157
+ * @example
158
+ * checkMd5val('d41d8cd98f00b204e9800998ecf8427e') // returns true
159
+ * checkMd5val('D41D8CD98F00B204E9800998ECF8427E') // returns false (uppercase)
160
+ * checkMd5val('z41d8cd98f00b204e9800998ecf8427e') // returns false (invalid character)
161
+ * checkMd5val('d41d8cd98f') // returns false (too short)
162
+ */
163
+ function checkMd5val(md5){
164
+ if(typeof md5 !== 'string') return false;
165
+ return /^[a-f0-9]{32}$/.test(md5);
166
+ }
167
+
168
+ /**
169
+ * Validates that all elements in an array are properly formatted MD5 hashes
170
+ * <br>
171
+ * <br>Checks if:
172
+ * <br>1. The input is an array
173
+ * <br>2. Every element in the array passes checkMd5val() validation
174
+ * <br>(32-character hexadecimal strings in lowercase)
175
+ *
176
+ * @param {*} arr - The array to validate
177
+ * @returns {boolean} True if all elements are valid MD5 hashes, false otherwise
178
+ * (also returns false if input is not an array)
179
+ * @see {@link module:api~checkMd5val|Function CheckMd5Val} for individual MD5 hash validation logic
180
+ *
181
+ * @example
182
+ * checkMd5arr(['d41d8cd98f00b204e9800998ecf8427e', '5d41402abc4b2a76b9719d911017c592']) // true
183
+ * checkMd5arr(['d41d8cd98f00b204e9800998ecf8427e', 'invalid']) // false
184
+ * checkMd5arr('not an array') // false
185
+ * checkMd5arr([]) // false (empty array is considered invalid)
186
+ */
187
+ function checkMd5arr(arr) {
188
+ if (!Array.isArray(arr)) return false;
189
+ if (arr.length === 0) return false;
190
+ return arr.every(item => {
191
+ return checkMd5val(item);
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Applies a custom transformation to what appears to be an MD5 hash
197
+ * <br>
198
+ * <br>This function performs a series of reversible transformations on an input string
199
+ * <br>that appears to be an MD5 hash (32 hexadecimal characters). The transformation includes:
200
+ * <br>1. Character restoration at position 9
201
+ * <br>2. XOR operation with position-dependent values
202
+ * <br>3. Byte reordering of the result
203
+ *
204
+ * @param {string} md5 - The input string (expected to be 32 hexadecimal characters)
205
+ * @returns {string} The transformed result (32 hexadecimal characters)
206
+ * @throws Will return the original input unchanged if length is not 32
207
+ *
208
+ * @example
209
+ * decodeMd5('a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6') // returns transformed value
210
+ * decodeMd5('short') // returns 'short' (unchanged)
211
+ */
212
+ function decodeMd5(md5) {
213
+ // Return unchanged if not 32 characters
214
+ if (md5.length !== 32) return md5;
215
+
216
+ // Restore character at position 9
217
+ const restoredHexChar = (md5.charCodeAt(9) - 'g'.charCodeAt(0)).toString(16);
218
+ const o = md5.slice(0, 9) + restoredHexChar + md5.slice(10);
219
+
220
+ // Apply XOR transformation to each character
221
+ let n = '';
222
+ for (let i = 0; i < o.length; i++) {
223
+ const orig = parseInt(o[i], 16) ^ (i & 15);
224
+ n += orig.toString(16);
225
+ }
226
+
227
+ // Reorder the bytes in the result
228
+ const e =
229
+ n.slice(8, 16) + // original bytes 8-15 (now first)
230
+ n.slice(0, 8) + // original bytes 0-7 (now second)
231
+ n.slice(24, 32) + // original bytes 24-31 (now third)
232
+ n.slice(16, 24); // original bytes 16-23 (now last)
233
+
234
+ return e;
235
+ }
236
+
237
+ /**
238
+ * Converts between standard and URL-safe Base64 encoding formats
239
+ * <br>
240
+ * <br>Base64 strings may contain '+', '/' and '=' characters that need to be replaced
241
+ * <br>for safe use in URLs. This function provides bidirectional conversion:
242
+ * <br>- Mode 1: Converts to URL-safe Base64 (RFC 4648 §5)
243
+ * <br>- Mode 2: Converts back to standard Base64
244
+ *
245
+ * @param {string} str - The Base64 string to convert
246
+ * @param {number} [mode=1] - Conversion direction:
247
+ * 1 = to URL-safe (default),
248
+ * 2 = to standard
249
+ * @returns {string} The converted Base64 string
250
+ *
251
+ * @example
252
+ * // To URL-safe Base64
253
+ * changeBase64Type('a+b/c=') // returns 'a-b_c='
254
+ *
255
+ * // To standard Base64
256
+ * changeBase64Type('a-b_c=', 2) // returns 'a+b/c='
257
+ *
258
+ * @see {@link https://tools.ietf.org/html/rfc4648#section-5|RFC 4648 §5} for URL-safe Base64
259
+ */
260
+ function changeBase64Type(str, mode = 1) {
261
+ return mode === 1
262
+ ? str.replace(/\+/g, '-').replace(/\//g, '_') // to url-safe
263
+ : str.replace(/-/g, '+').replace(/_/g, '/'); // to standard
264
+ }
265
+
266
+ /**
267
+ * Decrypts AES-128-CBC encrypted data using provided parameters
268
+ * <br>
269
+ * <br>This function:
270
+ * <br>1. Converts both parameters from URL-safe Base64 to standard Base64
271
+ * <br>2. Extracts the IV (first 16 bytes) and ciphertext from pp1
272
+ * <br>3. Uses pp2 as the decryption key
273
+ * <br>4. Performs AES-128-CBC decryption
274
+ *
275
+ * @param {string} pp1 - Combined IV and ciphertext in URL-safe Base64 format:
276
+ * First 16 bytes are IV, remainder is ciphertext
277
+ * @param {string} pp2 - Encryption key in URL-safe Base64 format
278
+ * @returns {string} The decrypted UTF-8 string
279
+ * @throws {Error} May throw errors for invalid inputs or decryption failures
280
+ *
281
+ * @example
282
+ * // Example usage (with actual encrypted data)
283
+ * const decrypted = decryptAES(
284
+ * 'MTIzNDU2Nzg5MDEyMzQ1Ng==...', // IV + ciphertext
285
+ * 'c2VjcmV0LWtleS1kYXRhCg==' // Key
286
+ * );
287
+ *
288
+ * @requires crypto Node.js crypto module
289
+ * @see {@link module:api~changeBase64Type|Function ChangeBase64Type} for Base64 format conversion
290
+ */
291
+ function decryptAES(pp1, pp2) {
292
+ // Convert from URL-safe Base64 to standard Base64
293
+ pp1 = changeBase64Type(pp1, 2);
294
+ pp2 = changeBase64Type(pp2, 2);
295
+
296
+ // Extract ciphertext (after first 16 bytes) and IV (first 16 bytes)
297
+ const cipherText = pp1.substring(16);
298
+ const key = Buffer.from(pp2, 'utf8');
299
+ const iv = Buffer.from(pp1.substring(0, 16), 'utf8');
300
+
301
+ // Create decipher with AES-128-CBC algorithm
302
+ const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
303
+
304
+ // Perform decryption
305
+ let decrypted = decipher.update(cipherText, 'base64', 'utf8');
306
+ decrypted += decipher.final('utf8');
307
+
308
+ return decrypted;
309
+ }
310
+
311
+ /**
312
+ * Encrypts data using RSA with a public key, with optional MD5 preprocessing
313
+ * <br>
314
+ * <br>Supports two encryption modes:
315
+ * <br>1. Direct encryption of the message (default)
316
+ * <br>2. MD5 hash preprocessing (applies MD5 + length padding before encryption)
317
+ *
318
+ * @param {string} message - The plaintext message to encrypt
319
+ * @param {string|Buffer} publicKeyPEM - RSA public key in PEM format
320
+ * @param {number} [mode=1] - Encryption mode:
321
+ * 1 = direct encryption,
322
+ * 2 = MD5 hash preprocessing
323
+ * @returns {string} Base64-encoded encrypted data
324
+ * @throws {Error} May throw errors for invalid keys or encryption failures
325
+ *
326
+ * @example
327
+ * // Direct encryption
328
+ * encryptRSA('secret message', publicKey);
329
+ *
330
+ * // With MD5 preprocessing
331
+ * encryptRSA('secret message', publicKey, 2);
332
+ *
333
+ * @requires crypto Node.js crypto module
334
+ */
335
+ function encryptRSA(message, publicKeyPEM, mode = 1) {
336
+ // Mode 2: Apply MD5 hash and length padding
337
+ if (mode === 2) {
338
+ const md5 = crypto.createHash('md5').update(message).digest('hex');
339
+ message = md5 + (md5.length<10?'0':'') + md5.length;
340
+ }
341
+
342
+ // Convert message to Buffer
343
+ const buffer = Buffer.from(message, 'utf8');
344
+
345
+ // Perform RSA encryption
346
+ const encrypted = crypto.publicEncrypt({
347
+ key: publicKeyPEM,
348
+ padding: crypto.constants.RSA_PKCS1_PADDING,
349
+ },
350
+ buffer,
351
+ );
352
+
353
+ // Return as Base64 string
354
+ return encrypted.toString('base64');
355
+ }
356
+
357
+ /**
358
+ * Generates a pseudo-random SHA-1 hash from combined client parameters
359
+ * <br>
360
+ * <br>Creates a deterministic hash value by combining multiple client-specific parameters.
361
+ * <br>This is typically used for generating session tokens or unique identifiers.
362
+ *
363
+ * @param {string} [client='web'] - Client identifier (e.g., 'web', 'mobile')
364
+ * @param {string} seval - Session evaluation parameter
365
+ * @param {string} encpwd - Encrypted password or password hash
366
+ * @param {string} email - User's email address
367
+ * @param {string} [browserid=''] - Browser fingerprint or identifier
368
+ * @param {string} random - Random value
369
+ * @returns {string} SHA-1 hash of the combined parameters (40-character hex string)
370
+ *
371
+ * @example
372
+ * // Basic usage
373
+ * const token = prandGen('web', 'session123', 'encryptedPwd', 'user@example.com', 'browser123', 'randomValue');
374
+ *
375
+ * // With default client and empty browserid
376
+ * const token = prandGen(undefined, 'session123', 'encryptedPwd', 'user@example.com', '', 'randomValue');
377
+ *
378
+ * @requires crypto Node.js crypto module
379
+ */
380
+ function prandGen(client = 'web', seval, encpwd, email, browserid = '', random) {
381
+ // Combine all parameters with hyphens
382
+ const combined = `${client}-${seval}-${encpwd}-${email}-${browserid}-${random}`;
383
+
384
+ // Generate SHA-1 hash and return as hex string
385
+ return crypto.createHash('sha1').update(combined).digest('hex');
386
+ }
387
+
388
+ /**
389
+ * TeraBoxApp API client class
390
+ *
391
+ * Provides a comprehensive interface for interacting with TeraBox services,
392
+ * including encryption utilities, API request handling, and session management.
393
+ *
394
+ * @class
395
+ * @property {module:api~FormUrlEncoded } FormUrlEncoded - Form URL encoding utility
396
+ * @property {module:api~signDownload } SignDownload - Download signature generator
397
+ * @property {module:api~checkMd5val } CheckMd5Val - MD5 hash validator (single)
398
+ * @property {module:api~checkMd5arr } CheckMd5Arr - MD5 hash validator (array)
399
+ * @property {module:api~decodeMd5 } DecodeMd5 - Custom MD5 transformation
400
+ * @property {module:api~changeBase64Type } ChangeBase64Type - Base64 format converter
401
+ * @property {module:api~decryptAES } DecryptAES - AES decryption utility
402
+ * @property {module:api~encryptRSA } EncryptRSA - RSA encryption utility
403
+ * @property {module:api~prandGen } PRandGen - Pseudo-random hash generator
404
+ *
405
+ * @property {string} TERABOX_DOMAIN - Default TeraBox domain
406
+ * @property {number} TERABOX_TIMEOUT - Default API timeout (10 seconds)
407
+ *
408
+ * @property {Object} data - Application data including tokens and keys
409
+ * @property {string} data.csrf - CSRF token
410
+ * @property {string} data.logid - Log ID
411
+ * @property {string} data.pcftoken - PCF token
412
+ * @property {string} data.bdstoken - BDS token
413
+ * @property {string} data.jsToken - JavaScript token
414
+ * @property {string} data.pubkey - Public key
415
+ *
416
+ * @property {TeraBoxAppParams} params - Application parameters and configuration
417
+ */
418
+ class TeraBoxApp {
419
+ // Encryption/Utility Methods 1
420
+ FormUrlEncoded = FormUrlEncoded;
421
+ SignDownload = signDownload;
422
+ CheckMd5Val = checkMd5val;
423
+ CheckMd5Arr = checkMd5arr;
424
+ DecodeMd5 = decodeMd5;
425
+
426
+ // Encryption/Utility Methods 2
427
+ ChangeBase64Type = changeBase64Type;
428
+ DecryptAES = decryptAES;
429
+ EncryptRSA = encryptRSA;
430
+ PRandGen = prandGen;
431
+
432
+ // Constants
433
+ TERABOX_DOMAIN = 'terabox.com';
434
+ TERABOX_TIMEOUT = 10000;
435
+
436
+ // app data
437
+ data = {
438
+ csrf: '',
439
+ logid: '0',
440
+ pcftoken: '',
441
+ bdstoken: '',
442
+ jsToken: '',
443
+ pubkey: '',
444
+ };
445
+
446
+ // Application parameters and configuration
447
+ params = {
448
+ whost: 'https://jp.' + this.TERABOX_DOMAIN,
449
+ uhost: 'https://c-jp.' + this.TERABOX_DOMAIN,
450
+ lang: 'en',
451
+ app: {
452
+ app_id: 250528,
453
+ web: 1,
454
+ channel: 'dubox',
455
+ clienttype: 0, // 5 is wap?
456
+ },
457
+ ver_android: '3.44.2',
458
+ ua: 'terabox;1.40.0.132;PC;PC-Windows;10.0.26100;WindowsTeraBox',
459
+ cookie: '',
460
+ auth: {},
461
+ account_id: 0,
462
+ account_name: '',
463
+ is_vip: false,
464
+ vip_type: 0,
465
+ space_used: 0,
466
+ space_total: Math.pow(1024, 3),
467
+ space_available: Math.pow(1024, 3),
468
+ cursor: 'null',
469
+ };
470
+
471
+ /**
472
+ * Creates a new TeraBoxApp instance
473
+ * @param {string} authData - Authentication data (NDUS token)
474
+ * @param {string} [authType='ndus'] - Authentication type (currently only 'ndus' supported)
475
+ * @throws {Error} Throws error if authType is not supported
476
+ */
477
+ constructor(authData, authType = 'ndus') {
478
+ this.params.cookie = `lang=${this.params.lang}`;
479
+ if(authType === 'ndus'){
480
+ this.params.cookie += authData ? '; ndus=' + authData : '';
481
+ }
482
+ else{
483
+ throw new Error('initTBApp', { cause: 'AuthType Not Supported!' });
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Updates application data including tokens and user information
489
+ * @param {string} [customPath] - Custom path to use for the update request
490
+ * @param {number} [retries=4] - Number of retry attempts
491
+ * @returns {Promise<Object>} The updated template data
492
+ * @async
493
+ * @throws {Error} Throws error if request fails or parsing fails
494
+ */
495
+ async updateAppData(customPath, retries = 4){
496
+ const url = new URL(this.params.whost + (customPath ? `/${customPath}` : '/main'));
497
+
498
+ try{
499
+ const req = await request(url, {
500
+ headers:{
501
+ 'User-Agent': this.params.ua,
502
+ 'Cookie': this.params.cookie,
503
+ },
504
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT + 10000),
505
+ });
506
+
507
+ if(req.statusCode === 302){
508
+ const newUrl = new URL(req.headers.location);
509
+ if(this.params.whost !== newUrl.origin){
510
+ this.params.whost = newUrl.origin;
511
+ console.warn(`[WARN] Default hostname changed to ${newUrl.origin}`);
512
+ }
513
+ const toPathname = newUrl.pathname.replace(/^\//, '');
514
+ const finalUrl = toPathname + newUrl.search;
515
+ return await this.updateAppData(finalUrl, retries);
516
+ }
517
+
518
+ if(req.headers['set-cookie']){
519
+ const cJar = new CookieJar();
520
+ this.params.cookie.split(';').map(cookie => cJar.setCookieSync(cookie, this.params.whost));
521
+ if(typeof req.headers['set-cookie'] === 'string'){
522
+ req.headers['set-cookie'] = [req.headers['set-cookie']];
523
+ }
524
+ for(const cookie of req.headers['set-cookie']){
525
+ cJar.setCookieSync(cookie.split('; ')[0], this.params.whost);
526
+ }
527
+ this.params.cookie = cJar.getCookiesSync(this.params.whost).map(cookie => cookie.cookieString()).join('; ');
528
+ }
529
+
530
+ const rdata = await req.body.text();
531
+ const tdataRegex = /<script>var templateData = (.*);<\/script>/;
532
+ const jsTokenRegex = /window.jsToken%20%3D%20a%7D%3Bfn%28%22(.*)%22%29/;
533
+ const tdata = rdata.match(tdataRegex) ? JSON.parse(rdata.match(tdataRegex)[1].split(';</script>')[0]) : {};
534
+ const isLoginReq = req.headers.location === '/login' ? true : false;
535
+
536
+ if(tdata.jsToken){
537
+ tdata.jsToken = tdata.jsToken.match(/%28%22(.*)%22%29/)[1];
538
+ }
539
+ else if(rdata.match(jsTokenRegex)){
540
+ tdata.jsToken = rdata.match(jsTokenRegex)[1];
541
+ }
542
+ else if(isLoginReq){
543
+ console.error('[ERROR] Failed to update jsToken [Login Required]');
544
+ }
545
+
546
+ if(req.headers.logid){
547
+ this.data.logid = req.headers.logid;
548
+ }
549
+
550
+ this.data.csrf = tdata.csrf || '';
551
+ this.data.pcftoken = tdata.pcftoken || '';
552
+ this.data.bdstoken = tdata.bdstoken || '';
553
+ this.data.jsToken = tdata.jsToken || '';
554
+
555
+ this.params.account_id = parseInt(tdata.uk) || 0;
556
+ if(typeof tdata.userVipIdentity === 'number' && tdata.userVipIdentity > 0){
557
+ this.params.is_vip = true;
558
+ this.params.vip_type = 1;
559
+ }
560
+
561
+ return tdata;
562
+ }
563
+ catch(error){
564
+ if(error.name === 'TimeoutError' && retries > 0){
565
+ await new Promise(resolve => setTimeout(resolve, 500));
566
+ return await this.updateAppData(customPath, retries - 1);
567
+ }
568
+ const errorPrefix = '[ERROR] Failed to update jsToken:';
569
+ if(error.name === 'TimeoutError'){
570
+ console.error(errorPrefix, error.message);
571
+ return;
572
+ }
573
+ error = new Error('updateAppData', { cause: error });
574
+ console.error(errorPrefix, error);
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Sets default VIP parameters
580
+ * @returns {void}
581
+ */
582
+ setVipDefaults(){
583
+ this.params.is_vip = true;
584
+ this.params.vip_type = 1; // 1: VIP, 2: SVIP
585
+ this.params.space_total = Math.pow(1024, 3) * 2;
586
+ this.params.space_available = Math.pow(1024, 3) * 2;
587
+ }
588
+
589
+ /**
590
+ * Makes an API request with retry logic
591
+ * @param {string} req_url - The request URL (relative to whost)
592
+ * @param {Object} [req_options={}] - Request options (headers, body, etc.)
593
+ * @param {number} [retries=4] - Number of retry attempts
594
+ * @returns {Promise<Object>} The JSON-parsed response data
595
+ * @async
596
+ * @throws {Error} Throws error if all retries fail
597
+ */
598
+ async doReq(req_url, req_options = {}, retries = 4){
599
+ const url = new URL(this.params.whost + req_url);
600
+ let reqm_options = structuredClone(req_options);
601
+ let req_headers = {};
602
+
603
+ if(reqm_options.headers){
604
+ req_headers = reqm_options.headers;
605
+ delete reqm_options.headers;
606
+ }
607
+
608
+ const save_cookies = reqm_options.save_cookies;
609
+ delete reqm_options.save_cookies;
610
+ const silent_retry = reqm_options.silent_retry;
611
+ delete reqm_options.silent_retry;
612
+ const req_timeout = reqm_options.timeout ? reqm_options.timeout : this.TERABOX_TIMEOUT;
613
+ delete reqm_options.timeout;
614
+
615
+ try {
616
+ const options = {
617
+ headers: {
618
+ 'User-Agent': this.params.ua,
619
+ 'Cookie': this.params.cookie,
620
+ ...req_headers,
621
+ },
622
+ ...reqm_options,
623
+ signal: AbortSignal.timeout(req_timeout),
624
+ };
625
+
626
+ const req = await request(url, options);
627
+
628
+ if(save_cookies && req.headers['set-cookie']){
629
+ const cJar = new CookieJar();
630
+ this.params.cookie.split(';').map(cookie => cJar.setCookieSync(cookie, this.params.whost));
631
+ if(typeof req.headers['set-cookie'] === 'string'){
632
+ req.headers['set-cookie'] = [req.headers['set-cookie']];
633
+ }
634
+ for(const cookie of req.headers['set-cookie']){
635
+ cJar.setCookieSync(cookie.split('; ')[0], this.params.whost);
636
+ }
637
+ this.params.cookie = cJar.getCookiesSync(this.params.whost).map(cookie => cookie.cookieString()).join('; ');
638
+ }
639
+
640
+ const rdata = await req.body.json();
641
+ return rdata;
642
+ }
643
+ catch(error){
644
+ if (retries > 0) {
645
+ await new Promise(resolve => setTimeout(resolve, 500));
646
+ if(!silent_retry){
647
+ console.error('[ERROR] DoReq:', req_url, '|', error.code, ':', error.message, '(retrying...)');
648
+ }
649
+ return await this.doReq(req_url, req_options, retries - 1);
650
+ }
651
+ throw new Error('doReq', { cause: error });
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Retrieves system configuration from the TeraBox API
657
+ * @returns {Promise<Object>} The system configuration JSON data
658
+ * @async
659
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
660
+ */
661
+ async getSysCfg(){
662
+ const url = new URL(this.params.whost + '/api/getsyscfg');
663
+ url.search = new URLSearchParams({
664
+ clienttype: this.params.app.clienttype,
665
+ language_type: this.params.lang,
666
+ cfg_category_keys: '[]',
667
+ version: 0,
668
+ });
669
+
670
+ try{
671
+ const req = await request(url, {
672
+ headers: {
673
+ 'User-Agent': this.params.ua,
674
+ // 'Cookie': this.params.cookie,
675
+ },
676
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
677
+ });
678
+
679
+ if (req.statusCode !== 200) {
680
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
681
+ }
682
+
683
+ const rdata = await req.body.json();
684
+ return rdata;
685
+ }
686
+ catch(error){
687
+ throw new Error('getSysCfg', { cause: error });
688
+ }
689
+ }
690
+
691
+ /**
692
+ * Checks login status of the current session.
693
+ * @returns {Promise<CheckLoginResponse>} The login status JSON data.
694
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails.
695
+ * @async
696
+ */
697
+ async checkLogin(){
698
+ const url = new URL(this.params.whost + '/api/check/login');
699
+
700
+ try{
701
+ const req = await request(url, {
702
+ headers: {
703
+ 'User-Agent': this.params.ua,
704
+ 'Cookie': this.params.cookie,
705
+ },
706
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
707
+ });
708
+
709
+ if (req.statusCode !== 200) {
710
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
711
+ }
712
+
713
+ const regionPrefix = req.headers['region-domain-prefix'];
714
+ if(regionPrefix){
715
+ const newHostname = `https://${regionPrefix}.${this.TERABOX_DOMAIN}`;
716
+ console.warn(`[WARN] Default hostname changed to ${newHostname}`);
717
+ this.params.whost = new URL(newHostname).origin;
718
+ return await this.checkLogin();
719
+ }
720
+
721
+ const rdata = await req.body.json();
722
+ if(rdata.errno === 0){
723
+ this.params.account_id = rdata.uk;
724
+ }
725
+ return rdata;
726
+ }
727
+ catch(error){
728
+ throw new Error('checkLogin', { cause: error });
729
+ }
730
+ }
731
+
732
+ /**
733
+ * Initiates the pre-login step for passport authentication
734
+ * @param {string} email - The user's email address
735
+ * @returns {Promise<Object>} The pre-login data JSON (includes seval, random, timestamp)
736
+ * @async
737
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
738
+ */
739
+ async passportPreLogin(email){
740
+ const url = new URL(this.params.whost + '/passport/prelogin');
741
+ const authUrl = 'wap/outlogin/login';
742
+
743
+ try{
744
+ if(this.data.pcftoken === ''){
745
+ await this.updateAppData(authUrl);
746
+ }
747
+
748
+ const formData = new this.FormUrlEncoded();
749
+ formData.append('client', 'web');
750
+ formData.append('pass_version', '2.8');
751
+ formData.append('clientfrom', 'h5');
752
+ formData.append('pcftoken', this.data.pcftoken);
753
+ formData.append('email', email);
754
+
755
+ const req = await request(url, {
756
+ method: 'POST',
757
+ headers: {
758
+ 'Content-Type': 'application/x-www-form-urlencoded',
759
+ 'User-Agent': this.params.ua,
760
+ 'Cookie': this.params.cookie,
761
+ Referer: this.params.whost,
762
+ },
763
+ body: formData.str(),
764
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
765
+ });
766
+
767
+ if (req.statusCode !== 200) {
768
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
769
+ }
770
+
771
+ const rdata = await req.body.json();
772
+ return rdata;
773
+ }
774
+ catch (error) {
775
+ throw new Error('passportPreLogin', { cause: error });
776
+ }
777
+ }
778
+
779
+ /**
780
+ * Completes the passport login process using preLoginData and password
781
+ * @param {Object} preLoginData - Data returned from passportPreLogin
782
+ * @param {string} preLoginData.seval - The seval value from pre-login.
783
+ * @param {string} preLoginData.random - The random value from pre-login.
784
+ * @param {number} preLoginData.timestamp - The timestamp from pre-login.
785
+ * @param {string} email - The user's email address
786
+ * @param {string} pass - The user's plaintext password
787
+ * @returns {Promise<Object>} The login response JSON (includes ndus token on success)
788
+ * @async
789
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
790
+ */
791
+ async passportLogin(preLoginData, email, pass){
792
+ const url = new URL(this.params.whost + '/passport/login');
793
+
794
+ try{
795
+ if(this.data.pubkey === ''){
796
+ await this.getPublicKey();
797
+ }
798
+
799
+ const cJar = new CookieJar();
800
+ this.params.cookie.split(';').map(cookie => cJar.setCookieSync(cookie, this.params.whost));
801
+ const browserid = cJar.toJSON().cookies.find(c => c.key === 'browserid').value || '';
802
+ const encpwd = this.ChangeBase64Type(this.EncryptRSA(pass, this.data.pubkey, 2));
803
+
804
+ const prand = this.PRandGen('web', preLoginData.seval, encpwd, email, browserid, preLoginData.random);
805
+
806
+ const formData = new this.FormUrlEncoded();
807
+ formData.append('client', 'web');
808
+ formData.append('pass_version', '2.8');
809
+ formData.append('clientfrom', 'h5');
810
+ formData.append('pcftoken', this.data.pcftoken);
811
+ formData.append('prand', prand);
812
+ formData.append('email', email);
813
+ formData.append('pwd', encpwd);
814
+ formData.append('seval', preLoginData.seval);
815
+ formData.append('random', preLoginData.random);
816
+ formData.append('timestamp', preLoginData.timestamp);
817
+
818
+ const req = await request(url, {
819
+ method: 'POST',
820
+ headers: {
821
+ 'Content-Type': 'application/x-www-form-urlencoded',
822
+ 'User-Agent': this.params.ua,
823
+ 'Cookie': this.params.cookie,
824
+ Referer: this.params.whost,
825
+ },
826
+ body: formData.str(),
827
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
828
+ });
829
+
830
+ if (req.statusCode !== 200) {
831
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
832
+ }
833
+
834
+ const rdata = await req.body.json();
835
+ if(rdata.code === 0){
836
+ if(typeof req.headers['set-cookie'] === 'string'){
837
+ req.headers['set-cookie'] = [req.headers['set-cookie']];
838
+ }
839
+ for(const cookie of req.headers['set-cookie']){
840
+ cJar.setCookieSync(cookie.split('; ')[0], this.params.whost);
841
+ }
842
+ const ndus = cJar.toJSON().cookies.find(c => c.key === 'ndus').value;
843
+ rdata.data.ndus = ndus;
844
+ }
845
+ return rdata;
846
+ }
847
+ catch (error) {
848
+ throw new Error('passportLogin', { cause: error });
849
+ }
850
+ }
851
+
852
+ /**
853
+ * Sends a registration code to the specified email
854
+ * @param {string} email - The email address to send the code to
855
+ * @returns {Promise<Object>} The send code response JSON (includes code and message)
856
+ * @async
857
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
858
+ */
859
+ async regSendCode(email){
860
+ const url = new URL(this.params.whost + '/passport/register_v4/sendcode');
861
+ const emailRegUrl = 'wap/outlogin/emailRegister';
862
+
863
+ try{
864
+ if(this.data.pcftoken === ''){
865
+ await this.updateAppData(emailRegUrl);
866
+ }
867
+
868
+ const formData = new this.FormUrlEncoded();
869
+ formData.append('client', 'web');
870
+ formData.append('pass_version', '2.8');
871
+ formData.append('clientfrom', 'h5');
872
+ formData.append('pcftoken', this.data.pcftoken);
873
+ formData.append('email', email);
874
+
875
+ const req = await request(url, {
876
+ method: 'POST',
877
+ headers: {
878
+ 'Content-Type': 'application/x-www-form-urlencoded',
879
+ 'User-Agent': this.params.ua,
880
+ 'Cookie': this.params.cookie,
881
+ Referer: this.params.whost,
882
+ },
883
+ body: formData.str(),
884
+ });
885
+
886
+ if (req.statusCode !== 200) {
887
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
888
+ }
889
+
890
+ const rdata = await req.body.json();
891
+ // rdata.code: 0 - OK
892
+ // rdata.code: 10 - Email format invalid
893
+ // rdata.code: 11 - Email has been register before
894
+ // rdata.code: 60 - Send code too fast, wait ~60sec
895
+ return rdata;
896
+ }
897
+ catch (error) {
898
+ throw new Error('regSendCode', { cause: error });
899
+ }
900
+ }
901
+
902
+ /**
903
+ * Verifies the registration code received via email
904
+ * @param {string} regToken - Registration token from send code response
905
+ * @param {string|number} code - The verification code sent to email
906
+ * @returns {Promise<Object>} The verification response JSON
907
+ * @async
908
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
909
+ */
910
+ async regVerify(regToken, code){
911
+ const url = new URL(this.params.whost + '/passport/register_v4/verify');
912
+
913
+ try{
914
+ const formData = new this.FormUrlEncoded();
915
+ formData.append('client', 'web');
916
+ formData.append('pass_version', '2.8');
917
+ formData.append('clientfrom', 'h5');
918
+ formData.append('pcftoken', this.data.pcftoken);
919
+ formData.append('token', regToken);
920
+ formData.append('code', code);
921
+
922
+ const req = await request(url, {
923
+ method: 'POST',
924
+ headers: {
925
+ 'Content-Type': 'application/x-www-form-urlencoded',
926
+ 'User-Agent': this.params.ua,
927
+ 'Cookie': this.params.cookie,
928
+ Referer: this.params.whost,
929
+ },
930
+ body: formData.str(),
931
+ });
932
+
933
+ if (req.statusCode !== 200) {
934
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
935
+ }
936
+
937
+ const rdata = await req.body.json();
938
+ // rdata.code: 0 - OK
939
+ // rdata.code: 59 - Email code is wrong
940
+ return rdata;
941
+ }
942
+ catch (error) {
943
+ throw new Error('regVerify', { cause: error });
944
+ }
945
+ }
946
+
947
+ /**
948
+ * Completes the registration process by setting a password
949
+ * @param {string} regToken - Registration token from verification step
950
+ * @param {string} pass - The new password to set, length is 6-15 and contains at least 1 Latin letter
951
+ * @returns {Promise<Object>} The finish registration response JSON (includes ndus token on success)
952
+ * @async
953
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
954
+ */
955
+ async regFinish(regToken, pass){
956
+ const url = new URL(this.params.whost + '/passport/register_v4/finish');
957
+
958
+ try{
959
+ if(this.data.pubkey === ''){
960
+ await this.getPublicKey();
961
+ }
962
+
963
+ if(typeof pass !== 'string' || pass.length < 6 || pass.length > 15 || !pass.match(/[a-z]/i)){
964
+ return { code: -2, logid: 0, msg: 'invalid password', };
965
+ }
966
+
967
+ const encpwd = this.ChangeBase64Type(this.EncryptRSA(pass, this.data.pubkey, 2));
968
+
969
+ const formData = new this.FormUrlEncoded();
970
+ formData.append('client', 'web');
971
+ formData.append('pass_version', '2.8');
972
+ formData.append('clientfrom', 'h5');
973
+ formData.append('pcftoken', this.data.pcftoken);
974
+ formData.append('token', regToken);
975
+ formData.append('pwd', encpwd);
976
+
977
+ const req = await request(url, {
978
+ method: 'POST',
979
+ headers: {
980
+ 'Content-Type': 'application/x-www-form-urlencoded',
981
+ 'User-Agent': this.params.ua,
982
+ 'Cookie': this.params.cookie,
983
+ Referer: this.params.whost,
984
+ },
985
+ body: formData.str(),
986
+ });
987
+
988
+ if (req.statusCode !== 200) {
989
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
990
+ }
991
+
992
+ const rdata = await req.body.json();
993
+ if(rdata.code === 0 && req.headers['set-cookie']){
994
+ const cJar = new CookieJar();
995
+
996
+ if(typeof req.headers['set-cookie'] === 'string'){
997
+ req.headers['set-cookie'] = [req.headers['set-cookie']];
998
+ }
999
+ for(const cookie of req.headers['set-cookie']){
1000
+ cJar.setCookieSync(cookie.split('; ')[0], this.params.whost);
1001
+ }
1002
+
1003
+ const ndus = cJar.toJSON().cookies.find(c => c.key === 'ndus').value;
1004
+ rdata.data.ndus = ndus;
1005
+ }
1006
+ return rdata;
1007
+ }
1008
+ catch (error) {
1009
+ throw new Error('regFinish', { cause: error });
1010
+ }
1011
+ }
1012
+
1013
+ /**
1014
+ * Retrieves passport user information for the current session
1015
+ * @returns {Promise<Object>} The passport user info JSON (includes display_name)
1016
+ * @async
1017
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1018
+ */
1019
+ async passportGetInfo(){
1020
+ const url = new URL(this.params.whost + '/passport/get_info');
1021
+
1022
+ try{
1023
+ const req = await request(url, {
1024
+ headers: {
1025
+ 'User-Agent': this.params.ua,
1026
+ 'Cookie': this.params.cookie,
1027
+ },
1028
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1029
+ });
1030
+
1031
+ if (req.statusCode !== 200) {
1032
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1033
+ }
1034
+
1035
+ const rdata = await req.body.json();
1036
+ if(rdata.errno === 0){
1037
+ this.params.account_name = rdata.data.display_name;
1038
+ }
1039
+ return rdata;
1040
+ }
1041
+ catch (error) {
1042
+ throw new Error('getPassport', { cause: error });
1043
+ }
1044
+ }
1045
+
1046
+ /**
1047
+ * Fetches membership information for the current user
1048
+ * @returns {Promise<Object>} The membership JSON (includes VIP status)
1049
+ * @async
1050
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1051
+ */
1052
+ async userMembership(){
1053
+ const url = new URL(this.params.whost + '/rest/2.0/membership/proxy/user');
1054
+ url.search = new URLSearchParams({
1055
+ method: 'query',
1056
+ });
1057
+
1058
+ try{
1059
+ const req = await request(url, {
1060
+ headers: {
1061
+ 'User-Agent': this.params.ua,
1062
+ 'Cookie': this.params.cookie,
1063
+ },
1064
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1065
+ });
1066
+
1067
+ if (req.statusCode !== 200) {
1068
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1069
+ }
1070
+
1071
+ const rdata = await req.body.json();
1072
+ if(rdata.error_code === 0){
1073
+ this.params.is_vip = rdata.data.member_info.is_vip > 0 ? true : false;
1074
+ // this.params.vip_type = this.params.is_vip ? 2 : 0;
1075
+ if(this.params.is_vip === 0){
1076
+ this.params.vip_type = 0;
1077
+ }
1078
+ }
1079
+ return rdata;
1080
+ }
1081
+ catch(error){
1082
+ throw new Error('userMembership', { cause: error });
1083
+ }
1084
+ }
1085
+
1086
+ /**
1087
+ * Retrieves current user information (username, VIP status)
1088
+ * @returns {Promise<Object>} The user info JSON (includes records array)
1089
+ * @async
1090
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1091
+ */
1092
+ async getCurrentUserInfo(){
1093
+ try{
1094
+ if(this.params.account_id === 0){
1095
+ await this.checkLogin();
1096
+ }
1097
+
1098
+ const curUser = await this.getUserInfo(this.params.account_id);
1099
+ if(curUser.records.length > 0){
1100
+ const thisUser = curUser.records[0];
1101
+ this.params.account_name = thisUser.uname;
1102
+ this.params.is_vip = thisUser.vip_type > 0 ? true : false;
1103
+ this.params.vip_type = thisUser.vip_type;
1104
+ }
1105
+ return curUser;
1106
+ }
1107
+ catch (error) {
1108
+ throw new Error('getCurrentUserInfo', { cause: error });
1109
+ }
1110
+ }
1111
+
1112
+ /**
1113
+ * Retrieves information for a specific user ID
1114
+ * @param {number|string} user_id - The user ID to look up
1115
+ * @returns {Promise<Object>} The user info JSON (includes data)
1116
+ * @async
1117
+ * @throws {Error} Throws error if user_id is invalid, HTTP status is not 200, or request fails
1118
+ */
1119
+ async getUserInfo(user_id){
1120
+ user_id = parseInt(user_id);
1121
+ const url = new URL(this.params.whost + '/api/user/getinfo');
1122
+ url.search = new URLSearchParams({
1123
+ user_list: JSON.stringify([user_id]),
1124
+ need_relation: 0,
1125
+ need_secret_info: 1,
1126
+ });
1127
+
1128
+ try{
1129
+ if(isNaN(user_id) || !Number.isSafeInteger(user_id)){
1130
+ throw new Error(`${user_id} is not user id`);
1131
+ }
1132
+
1133
+ const req = await request(url, {
1134
+ headers: {
1135
+ 'User-Agent': this.params.ua,
1136
+ 'Cookie': this.params.cookie,
1137
+ },
1138
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1139
+ });
1140
+
1141
+ if (req.statusCode !== 200) {
1142
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1143
+ }
1144
+
1145
+ const rdata = await req.body.json();
1146
+ return rdata;
1147
+ }
1148
+ catch (error) {
1149
+ throw new Error('getUserInfo', { cause: error });
1150
+ }
1151
+ }
1152
+
1153
+ /**
1154
+ * Retrieves storage quota information for the current account
1155
+ * @returns {Promise<Object>} The quota JSON (includes total, used, available)
1156
+ * @async
1157
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1158
+ */
1159
+ async getQuota(){
1160
+ const url = new URL(this.params.whost + '/api/quota');
1161
+ url.search = new URLSearchParams({
1162
+ checkexpire: 1,
1163
+ checkfree: 1,
1164
+ });
1165
+
1166
+ try{
1167
+ const req = await request(url, {
1168
+ headers: {
1169
+ 'User-Agent': this.params.ua,
1170
+ 'Cookie': this.params.cookie,
1171
+ },
1172
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1173
+ });
1174
+
1175
+ if (req.statusCode !== 200) {
1176
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1177
+ }
1178
+
1179
+ const rdata = await req.body.json();
1180
+ if(rdata.errno === 0){
1181
+ rdata.available = rdata.total - rdata.used;
1182
+ this.params.space_available = rdata.available;
1183
+ this.params.space_total = rdata.total;
1184
+ this.params.space_used = rdata.used;
1185
+ }
1186
+ return rdata;
1187
+ }
1188
+ catch (error) {
1189
+ throw new Error('getQuota', { cause: error });
1190
+ }
1191
+ }
1192
+
1193
+ /**
1194
+ * Retrieves the user's coins count (points)
1195
+ * @returns {Promise<Object>} The coins count JSON (includes records of coin usage)
1196
+ * @async
1197
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1198
+ */
1199
+ async getCoinsCount(){
1200
+ const url = new URL(this.params.whost + '/rest/1.0/inte/system/getrecord');
1201
+
1202
+ try{
1203
+ const req = await request(url, {
1204
+ headers: {
1205
+ 'User-Agent': this.params.ua,
1206
+ 'Cookie': this.params.cookie,
1207
+ },
1208
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1209
+ });
1210
+
1211
+ if (req.statusCode !== 200) {
1212
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1213
+ }
1214
+
1215
+ const rdata = await req.body.json();
1216
+ return rdata;
1217
+ }
1218
+ catch (error) {
1219
+ throw new Error('getCoinsCount', { cause: error });
1220
+ }
1221
+ }
1222
+
1223
+ /**
1224
+ * Retrieves the contents of a remote directory
1225
+ * @param {string} remoteDir - Remote directory path to list
1226
+ * @param {number} [page=1] - Page number for pagination
1227
+ * @returns {Promise<Object>} The directory listing JSON (includes entries array)
1228
+ * @async
1229
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1230
+ */
1231
+ async getRemoteDir(remoteDir, page = 1){
1232
+ const url = new URL(this.params.whost + '/api/list');
1233
+
1234
+ try{
1235
+ const formData = new this.FormUrlEncoded();
1236
+ formData.append('order', 'name');
1237
+ formData.append('desc', 0);
1238
+ formData.append('dir', remoteDir);
1239
+ formData.append('num', 20000);
1240
+ formData.append('page', page);
1241
+ formData.append('showempty', 0);
1242
+
1243
+ const req = await request(url, {
1244
+ method: 'POST',
1245
+ body: formData.str(),
1246
+ headers: {
1247
+ 'Content-Type': 'application/x-www-form-urlencoded',
1248
+ 'User-Agent': this.params.ua,
1249
+ 'Cookie': this.params.cookie,
1250
+ },
1251
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1252
+ });
1253
+
1254
+ if (req.statusCode !== 200) {
1255
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1256
+ }
1257
+
1258
+ const rdata = await req.body.json();
1259
+ return rdata;
1260
+ }
1261
+ catch (error) {
1262
+ throw new Error('getRemoteDir', { cause: error });
1263
+ }
1264
+ }
1265
+
1266
+ /**
1267
+ * Retrieves the contents of a remote directory with specific file category
1268
+ * @param {number} [categoryId=1] - selected category:
1269
+ * <br>1: video
1270
+ * <br>2: audio
1271
+ * <br>3: pictures
1272
+ * <br>4: documents
1273
+ * <br>5: apps
1274
+ * <br>6: other
1275
+ * <br>7: torrent
1276
+ * @param {string} remoteDir - Remote directory path to list
1277
+ * @param {number} [page=1] - Page number for pagination
1278
+ * @returns {Promise<Object>} The directory listing JSON (includes entries array)
1279
+ * @async
1280
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1281
+ */
1282
+ async getCategoryList(categoryId = 1, remoteDir = '/', page = 1, order = 'name', desc = 0, num = 20000){
1283
+ const url = new URL(this.params.whost + '/api/categorylist');
1284
+
1285
+ try{
1286
+ const formData = new this.FormUrlEncoded();
1287
+ formData.append('order', order);
1288
+ formData.append('desc', desc);
1289
+ formData.append('dir', remoteDir);
1290
+ formData.append('num', num);
1291
+ formData.append('page', page);
1292
+ formData.append('category', categoryId);
1293
+
1294
+ const req = await request(url, {
1295
+ method: 'POST',
1296
+ body: formData.str(),
1297
+ headers: {
1298
+ 'Content-Type': 'application/x-www-form-urlencoded',
1299
+ 'User-Agent': this.params.ua,
1300
+ 'Cookie': this.params.cookie,
1301
+ },
1302
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1303
+ });
1304
+
1305
+ if (req.statusCode !== 200) {
1306
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1307
+ }
1308
+
1309
+ const rdata = await req.body.json();
1310
+ return rdata;
1311
+ }
1312
+ catch (error) {
1313
+ throw new Error('getCategoryList', { cause: error });
1314
+ }
1315
+ }
1316
+
1317
+ /**
1318
+ * Retrieves the contents of the recycle bin
1319
+ * @returns {Promise<Object>} The recycle bin listing JSON (includes entries array)
1320
+ * @async
1321
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1322
+ */
1323
+ async getRecycleBin(page = 1){
1324
+ const url = new URL(this.params.whost + '/api/recycle/list');
1325
+
1326
+ try{
1327
+ url.search = new URLSearchParams({
1328
+ // order: 'name',
1329
+ desc: 0,
1330
+ num: 20000,
1331
+ page: page,
1332
+ });
1333
+
1334
+
1335
+ const req = await request(url, {
1336
+ headers: {
1337
+ 'User-Agent': this.params.ua,
1338
+ 'Cookie': this.params.cookie,
1339
+ },
1340
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1341
+ });
1342
+
1343
+ if (req.statusCode !== 200) {
1344
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1345
+ }
1346
+
1347
+ const rdata = await req.body.json();
1348
+ return rdata;
1349
+ }
1350
+ catch (error) {
1351
+ throw new Error('getRecycleBin', { cause: error });
1352
+ }
1353
+ }
1354
+
1355
+ /**
1356
+ * Clears all items in the recycle bin
1357
+ * @returns {Promise<Object>} The clear recycle bin response JSON
1358
+ * @async
1359
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1360
+ */
1361
+ async clearRecycleBin(){
1362
+ const url = new URL(this.params.whost + '/api/recycle/clear');
1363
+
1364
+ try{
1365
+ url.search = new URLSearchParams({
1366
+ 'async': 1,
1367
+ });
1368
+
1369
+ const req = await request(url, {
1370
+ headers: {
1371
+ 'User-Agent': this.params.ua,
1372
+ 'Cookie': this.params.cookie,
1373
+ },
1374
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1375
+ });
1376
+
1377
+ if (req.statusCode !== 200) {
1378
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1379
+ }
1380
+
1381
+ const rdata = await req.body.json();
1382
+ return rdata;
1383
+ }
1384
+ catch (error) {
1385
+ throw new Error('clearRecycleBin', { cause: error });
1386
+ }
1387
+ }
1388
+
1389
+ /**
1390
+ * Initiates a precreate request for a file (reserve upload ID and pre-upload checks)
1391
+ * @param {Object} data - File data including remote_dir, file, size, upload_id (optional), and hash info
1392
+ * @param {string} data.remote_dir - Remote directory path
1393
+ * @param {string} data.file - Filename
1394
+ * @param {number} data.size - File size in bytes
1395
+ * @param {string} [data.upload_id] - Existing upload ID for resuming
1396
+ * @param {Object} data.hash - Hash information
1397
+ * @param {string} data.hash.file - MD5 hash of full file
1398
+ * @param {string} data.hash.slice - MD5 hash of first slice
1399
+ * @param {number} data.hash.crc32 - CRC32 value
1400
+ * @param {Array<string>} data.hash.chunks - Array of MD5 chunk hashes
1401
+ * @returns {Promise<Object>} The precreate response JSON (includes upload_id, etc.)
1402
+ * @async
1403
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1404
+ */
1405
+ async precreateFile(data){
1406
+ const formData = new this.FormUrlEncoded();
1407
+ formData.append('path', makeRemoteFPath(data.remote_dir, data.file));
1408
+ // formData.append('target_path', data.remote_dir);
1409
+ formData.append('autoinit', 1);
1410
+ formData.append('size', data.size);
1411
+ formData.append('file_limit_switch_v34', 'true');
1412
+ formData.append('block_list', '[]');
1413
+ formData.append('rtype', 2);
1414
+
1415
+ if(data.upload_id && typeof data.upload_id === 'string' && data.upload_id !== ''){
1416
+ formData.append('uploadid', data.upload_id);
1417
+ }
1418
+
1419
+ // check if has correct md5 values
1420
+ if(this.CheckMd5Val(data.hash.slice) && this.CheckMd5Val(data.hash.file)){
1421
+ formData.append('content-md5', data.hash.file);
1422
+ formData.append('slice-md5', data.hash.slice);
1423
+ }
1424
+
1425
+ // check crc32int and ignore field for crc32 out of range
1426
+ if(Number.isSafeInteger(data.hash.crc32) && data.hash.crc32 >= 0 && data.hash.crc32 <= 0xFFFFFFFF){
1427
+ formData.append('content-crc32', data.hash.crc32);
1428
+ }
1429
+
1430
+ // check chunks hash
1431
+ if(!this.CheckMd5Arr(data.hash.chunks)){
1432
+ const predefinedHash = ['5910a591dd8fc18c32a8f3df4fdc1761']
1433
+
1434
+ if(data.size > 4 * 1024 * 1024){
1435
+ predefinedHash.push('a5fc157d78e6ad1c7e114b056c92821e');
1436
+ }
1437
+
1438
+ formData.set('block_list', JSON.stringify(predefinedHash));
1439
+ }
1440
+ else{
1441
+ formData.set('block_list', JSON.stringify(data.hash.chunks));
1442
+ }
1443
+
1444
+ // formData.append('local_ctime', '');
1445
+ // formData.append('local_mtime', '');
1446
+
1447
+ const url = new URL(this.params.whost + `/api/precreate`);
1448
+
1449
+ try{
1450
+ if(this.data.jsToken === ''){
1451
+ await this.updateAppData();
1452
+ }
1453
+
1454
+ url.search = new URLSearchParams({
1455
+ ...this.params.app,
1456
+ jsToken: this.data.jsToken,
1457
+ });
1458
+
1459
+ const req = await request(url, {
1460
+ method: 'POST',
1461
+ body: formData.str(),
1462
+ headers: {
1463
+ 'Content-Type': 'application/x-www-form-urlencoded',
1464
+ 'User-Agent': this.params.ua,
1465
+ 'Cookie': this.params.cookie,
1466
+ },
1467
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1468
+ });
1469
+
1470
+ if (req.statusCode !== 200) {
1471
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1472
+ }
1473
+
1474
+ // uploadid = 'P1-' + BASE64(ServerLocalIP + ':' + ServerTime + ':' + RequestID)
1475
+ const rdata = await req.body.json();
1476
+ // rdata.errno: 4000023 - need verify
1477
+ if(rdata.errno === 4000023){
1478
+ await this.updateAppData();
1479
+ return await this.precreateFile(data);
1480
+ }
1481
+ return rdata;
1482
+ }
1483
+ catch (error) {
1484
+ throw new Error('precreateFile', { cause: error });
1485
+ }
1486
+ }
1487
+
1488
+ /**
1489
+ * Attempts a rapid upload using existing file hashes (skip actual upload if file already on server)
1490
+ * @param {Object} data - File data including remote_dir, file, size, and hash info
1491
+ * @param {string} data.remote_dir - Remote directory path
1492
+ * @param {string} data.file - Filename
1493
+ * @param {number} data.size - File size in bytes
1494
+ * @param {Object} data.hash - Hash information
1495
+ * @param {string} data.hash.file - MD5 hash of full file
1496
+ * @param {string} data.hash.slice - MD5 hash of first slice
1497
+ * @param {number} data.hash.crc32 - CRC32 value
1498
+ * @param {Array<string>} [data.hash.chunks] - Array of MD5 chunk hashes
1499
+ * @returns {Promise<Object>} The rapid upload response JSON (indicates success or fallback)
1500
+ * @async
1501
+ * @throws {Error} Throws error if file size < 256KB, invalid hashes, HTTP status is not 200, or request fails
1502
+ */
1503
+ async rapidUpload(data){
1504
+ const formData = new this.FormUrlEncoded({
1505
+ path: makeRemoteFPath(data.remote_dir, data.file),
1506
+ //target_path: data.remote_dir
1507
+ 'content-length': data.size,
1508
+ 'content-md5': data.hash.file,
1509
+ 'slice-md5': data.hash.slice,
1510
+ 'content-crc32': data.hash.crc32,
1511
+ //local_ctime: '',
1512
+ //local_mtime: '',
1513
+ block_list: JSON.stringify(data.hash.chunks || []),
1514
+ rtype: 2,
1515
+ mode: 1,
1516
+ });
1517
+
1518
+ if(!this.CheckMd5Val(data.hash.slice) || !this.CheckMd5Val(data.hash.file)){
1519
+ const badMD5 = new Error('Bad MD5 Slice Hash or MD5 File Hash');
1520
+ throw new Error('rapidUpload', { cause: badMD5 });
1521
+ }
1522
+
1523
+ if(!Number.isSafeInteger(data.hash.crc32) || data.hash.crc32 < 0 || data.hash.crc32 > 0xFFFFFFFF){
1524
+ formData.delete('content-crc32');
1525
+ }
1526
+
1527
+ if(!this.CheckMd5Arr(data.hash.chunks)){
1528
+ // use unsafe rapid upload if we don't have chunks hash
1529
+ formData.delete('block_list');
1530
+ formData.set('rtype', 3);
1531
+ }
1532
+
1533
+ const url = new URL(this.params.whost + '/api/rapidupload');
1534
+
1535
+ try{
1536
+ if(data.size < 256 * 1024){
1537
+ throw new Error(`File size too small!`);
1538
+ }
1539
+
1540
+ const req = await request(url, {
1541
+ method: 'POST',
1542
+ body: formData.str(),
1543
+ headers: {
1544
+ 'Content-Type': 'application/x-www-form-urlencoded',
1545
+ 'User-Agent': this.params.ua,
1546
+ 'Cookie': this.params.cookie,
1547
+ },
1548
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1549
+ });
1550
+
1551
+ if (req.statusCode !== 200) {
1552
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1553
+ }
1554
+
1555
+ const rdata = await req.body.json();
1556
+ // rdata.errno: 2 - already exist?
1557
+ return rdata;
1558
+ }
1559
+ catch (error) {
1560
+ throw new Error('rapidUpload', { cause: error });
1561
+ }
1562
+ }
1563
+
1564
+ /**
1565
+ * Attempts a upload file from remote server
1566
+ * @param {string} urls - Source urls (coma-separated)
1567
+ * @param {string} remote_dir - Remote directory path
1568
+ * @returns {Promise<Object>} The remote upload response JSON (indicates success or fallback)
1569
+ * @async
1570
+ * @throws {Error} Throws error if HTTP status is not 200, or request fails
1571
+ */
1572
+ async remoteUpload(urls, remote_dir = '/Remote Upload'){
1573
+ const formData = new this.FormUrlEncoded({
1574
+ urls: urls,
1575
+ upload_to: remote_dir,
1576
+ });
1577
+
1578
+ const url = new URL(this.params.whost + '/webmaster/remoteupload/submit');
1579
+
1580
+ try{
1581
+ const req = await request(url, {
1582
+ method: 'POST',
1583
+ body: formData.str(),
1584
+ headers: {
1585
+ 'Content-Type': 'application/x-www-form-urlencoded',
1586
+ 'User-Agent': this.params.ua,
1587
+ 'Cookie': this.params.cookie,
1588
+ },
1589
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1590
+ });
1591
+
1592
+ if (req.statusCode !== 200) {
1593
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1594
+ }
1595
+
1596
+ const rdata = await req.body.json();
1597
+ return rdata;
1598
+ }
1599
+ catch (error) {
1600
+ throw new Error('remoteUpload', { cause: error });
1601
+ }
1602
+ }
1603
+
1604
+ /**
1605
+ * Retrieves an upload host endpoint to use for file uploads
1606
+ * @returns {Promise<Object>} The upload host response JSON (includes host field)
1607
+ * @async
1608
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1609
+ */
1610
+ async getUploadHost(){
1611
+ const url = new URL(this.params.whost + '/rest/2.0/pcs/file?method=locateupload');
1612
+ try{
1613
+ const req = await request(url, {
1614
+ headers: {
1615
+ 'User-Agent': this.params.ua,
1616
+ 'Cookie': this.params.cookie,
1617
+ },
1618
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1619
+ });
1620
+
1621
+ if (req.statusCode !== 200) {
1622
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1623
+ }
1624
+
1625
+ const rdata = await req.body.json();
1626
+ this.params.uhost = 'https://' + rdata.host;
1627
+ return rdata;
1628
+ }
1629
+ catch (error) {
1630
+ throw new Error('getUploadHost', { cause: error });
1631
+ }
1632
+ }
1633
+
1634
+ /**
1635
+ * Uploads a single chunk (part) of a file
1636
+ * @param {Object} data - File data including remote_dir, file, upload_id
1637
+ * @param {number} partseq - The sequence number of this chunk (0-based)
1638
+ * @param {Blob|Buffer} blob - The binary data of the chunk
1639
+ * @param {function} [reqHandler] - Optional request progress handler
1640
+ * @param {AbortSignal} [externalAbort] - Optional external abort signal
1641
+ * @returns {Promise<Object>} The upload chunk response JSON (includes MD5 for chunk)
1642
+ * @async
1643
+ * @throws {Error} Throws error if HTTP status is not 200, chunk upload fails, or request times out
1644
+ */
1645
+ async uploadChunk(data, partseq, blob, reqHandler, externalAbort) {
1646
+ const timeoutAborter = new AbortController;
1647
+ const timeoutId = setTimeout(() => { timeoutAborter.abort(); }, this.TERABOX_TIMEOUT);
1648
+ externalAbort = externalAbort ? externalAbort : new AbortController().signal;
1649
+
1650
+ const url = new URL(`${this.params.uhost}/rest/2.0/pcs/superfile2`);
1651
+ url.search = new URLSearchParams({
1652
+ method: 'upload',
1653
+ ...this.params.app,
1654
+ // type: 'tmpfile',
1655
+ path: makeRemoteFPath(data.remote_dir, data.file),
1656
+ uploadid: data.upload_id,
1657
+ // uploadsign: 0,
1658
+ partseq: partseq,
1659
+ });
1660
+
1661
+ const formData = new FormData();
1662
+ formData.append('file', blob, 'blob');
1663
+
1664
+ const req = await request(url, {
1665
+ method: 'POST',
1666
+ body: formData,
1667
+ headers: {
1668
+ 'User-Agent': this.params.ua,
1669
+ 'Cookie': this.params.cookie,
1670
+ },
1671
+ signal: AbortSignal.any([
1672
+ externalAbort,
1673
+ timeoutAborter.signal,
1674
+ ]),
1675
+ });
1676
+
1677
+ clearTimeout(timeoutId);
1678
+
1679
+ if (req.statusCode !== 200) {
1680
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1681
+ }
1682
+
1683
+ const res = await req.body.json();
1684
+ if (res.error_code) {
1685
+ const uploadError = new Error(`Upload failed! Error Code #${res.error_code}`);
1686
+ uploadError.data = res;
1687
+ throw uploadError;
1688
+ }
1689
+ return res;
1690
+ }
1691
+
1692
+ /**
1693
+ * Creates a new directory in the remote file system
1694
+ * @param {string} remoteDir - The path of the directory to create
1695
+ * @returns {Promise<Object>} The create directory response JSON
1696
+ * @async
1697
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1698
+ */
1699
+ async createDir(remoteDir){
1700
+ const formData = new this.FormUrlEncoded();
1701
+ formData.append('path', remoteDir);
1702
+ formData.append('isdir', 1);
1703
+ formData.append('block_list', '[]');
1704
+
1705
+ const url = new URL(this.params.whost + '/api/create?a=commit');
1706
+
1707
+ try{
1708
+ const req = await request(url, {
1709
+ method: 'POST',
1710
+ body: formData.str(),
1711
+ headers: {
1712
+ 'Content-Type': 'application/x-www-form-urlencoded',
1713
+ 'User-Agent': this.params.ua,
1714
+ 'Cookie': this.params.cookie,
1715
+ },
1716
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1717
+ });
1718
+
1719
+ if (req.statusCode !== 200) {
1720
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1721
+ }
1722
+
1723
+ const rdata = await req.body.json();
1724
+ // rdata.errno: -7 - param path file name is invalid
1725
+ return rdata;
1726
+ }
1727
+ catch (error) {
1728
+ throw new Error('createFolder', { cause: error });
1729
+ }
1730
+ }
1731
+
1732
+ /**
1733
+ * Creates a new file entry on the server after uploading chunks
1734
+ * @param {Object} data - File data including remote_dir, file, size, hash, upload_id, and chunks
1735
+ * @param {string} data.remote_dir - Remote directory path
1736
+ * @param {string} data.file - Filename
1737
+ * @param {number} data.size - File size in bytes
1738
+ * @param {Object} data.hash - Hash information
1739
+ * @param {string} data.hash.file - MD5 hash of full file
1740
+ * @param {string} data.hash.slice - MD5 hash of first slice
1741
+ * @param {number} data.hash.crc32 - CRC32 value
1742
+ * @param {Array<string>} data.hash.chunks - Array of MD5 chunk hashes
1743
+ * @param {string} data.upload_id - Upload ID obtained from precreate
1744
+ * @returns {Promise<Object>} The create file response JSON (includes MD5 and ETag)
1745
+ * @async
1746
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1747
+ */
1748
+ async createFile(data){
1749
+ const formData = new this.FormUrlEncoded();
1750
+ formData.append('path', makeRemoteFPath(data.remote_dir, data.file));
1751
+ // formData.append('isdir', 0);
1752
+ formData.append('size', data.size);
1753
+ formData.append('isdir', 0);
1754
+
1755
+ // check if has correct md5 values
1756
+ if(this.CheckMd5Val(data.hash.slice) && this.CheckMd5Val(data.hash.file)){
1757
+ formData.append('content-md5', data.hash.file);
1758
+ formData.append('slice-md5', data.hash.slice);
1759
+ }
1760
+
1761
+ // check crc32int and ignore field for crc32 out of range
1762
+ if(Number.isSafeInteger(data.hash.crc32) && data.hash.crc32 >= 0 && data.hash.crc32 <= 0xFFFFFFFF){
1763
+ formData.append('content-crc32', data.hash.crc32);
1764
+ }
1765
+
1766
+ formData.append('block_list', JSON.stringify(data.hash.chunks));;
1767
+ formData.append('uploadid', data.upload_id);
1768
+ formData.append('rtype', 2);
1769
+
1770
+ // formData.append('local_ctime', '');
1771
+ // formData.append('local_mtime', '');
1772
+ // formData.append('zip_quality', '');
1773
+ // formData.append('zip_sign', '');
1774
+ // formData.append('is_revision', 0);
1775
+ // formData.append('mode', 2); // 2 is Batch Upload
1776
+ // formData.append('exif_info', exifJsonStr);
1777
+
1778
+ const url = new URL(this.params.whost + `/api/create`);
1779
+
1780
+ try{
1781
+ const req = await request(url, {
1782
+ method: 'POST',
1783
+ body: formData.str(),
1784
+ headers: {
1785
+ 'Content-Type': 'application/x-www-form-urlencoded',
1786
+ 'User-Agent': this.params.ua,
1787
+ 'Cookie': this.params.cookie,
1788
+ },
1789
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1790
+ });
1791
+
1792
+ if (req.statusCode !== 200) {
1793
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1794
+ }
1795
+
1796
+ const rdata = await req.body.json();
1797
+ // rdata.errno: 31355 - pcs service failed
1798
+ if(rdata.md5){
1799
+ // encrypted etag
1800
+ rdata.emd5 = rdata.md5;
1801
+ // decrypted etag (without chunk count)
1802
+ rdata.md5 = this.DecodeMd5(rdata.emd5);
1803
+ // set custom etag
1804
+ rdata.etag = rdata.md5;
1805
+ if(data.hash.chunks.length > 1){
1806
+ rdata.etag += '-' + data.hash.chunks.length;
1807
+ }
1808
+ }
1809
+ return rdata;
1810
+ }
1811
+ catch (error) {
1812
+ console.log(error);
1813
+ throw new Error('createFile', { cause: error });
1814
+ }
1815
+ }
1816
+
1817
+ /**
1818
+ * Performs file management operations (delete, copy, move, rename)
1819
+ * @param {string} operation - Operation type: 'delete', 'copy', 'move', 'rename'
1820
+ * @param {Array} fmparams - Parameters for the operation (array of paths or objects)
1821
+ * @returns {Promise<Object>} The file manager response JSON
1822
+ * @async
1823
+ * @throws {Error} Throws error if fmparams is not an array, HTTP status is not 200, or request fails
1824
+ */
1825
+ async filemanager(operation, fmparams){
1826
+ // For Delete: ["/path1","path2.rar"]
1827
+ // For Move: [{"path":"/myfolder/source.bin","dest":"/target/","newname":"newfilename.bin"}]
1828
+ // For Copy same as move
1829
+ // + "ondup": newcopy, overwrite (optional, skip by default)
1830
+ // For rename [{"id":1111,"path":"/dir1/src.bin","newname":"myfile2.bin"}]
1831
+
1832
+ // operation - copy (file copy), move (file movement), rename (file renaming), and delete (file deletion)
1833
+ // opera=copy: filelist: [{"path":"/hello/test.mp4","dest":"","newname":"test.mp4"}]
1834
+ // opera=move: filelist: [{"path":"/test.mp4","dest":"/test_dir","newname":"test.mp4"}]
1835
+ // opera=rename: filelist:[{"path":"/hello/test.mp4","newname":"test_one.mp4"}]
1836
+ // opera=delete: filelist: ["/test.mp4"]
1837
+
1838
+ if(!Array.isArray(fmparams)){
1839
+ throw new Error('filemanager', { cause: new Error('FS paths should be in array!') });
1840
+ }
1841
+
1842
+ const url = new URL(this.params.whost + '/api/filemanager');
1843
+
1844
+ const formData = new this.FormUrlEncoded();
1845
+ formData.append('filelist', JSON.stringify(fmparams));
1846
+
1847
+ try{
1848
+ if(this.data.jsToken === ''){
1849
+ await this.updateAppData();
1850
+ }
1851
+
1852
+ url.search = new URLSearchParams({
1853
+ ...this.params.app,
1854
+ jsToken: this.data.jsToken,
1855
+ // 'async': 1,
1856
+ onnest: 'fail',
1857
+ opera: operation, // delete, copy, move, rename
1858
+ });
1859
+
1860
+ const req = await request(url, {
1861
+ method: 'POST',
1862
+ body: formData.str(),
1863
+ headers: {
1864
+ 'Content-Type': 'application/x-www-form-urlencoded',
1865
+ 'User-Agent': this.params.ua,
1866
+ 'Cookie': this.params.cookie,
1867
+ },
1868
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1869
+ });
1870
+
1871
+ if (req.statusCode !== 200) {
1872
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
1873
+ }
1874
+
1875
+ const rdata = await req.body.json();
1876
+ if(rdata.errno === 450016){
1877
+ await this.updateAppData();
1878
+ return await this.filemanager(operation, fmparams);
1879
+ }
1880
+ return rdata;
1881
+ }
1882
+ catch (error) {
1883
+ throw new Error('filemanager', { cause: error });
1884
+ }
1885
+ }
1886
+
1887
+ /**
1888
+ * Retrieves a list of shares created by the user
1889
+ * @returns {Promise<Object>} The share list JSON (includes share entries)
1890
+ * @async
1891
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1892
+ */
1893
+ async shareList(page = 1){
1894
+ const url = new URL(this.params.whost + '/share/teratransfer/sharelist');
1895
+
1896
+ try{
1897
+ url.search = new URLSearchParams({
1898
+ // ...this.params.app,
1899
+ page_size: 100,
1900
+ page: page,
1901
+ });
1902
+
1903
+ const req = await request(url, {
1904
+ headers: {
1905
+ 'User-Agent': this.params.ua,
1906
+ 'Cookie': this.params.cookie,
1907
+ },
1908
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1909
+ });
1910
+
1911
+ const rdata = await req.body.json();
1912
+ return rdata;
1913
+ }
1914
+ catch (error) {
1915
+ throw new Error('shareList', { cause: error });
1916
+ }
1917
+ }
1918
+
1919
+ /**
1920
+ * Sets sharing parameters (e.g., password, expiration) for specified files
1921
+ * @param {Array<string>} filelist - Array of file paths to share
1922
+ * @param {string} [pass=''] - Optional 4-character alphanumeric password
1923
+ * @param {number} [period=0] - Sharing period in days (0 for no expiration)
1924
+ * @returns {Promise<Object>} The share set response JSON (includes share IDs)
1925
+ * @async
1926
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1927
+ */
1928
+ async shareSet(filelist, pass = '', period = 0){
1929
+ const url = new URL(this.params.whost + '/share/pset');
1930
+
1931
+ try{
1932
+ url.search = new URLSearchParams({
1933
+ // ...this.params.app,
1934
+ });
1935
+
1936
+ filelist = Array.isArray(filelist) ? filelist : [];
1937
+ filelist = JSON.stringify(filelist);
1938
+
1939
+ pass = typeof pass === 'string' && pass.match(/^[0-9a-z]{4}$/i) ? pass : '';
1940
+ const schannel = pass !== '' ? 4 : 0;
1941
+
1942
+ // 0 - infinity, otherwise valid X days
1943
+ period = parseInt(period);
1944
+ period = !isNaN(period) && Number.isSafeInteger(period) ? period : 0;
1945
+
1946
+ const formData = new this.FormUrlEncoded();
1947
+ formData.append('schannel', schannel);
1948
+ formData.append('channel_list', '[]');
1949
+ formData.append('period', period);
1950
+ formData.append('path_list', filelist);
1951
+ formData.append('pwd', pass);
1952
+
1953
+ const req = await request(url, {
1954
+ headers: {
1955
+ 'Content-Type': 'application/x-www-form-urlencoded',
1956
+ 'User-Agent': this.params.ua,
1957
+ 'Cookie': this.params.cookie,
1958
+ Referer: this.params.whost,
1959
+ },
1960
+ body: formData.str(),
1961
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
1962
+ });
1963
+
1964
+ const rdata = await req.body.json();
1965
+ return rdata;
1966
+ }
1967
+ catch (error) {
1968
+ throw new Error('shareSet', { cause: error });
1969
+ }
1970
+ }
1971
+
1972
+ /**
1973
+ * Cancels existing shares by share ID
1974
+ * @param {Array<number>} [shareid_list=[]] - Array of share IDs to cancel
1975
+ * @returns {Promise<Object>} The share cancel response JSON
1976
+ * @async
1977
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
1978
+ */
1979
+ async shareCancel(shareid_list = []){
1980
+ const url = new URL(this.params.whost + '/share/cancel');
1981
+
1982
+ try{
1983
+ url.search = new URLSearchParams({
1984
+ // ...this.params.app,
1985
+ });
1986
+
1987
+ shareid_list = Array.isArray(shareid_list) ? shareid_list : [];
1988
+ shareid_list = JSON.stringify(shareid_list);
1989
+
1990
+ const formData = new this.FormUrlEncoded();
1991
+ formData.append('shareid_list', shareid_list);
1992
+
1993
+ const req = await request(url, {
1994
+ headers: {
1995
+ 'Content-Type': 'application/x-www-form-urlencoded',
1996
+ 'User-Agent': this.params.ua,
1997
+ 'Cookie': this.params.cookie,
1998
+ Referer: this.params.whost,
1999
+ },
2000
+ body: formData.str(),
2001
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
2002
+ });
2003
+
2004
+ const rdata = await req.body.json();
2005
+ return rdata;
2006
+ }
2007
+ catch (error) {
2008
+ throw new Error('shareCancel', { cause: error });
2009
+ }
2010
+ }
2011
+
2012
+ /**
2013
+ * Retrieves information for a shortened URL share
2014
+ * @param {string} shortUrl - The short url: after "surl="
2015
+ * @returns {Promise<Object>} The short URL info JSON (includes file list, permissions)
2016
+ * @async
2017
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
2018
+ */
2019
+ async shortUrlInfo(shortUrl){
2020
+ const url = new URL(this.params.whost + '/api/shorturlinfo');
2021
+
2022
+ try{
2023
+ url.search = new URLSearchParams({
2024
+ //...this.params.app,
2025
+ shorturl: '1' + shortUrl,
2026
+ root: 1,
2027
+ });
2028
+
2029
+ const connector = buildConnector({ ciphers: tls.DEFAULT_CIPHERS + ':!ECDHE-RSA-AES128-SHA' });
2030
+ const client = new Client(this.params.whost, { connect: connector });
2031
+ const req = await request(url, {
2032
+ method: 'GET',
2033
+ headers: {
2034
+ 'User-Agent': this.params.ua,
2035
+ 'Cookie': this.params.cookie,
2036
+ },
2037
+ dispatcher: client,
2038
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
2039
+ });
2040
+
2041
+ if (req.statusCode !== 200) {
2042
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
2043
+ }
2044
+
2045
+ const rdata = await req.body.json();
2046
+ return rdata;
2047
+ }
2048
+ catch (error) {
2049
+ throw new Error('shortUrlInfo', { cause: error });
2050
+ }
2051
+ }
2052
+
2053
+ /**
2054
+ * Lists files under a shortened URL share
2055
+ * @param {string} shortUrl - The short url: after "surl="
2056
+ * @param {string} [remoteDir=''] - Remote directory under share (empty for root)
2057
+ * @param {number} [page=1] - Page number for pagination
2058
+ * @returns {Promise<Object>} The short URL file list JSON (includes entries array)
2059
+ * @async
2060
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
2061
+ */
2062
+ async shortUrlList(shortUrl, remoteDir = '', page = 1){
2063
+ const url = new URL(this.params.whost + '/share/list');
2064
+ remoteDir = remoteDir || ''
2065
+
2066
+ try{
2067
+ if(this.data.jsToken === ''){
2068
+ await this.updateAppData();
2069
+ }
2070
+
2071
+ url.search = new URLSearchParams({
2072
+ ...this.params.app,
2073
+ jsToken: this.data.jsToken,
2074
+ shorturl: shortUrl,
2075
+ by: 'name',
2076
+ order: 'asc',
2077
+ num: 20000,
2078
+ dir: remoteDir,
2079
+ page: page,
2080
+ });
2081
+
2082
+ if(remoteDir === ''){
2083
+ url.searchParams.append('root', '1');
2084
+ }
2085
+
2086
+ const connector = buildConnector({ ciphers: tls.DEFAULT_CIPHERS + ':!ECDHE-RSA-AES128-SHA' });
2087
+ const client = new Client(this.params.whost, { connect: connector });
2088
+ const req = await request(url, {
2089
+ method: 'GET',
2090
+ headers: {
2091
+ 'User-Agent': this.params.ua,
2092
+ 'Cookie': this.params.cookie,
2093
+ },
2094
+ dispatcher: client,
2095
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
2096
+ });
2097
+
2098
+ if (req.statusCode !== 200) {
2099
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
2100
+ }
2101
+
2102
+ const rdata = await req.body.json();
2103
+ // rdata.errno: 4000020 - need verify
2104
+ if(rdata.errno === 4000020){
2105
+ await this.updateAppData();
2106
+ return await this.shortUrlList(shortUrl, remoteDir, page);
2107
+ }
2108
+ return rdata;
2109
+ }
2110
+ catch (error) {
2111
+ throw new Error('shortUrlList', { cause: error });
2112
+ }
2113
+ }
2114
+
2115
+ /**
2116
+ * Retrieves file difference (delta) information for synchronization
2117
+ * @returns {Promise<Object>} The file diff JSON (includes entries, request_id, has_more flag)
2118
+ * @async
2119
+ * @throws {Error} Throws error if HTTP status is not 200, request fails, or on recursive errors
2120
+ */
2121
+ async fileDiff(){
2122
+ const formData = new this.FormUrlEncoded();
2123
+ formData.append('cursor', this.params.cursor);
2124
+ if(this.params.cursor === 'null'){
2125
+ formData.append('c', 'full');
2126
+ }
2127
+ formData.append('action', 'manual');
2128
+
2129
+ const url = new URL(this.params.whost + '/api/filediff');
2130
+ url.search = new URLSearchParams({
2131
+ ...this.params.app,
2132
+ block_list: 1,
2133
+ // rand: '',
2134
+ // time: '',
2135
+ // vip: this.params.vip_type,
2136
+ // wp_retry_num: 2,
2137
+ // lang: this.params.lang,
2138
+ // logid: '',
2139
+ });
2140
+
2141
+ try{
2142
+ const req = await request(url, {
2143
+ method: 'POST',
2144
+ body: formData.str(),
2145
+ headers: {
2146
+ 'Content-Type': 'application/x-www-form-urlencoded',
2147
+ 'User-Agent': this.params.ua,
2148
+ 'Cookie': this.params.cookie,
2149
+ },
2150
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
2151
+ });
2152
+
2153
+ if (req.statusCode !== 200) {
2154
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
2155
+ }
2156
+
2157
+ const rdata = await req.body.json();
2158
+ if(rdata.errno === 0){
2159
+ this.params.cursor = rdata.cursor;
2160
+ if(!Array.isArray(rdata.request_id)){
2161
+ rdata.request_id = [ rdata.request_id ];
2162
+ }
2163
+ if(rdata.has_more){
2164
+ // Extra FileDiff request...
2165
+ const rFileDiff = await this.fileDiff();
2166
+ if(rFileDiff.errno === 0){
2167
+ rdata.reset = rFileDiff.reset;
2168
+ rdata.request_id = rdata.request_id.concat(rFileDiff.request_id);
2169
+ rdata.entries = Object.assign({}, rdata.entries, rFileDiff.entries);
2170
+ rdata.has_more = rFileDiff.has_more;
2171
+ }
2172
+ }
2173
+ }
2174
+ return rdata;
2175
+ }
2176
+ catch (error) {
2177
+ this.params.cursor = 'null';
2178
+ throw new Error('fileDiff', { cause: error });
2179
+ }
2180
+ }
2181
+
2182
+ /**
2183
+ * Generates a PAN token for subsequent API requests
2184
+ * @returns {Promise<Object>} The PAN token response JSON (includes pan token and expire)
2185
+ * @async
2186
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
2187
+ */
2188
+ async genPanToken(){
2189
+ const url = new URL(this.params.whost + '/api/pantoken');
2190
+
2191
+ try{
2192
+ url.search = new URLSearchParams({
2193
+ ...this.params.app,
2194
+ lang: this.params.lang,
2195
+ u: 'https://www.terabox.com',
2196
+ });
2197
+
2198
+ const req = await request(url, {
2199
+ headers: {
2200
+ 'User-Agent': this.params.ua,
2201
+ 'Cookie': this.params.cookie,
2202
+ },
2203
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
2204
+ });
2205
+
2206
+ if (req.statusCode !== 200) {
2207
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
2208
+ }
2209
+
2210
+ const rdata = await req.body.json();
2211
+ return rdata;
2212
+ }
2213
+ catch (error) {
2214
+ throw new Error('genPanToken', { cause: error });
2215
+ }
2216
+ }
2217
+
2218
+ /**
2219
+ * Retrieves home page information (user info, sign data)
2220
+ * @returns {Promise<Object>} The home info JSON (includes sign1, sign3, data.signb)
2221
+ * @async
2222
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
2223
+ */
2224
+ async getHomeInfo(){
2225
+ const url = new URL(this.params.whost + '/api/home/info');
2226
+
2227
+ try{
2228
+ const req = await request(url, {
2229
+ headers: {
2230
+ 'User-Agent': this.params.ua,
2231
+ 'Cookie': this.params.cookie,
2232
+ },
2233
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
2234
+ });
2235
+
2236
+ if (req.statusCode !== 200) {
2237
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
2238
+ }
2239
+
2240
+ const rdata = await req.body.json();
2241
+ if(rdata.errno === 0){
2242
+ rdata.data.signb = this.SignDownload(rdata.data.sign3, rdata.data.sign1);
2243
+ }
2244
+ return rdata;
2245
+ }
2246
+ catch (error) {
2247
+ throw new Error('getHomeInfo', { cause: error });
2248
+ }
2249
+ }
2250
+
2251
+ /**
2252
+ * Initiates a download request for specified file IDs
2253
+ * @param {Array<number>} fs_ids - Array of file system IDs to download
2254
+ * @param {string} signb - Base64-encoded signature from getHomeInfo
2255
+ * @returns {Promise<Object>} The download response JSON (includes dlink URLs)
2256
+ * @async
2257
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
2258
+ */
2259
+ async download(fs_ids){
2260
+ const url = new URL(this.params.whost + '/api/download');
2261
+
2262
+ try{
2263
+ const homeInfo = await this.getHomeInfo();
2264
+ if(homeInfo.errno !== 0){
2265
+ throw new Error(`API error! Bad HomeInfo response`);
2266
+ }
2267
+
2268
+ const formData = new this.FormUrlEncoded({
2269
+ fidlist: JSON.stringify(fs_ids),
2270
+ type: 'dlink',
2271
+ vip: 2, // this.params.vip_type
2272
+ sign: homeInfo.data.signb,
2273
+ timestamp: homeInfo.data.timestamp,
2274
+ need_speed: 1, // Premium speed?..
2275
+ });
2276
+
2277
+ const req = await request(url, {
2278
+ method: 'POST',
2279
+ body: formData.str(),
2280
+ headers: {
2281
+ 'Content-Type': 'application/x-www-form-urlencoded',
2282
+ 'User-Agent': this.params.ua,
2283
+ 'Cookie': this.params.cookie,
2284
+ },
2285
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
2286
+ });
2287
+
2288
+ if (req.statusCode !== 200) {
2289
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
2290
+ }
2291
+
2292
+ const rdata = await req.body.json();
2293
+ return rdata;
2294
+ }
2295
+ catch (error) {
2296
+ throw new Error('download', { cause: error });
2297
+ }
2298
+ }
2299
+
2300
+ /**
2301
+ * Retrieves the streaming contents of a remote file
2302
+ * @param {string} remotePath - Remote video file
2303
+ * @param {string} type - Streaming type:
2304
+ * <br>M3U8_FLV_264_480
2305
+ * <br>M3U8_AUTO_240
2306
+ * <br>M3U8_AUTO_360
2307
+ * <br>M3U8_AUTO_480
2308
+ * <br>M3U8_AUTO_720
2309
+ * <br>M3U8_AUTO_1080
2310
+ * <br>M3U8_SUBTITLE_SRT
2311
+ * @returns {Promise<Object>} m3u8 playlist, or JSON with error
2312
+ * @async
2313
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
2314
+ */
2315
+ async getStream(remotePath = '/video.mp4', type = 'M3U8_AUTO_480'){
2316
+ const url = new URL(this.params.whost + '/api/streaming');
2317
+
2318
+ try{
2319
+ const formData = new this.FormUrlEncoded();
2320
+ formData.append('path', remotePath);
2321
+ formData.append('type', type);
2322
+ formData.append('vip', 2);
2323
+
2324
+ const req = await request(url, {
2325
+ method: 'POST',
2326
+ body: formData.str(),
2327
+ headers: {
2328
+ 'Content-Type': 'application/x-www-form-urlencoded',
2329
+ 'User-Agent': this.params.ua,
2330
+ 'Cookie': this.params.cookie,
2331
+ },
2332
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
2333
+ });
2334
+
2335
+ if (req.statusCode !== 200) {
2336
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
2337
+ }
2338
+
2339
+ const rdata = await req.body.json();
2340
+ return rdata;
2341
+ }
2342
+ catch (error) {
2343
+ throw new Error('getStream', { cause: error });
2344
+ }
2345
+ }
2346
+
2347
+ /**
2348
+ * Retrieves metadata for specified remote files
2349
+ * @param {Array<Object>} remote_file_list - Array of file descriptor objects { fs_id, path, etc. }
2350
+ * @returns {Promise<Object>} The file metadata JSON (includes size, md5, etc.)
2351
+ * @async
2352
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
2353
+ */
2354
+ async getFileMeta(remote_file_list){
2355
+ const url = new URL(this.params.whost + '/api/filemetas');
2356
+
2357
+ try{
2358
+ const formData = new this.FormUrlEncoded({
2359
+ dlink: 1,
2360
+ origin: 'dlna',
2361
+ target: JSON.stringify(remote_file_list),
2362
+ });
2363
+
2364
+ const req = await request(url, {
2365
+ method: 'POST',
2366
+ body: formData.str(),
2367
+ headers: {
2368
+ 'Content-Type': 'application/x-www-form-urlencoded',
2369
+ 'User-Agent': this.params.ua,
2370
+ 'Cookie': this.params.cookie,
2371
+ },
2372
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
2373
+ });
2374
+
2375
+ if (req.statusCode !== 200) {
2376
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
2377
+ }
2378
+
2379
+ const rdata = await req.body.json();
2380
+ return rdata;
2381
+ }
2382
+ catch (error) {
2383
+ throw new Error('getFileMeta', { cause: error });
2384
+ }
2385
+ }
2386
+
2387
+ /**
2388
+ * Retrieves a list of recent uploads for the account
2389
+ * @param {number} [page=1] - Page number for pagination
2390
+ * @returns {Promise<Object>} The recent uploads JSON (includes records array)
2391
+ * @async
2392
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
2393
+ */
2394
+ async getRecentUploads(page = 1){
2395
+ const url = new URL(this.params.whost + '/rest/recent/listall');
2396
+
2397
+ try{
2398
+ url.search = new URLSearchParams({
2399
+ ...this.params.app,
2400
+ version: this.params.ver_android,
2401
+ // num: 20000, ???
2402
+ // page: page, ???
2403
+ });
2404
+
2405
+ const req = await request(url, {
2406
+ method: 'GET',
2407
+ body: formData.str(),
2408
+ headers: {
2409
+ 'User-Agent': this.params.ua,
2410
+ 'Cookie': this.params.cookie,
2411
+ },
2412
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
2413
+ });
2414
+
2415
+ if (req.statusCode !== 200) {
2416
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
2417
+ }
2418
+
2419
+ const rdata = await req.body.json();
2420
+ return rdata;
2421
+ }
2422
+ catch (error) {
2423
+ throw new Error('getRecentUploads', { cause: error });
2424
+ }
2425
+ }
2426
+
2427
+ /**
2428
+ * Retrieves the RSA public key from the server for encryption
2429
+ * @returns {Promise<Object>} The public key response JSON (includes pp1 and pp2)
2430
+ * @async
2431
+ * @throws {Error} Throws error if HTTP status is not 200 or request fails
2432
+ */
2433
+ async getPublicKey(){
2434
+ const url = new URL(this.params.whost + '/passport/getpubkey');
2435
+
2436
+ try{
2437
+ const req = await request(url, {
2438
+ method: 'GET',
2439
+ headers: {
2440
+ 'User-Agent': this.params.ua,
2441
+ },
2442
+ signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
2443
+ });
2444
+
2445
+ if (req.statusCode !== 200) {
2446
+ throw new Error(`HTTP error! Status: ${req.statusCode}`);
2447
+ }
2448
+
2449
+ const rdata = await req.body.json();
2450
+
2451
+ if(rdata.code === 0){
2452
+ this.data.pubkey = this.DecryptAES(rdata.data.pp1, rdata.data.pp2);
2453
+ }
2454
+
2455
+ return rdata;
2456
+ }
2457
+ catch (error) {
2458
+ throw new Error('getPublicKey', { cause: error });
2459
+ }
2460
+ }
2461
+ }
2462
+
2463
+ // exports
2464
+ export default TeraBoxApp;
2465
+ export { TeraBoxApp };