httpcloak 1.0.0 → 1.0.2

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/lib/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * HTTPCloak Node.js Client
3
3
  *
4
- * Provides HTTP client with browser fingerprint emulation.
4
+ * A fetch/axios-compatible HTTP client with browser fingerprint emulation.
5
+ * Provides TLS fingerprinting for HTTP requests.
5
6
  */
6
7
 
7
8
  const koffi = require("koffi");
@@ -19,6 +20,48 @@ class HTTPCloakError extends Error {
19
20
  }
20
21
  }
21
22
 
23
+ /**
24
+ * Available browser presets for TLS fingerprinting.
25
+ *
26
+ * Use these constants instead of typing preset strings manually:
27
+ * const httpcloak = require("httpcloak");
28
+ * httpcloak.configure({ preset: httpcloak.Preset.CHROME_143 });
29
+ *
30
+ * // Or with Session
31
+ * const session = new httpcloak.Session({ preset: httpcloak.Preset.FIREFOX_133 });
32
+ */
33
+ const Preset = {
34
+ // Chrome 143 (latest)
35
+ CHROME_143: "chrome-143",
36
+ CHROME_143_WINDOWS: "chrome-143-windows",
37
+ CHROME_143_LINUX: "chrome-143-linux",
38
+ CHROME_143_MACOS: "chrome-143-macos",
39
+
40
+ // Chrome 131
41
+ CHROME_131: "chrome-131",
42
+ CHROME_131_WINDOWS: "chrome-131-windows",
43
+ CHROME_131_LINUX: "chrome-131-linux",
44
+ CHROME_131_MACOS: "chrome-131-macos",
45
+
46
+ // Firefox
47
+ FIREFOX_133: "firefox-133",
48
+
49
+ // Safari
50
+ SAFARI_18: "safari-18",
51
+
52
+ /**
53
+ * Get all available preset names
54
+ * @returns {string[]} List of all preset names
55
+ */
56
+ all() {
57
+ return [
58
+ this.CHROME_143, this.CHROME_143_WINDOWS, this.CHROME_143_LINUX, this.CHROME_143_MACOS,
59
+ this.CHROME_131, this.CHROME_131_WINDOWS, this.CHROME_131_LINUX, this.CHROME_131_MACOS,
60
+ this.FIREFOX_133, this.SAFARI_18,
61
+ ];
62
+ },
63
+ };
64
+
22
65
  /**
23
66
  * Response object returned from HTTP requests
24
67
  */
@@ -26,17 +69,51 @@ class Response {
26
69
  constructor(data) {
27
70
  this.statusCode = data.status_code || 0;
28
71
  this.headers = data.headers || {};
29
- this.body = Buffer.from(data.body || "", "utf8");
30
- this.text = data.body || "";
72
+ this._body = Buffer.from(data.body || "", "utf8");
73
+ this._text = data.body || "";
31
74
  this.finalUrl = data.final_url || "";
32
75
  this.protocol = data.protocol || "";
33
76
  }
34
77
 
78
+ /** Response body as string */
79
+ get text() {
80
+ return this._text;
81
+ }
82
+
83
+ /** Response body as Buffer (requests compatibility) */
84
+ get body() {
85
+ return this._body;
86
+ }
87
+
88
+ /** Response body as Buffer (requests compatibility alias) */
89
+ get content() {
90
+ return this._body;
91
+ }
92
+
93
+ /** Final URL after redirects (requests compatibility alias) */
94
+ get url() {
95
+ return this.finalUrl;
96
+ }
97
+
98
+ /** True if status code < 400 (requests compatibility) */
99
+ get ok() {
100
+ return this.statusCode < 400;
101
+ }
102
+
35
103
  /**
36
104
  * Parse response body as JSON
37
105
  */
38
106
  json() {
39
- return JSON.parse(this.text);
107
+ return JSON.parse(this._text);
108
+ }
109
+
110
+ /**
111
+ * Raise error if status >= 400 (requests compatibility)
112
+ */
113
+ raiseForStatus() {
114
+ if (!this.ok) {
115
+ throw new HTTPCloakError(`HTTP ${this.statusCode}`);
116
+ }
40
117
  }
41
118
  }
42
119
 
@@ -47,7 +124,6 @@ function getPlatformPackageName() {
47
124
  const platform = os.platform();
48
125
  const arch = os.arch();
49
126
 
50
- // Map to npm platform names
51
127
  let platName;
52
128
  if (platform === "darwin") {
53
129
  platName = "darwin";
@@ -76,13 +152,11 @@ function getLibPath() {
76
152
  const platform = os.platform();
77
153
  const arch = os.arch();
78
154
 
79
- // Check environment variable first
80
155
  const envPath = process.env.HTTPCLOAK_LIB_PATH;
81
156
  if (envPath && fs.existsSync(envPath)) {
82
157
  return envPath;
83
158
  }
84
159
 
85
- // Try to load from platform-specific optional dependency
86
160
  const packageName = getPlatformPackageName();
87
161
  try {
88
162
  const libPath = require(packageName);
@@ -90,10 +164,9 @@ function getLibPath() {
90
164
  return libPath;
91
165
  }
92
166
  } catch (e) {
93
- // Optional dependency not installed, fall back to local search
167
+ // Optional dependency not installed
94
168
  }
95
169
 
96
- // Normalize architecture for library name
97
170
  let archName;
98
171
  if (arch === "x64" || arch === "amd64") {
99
172
  archName = "amd64";
@@ -103,7 +176,6 @@ function getLibPath() {
103
176
  archName = arch;
104
177
  }
105
178
 
106
- // Determine OS name and extension
107
179
  let osName, ext;
108
180
  if (platform === "darwin") {
109
181
  osName = "darwin";
@@ -118,7 +190,6 @@ function getLibPath() {
118
190
 
119
191
  const libName = `libhttpcloak-${osName}-${archName}${ext}`;
120
192
 
121
- // Search paths (fallback for local development)
122
193
  const searchPaths = [
123
194
  path.join(__dirname, libName),
124
195
  path.join(__dirname, "..", libName),
@@ -145,26 +216,45 @@ function getLib() {
145
216
  const libPath = getLibPath();
146
217
  const nativeLib = koffi.load(libPath);
147
218
 
219
+ // Use void* for string returns so we can free them properly
148
220
  lib = {
149
221
  httpcloak_session_new: nativeLib.func("httpcloak_session_new", "int64", ["str"]),
150
222
  httpcloak_session_free: nativeLib.func("httpcloak_session_free", "void", ["int64"]),
151
- httpcloak_get: nativeLib.func("httpcloak_get", "str", ["int64", "str", "str"]),
152
- httpcloak_post: nativeLib.func("httpcloak_post", "str", ["int64", "str", "str", "str"]),
153
- httpcloak_request: nativeLib.func("httpcloak_request", "str", ["int64", "str"]),
154
- httpcloak_get_cookies: nativeLib.func("httpcloak_get_cookies", "str", ["int64"]),
223
+ httpcloak_get: nativeLib.func("httpcloak_get", "void*", ["int64", "str", "str"]),
224
+ httpcloak_post: nativeLib.func("httpcloak_post", "void*", ["int64", "str", "str", "str"]),
225
+ httpcloak_request: nativeLib.func("httpcloak_request", "void*", ["int64", "str"]),
226
+ httpcloak_get_cookies: nativeLib.func("httpcloak_get_cookies", "void*", ["int64"]),
155
227
  httpcloak_set_cookie: nativeLib.func("httpcloak_set_cookie", "void", ["int64", "str", "str"]),
156
- httpcloak_free_string: nativeLib.func("httpcloak_free_string", "void", ["str"]),
157
- httpcloak_version: nativeLib.func("httpcloak_version", "str", []),
158
- httpcloak_available_presets: nativeLib.func("httpcloak_available_presets", "str", []),
228
+ httpcloak_free_string: nativeLib.func("httpcloak_free_string", "void", ["void*"]),
229
+ httpcloak_version: nativeLib.func("httpcloak_version", "void*", []),
230
+ httpcloak_available_presets: nativeLib.func("httpcloak_available_presets", "void*", []),
159
231
  };
160
232
  }
161
233
  return lib;
162
234
  }
163
235
 
236
+ /**
237
+ * Convert a C string pointer to JS string and free the memory
238
+ */
239
+ function ptrToString(ptr) {
240
+ if (!ptr) {
241
+ return null;
242
+ }
243
+ try {
244
+ // Decode the C string from the pointer
245
+ const str = koffi.decode(ptr, "str");
246
+ return str;
247
+ } finally {
248
+ // Always free the C string to prevent memory leaks
249
+ getLib().httpcloak_free_string(ptr);
250
+ }
251
+ }
252
+
164
253
  /**
165
254
  * Parse response from the native library
166
255
  */
167
- function parseResponse(result) {
256
+ function parseResponse(resultPtr) {
257
+ const result = ptrToString(resultPtr);
168
258
  if (!result) {
169
259
  throw new HTTPCloakError("No response received");
170
260
  }
@@ -178,12 +268,147 @@ function parseResponse(result) {
178
268
  return new Response(data);
179
269
  }
180
270
 
271
+ /**
272
+ * Add query parameters to URL
273
+ */
274
+ function addParamsToUrl(url, params) {
275
+ if (!params || Object.keys(params).length === 0) {
276
+ return url;
277
+ }
278
+
279
+ const urlObj = new URL(url);
280
+ for (const [key, value] of Object.entries(params)) {
281
+ urlObj.searchParams.append(key, String(value));
282
+ }
283
+ return urlObj.toString();
284
+ }
285
+
286
+ /**
287
+ * Apply basic auth to headers
288
+ */
289
+ function applyAuth(headers, auth) {
290
+ if (!auth) {
291
+ return headers;
292
+ }
293
+
294
+ const [username, password] = auth;
295
+ const credentials = Buffer.from(`${username}:${password}`).toString("base64");
296
+
297
+ headers = headers ? { ...headers } : {};
298
+ headers["Authorization"] = `Basic ${credentials}`;
299
+ return headers;
300
+ }
301
+
302
+ /**
303
+ * Detect MIME type from filename
304
+ */
305
+ function detectMimeType(filename) {
306
+ const ext = path.extname(filename).toLowerCase();
307
+ const mimeTypes = {
308
+ ".html": "text/html",
309
+ ".htm": "text/html",
310
+ ".css": "text/css",
311
+ ".js": "application/javascript",
312
+ ".json": "application/json",
313
+ ".xml": "application/xml",
314
+ ".txt": "text/plain",
315
+ ".csv": "text/csv",
316
+ ".jpg": "image/jpeg",
317
+ ".jpeg": "image/jpeg",
318
+ ".png": "image/png",
319
+ ".gif": "image/gif",
320
+ ".webp": "image/webp",
321
+ ".svg": "image/svg+xml",
322
+ ".ico": "image/x-icon",
323
+ ".bmp": "image/bmp",
324
+ ".mp3": "audio/mpeg",
325
+ ".wav": "audio/wav",
326
+ ".ogg": "audio/ogg",
327
+ ".mp4": "video/mp4",
328
+ ".webm": "video/webm",
329
+ ".pdf": "application/pdf",
330
+ ".zip": "application/zip",
331
+ ".gz": "application/gzip",
332
+ ".tar": "application/x-tar",
333
+ };
334
+ return mimeTypes[ext] || "application/octet-stream";
335
+ }
336
+
337
+ /**
338
+ * Encode multipart form data
339
+ * @param {Object} data - Form fields (key-value pairs)
340
+ * @param {Object} files - Files to upload
341
+ * Each key is the field name, value can be:
342
+ * - Buffer: raw file content
343
+ * - { filename, content, contentType? }: file with metadata
344
+ * @returns {{ body: Buffer, contentType: string }}
345
+ */
346
+ function encodeMultipart(data, files) {
347
+ const boundary = `----HTTPCloakBoundary${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`;
348
+ const parts = [];
349
+
350
+ // Add form fields
351
+ if (data) {
352
+ for (const [key, value] of Object.entries(data)) {
353
+ parts.push(
354
+ `--${boundary}\r\n` +
355
+ `Content-Disposition: form-data; name="${key}"\r\n\r\n` +
356
+ `${value}\r\n`
357
+ );
358
+ }
359
+ }
360
+
361
+ // Add files
362
+ if (files) {
363
+ for (const [fieldName, fileValue] of Object.entries(files)) {
364
+ let filename, content, contentType;
365
+
366
+ if (Buffer.isBuffer(fileValue)) {
367
+ filename = fieldName;
368
+ content = fileValue;
369
+ contentType = "application/octet-stream";
370
+ } else if (typeof fileValue === "object" && fileValue !== null) {
371
+ filename = fileValue.filename || fieldName;
372
+ content = fileValue.content;
373
+ contentType = fileValue.contentType || detectMimeType(filename);
374
+
375
+ if (!Buffer.isBuffer(content)) {
376
+ content = Buffer.from(content);
377
+ }
378
+ } else {
379
+ throw new HTTPCloakError(`Invalid file value for field '${fieldName}'`);
380
+ }
381
+
382
+ parts.push(Buffer.from(
383
+ `--${boundary}\r\n` +
384
+ `Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"\r\n` +
385
+ `Content-Type: ${contentType}\r\n\r\n`
386
+ ));
387
+ parts.push(content);
388
+ parts.push(Buffer.from("\r\n"));
389
+ }
390
+ }
391
+
392
+ parts.push(Buffer.from(`--${boundary}--\r\n`));
393
+
394
+ // Combine all parts
395
+ const bodyParts = parts.map(p => Buffer.isBuffer(p) ? p : Buffer.from(p));
396
+ const body = Buffer.concat(bodyParts);
397
+
398
+ return {
399
+ body,
400
+ contentType: `multipart/form-data; boundary=${boundary}`,
401
+ };
402
+ }
403
+
181
404
  /**
182
405
  * Get the httpcloak library version
183
406
  */
184
407
  function version() {
185
408
  const nativeLib = getLib();
186
- return nativeLib.httpcloak_version() || "unknown";
409
+ const resultPtr = nativeLib.httpcloak_version();
410
+ const result = ptrToString(resultPtr);
411
+ return result || "unknown";
187
412
  }
188
413
 
189
414
  /**
@@ -191,7 +416,8 @@ function version() {
191
416
  */
192
417
  function availablePresets() {
193
418
  const nativeLib = getLib();
194
- const result = nativeLib.httpcloak_available_presets();
419
+ const resultPtr = nativeLib.httpcloak_available_presets();
420
+ const result = ptrToString(resultPtr);
195
421
  if (result) {
196
422
  return JSON.parse(result);
197
423
  }
@@ -208,19 +434,51 @@ class Session {
208
434
  * @param {string} [options.preset="chrome-143"] - Browser preset to use
209
435
  * @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port")
210
436
  * @param {number} [options.timeout=30] - Request timeout in seconds
437
+ * @param {string} [options.httpVersion="auto"] - HTTP version: "auto", "h1", "h2", "h3"
438
+ * @param {boolean} [options.verify=true] - SSL certificate verification
439
+ * @param {boolean} [options.allowRedirects=true] - Follow redirects
440
+ * @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
441
+ * @param {number} [options.retry=0] - Number of retries on failure
442
+ * @param {number[]} [options.retryOnStatus] - Status codes to retry on
211
443
  */
212
444
  constructor(options = {}) {
213
- const { preset = "chrome-143", proxy = null, timeout = 30 } = options;
445
+ const {
446
+ preset = "chrome-143",
447
+ proxy = null,
448
+ timeout = 30,
449
+ httpVersion = "auto",
450
+ verify = true,
451
+ allowRedirects = true,
452
+ maxRedirects = 10,
453
+ retry = 0,
454
+ retryOnStatus = null,
455
+ } = options;
214
456
 
215
457
  this._lib = getLib();
458
+ this.headers = {}; // Default headers
216
459
 
217
460
  const config = {
218
461
  preset,
219
462
  timeout,
463
+ http_version: httpVersion,
220
464
  };
221
465
  if (proxy) {
222
466
  config.proxy = proxy;
223
467
  }
468
+ if (!verify) {
469
+ config.verify = false;
470
+ }
471
+ if (!allowRedirects) {
472
+ config.allow_redirects = false;
473
+ } else if (maxRedirects !== 10) {
474
+ config.max_redirects = maxRedirects;
475
+ }
476
+ if (retry > 0) {
477
+ config.retry = retry;
478
+ if (retryOnStatus) {
479
+ config.retry_on_status = retryOnStatus;
480
+ }
481
+ }
224
482
 
225
483
  this._handle = this._lib.httpcloak_session_new(JSON.stringify(config));
226
484
 
@@ -239,6 +497,16 @@ class Session {
239
497
  }
240
498
  }
241
499
 
500
+ /**
501
+ * Merge session headers with request headers
502
+ */
503
+ _mergeHeaders(headers) {
504
+ if (!this.headers || Object.keys(this.headers).length === 0) {
505
+ return headers;
506
+ }
507
+ return { ...this.headers, ...headers };
508
+ }
509
+
242
510
  // ===========================================================================
243
511
  // Synchronous Methods
244
512
  // ===========================================================================
@@ -246,11 +514,20 @@ class Session {
246
514
  /**
247
515
  * Perform a synchronous GET request
248
516
  * @param {string} url - Request URL
249
- * @param {Object} [headers] - Optional custom headers
517
+ * @param {Object} [options] - Request options
518
+ * @param {Object} [options.headers] - Custom headers
519
+ * @param {Object} [options.params] - Query parameters
520
+ * @param {Array} [options.auth] - Basic auth [username, password]
250
521
  * @returns {Response} Response object
251
522
  */
252
- getSync(url, headers = null) {
253
- const headersJson = headers ? JSON.stringify(headers) : null;
523
+ getSync(url, options = {}) {
524
+ const { headers = null, params = null, auth = null } = options;
525
+
526
+ url = addParamsToUrl(url, params);
527
+ let mergedHeaders = this._mergeHeaders(headers);
528
+ mergedHeaders = applyAuth(mergedHeaders, auth);
529
+
530
+ const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
254
531
  const result = this._lib.httpcloak_get(this._handle, url, headersJson);
255
532
  return parseResponse(result);
256
533
  }
@@ -258,58 +535,111 @@ class Session {
258
535
  /**
259
536
  * Perform a synchronous POST request
260
537
  * @param {string} url - Request URL
261
- * @param {string|Buffer|Object} [body] - Request body
262
- * @param {Object} [headers] - Optional custom headers
538
+ * @param {Object} [options] - Request options
539
+ * @param {string|Buffer|Object} [options.body] - Request body
540
+ * @param {Object} [options.json] - JSON body (will be serialized)
541
+ * @param {Object} [options.data] - Form data (will be URL encoded)
542
+ * @param {Object} [options.files] - Files to upload as multipart/form-data
543
+ * Each key is the field name, value can be:
544
+ * - Buffer: raw file content
545
+ * - { filename, content, contentType? }: file with metadata
546
+ * @param {Object} [options.headers] - Custom headers
547
+ * @param {Object} [options.params] - Query parameters
548
+ * @param {Array} [options.auth] - Basic auth [username, password]
263
549
  * @returns {Response} Response object
264
550
  */
265
- postSync(url, body = null, headers = null) {
266
- if (typeof body === "object" && body !== null && !Buffer.isBuffer(body)) {
267
- body = JSON.stringify(body);
268
- headers = headers || {};
269
- if (!headers["Content-Type"]) {
270
- headers["Content-Type"] = "application/json";
551
+ postSync(url, options = {}) {
552
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, auth = null } = options;
553
+
554
+ url = addParamsToUrl(url, params);
555
+ let mergedHeaders = this._mergeHeaders(headers);
556
+
557
+ // Handle multipart file upload
558
+ if (files !== null) {
559
+ const formData = (data !== null && typeof data === "object") ? data : null;
560
+ const multipart = encodeMultipart(formData, files);
561
+ body = multipart.body.toString("latin1"); // Preserve binary data
562
+ mergedHeaders = mergedHeaders || {};
563
+ mergedHeaders["Content-Type"] = multipart.contentType;
564
+ }
565
+ // Handle JSON body
566
+ else if (json !== null) {
567
+ body = JSON.stringify(json);
568
+ mergedHeaders = mergedHeaders || {};
569
+ if (!mergedHeaders["Content-Type"]) {
570
+ mergedHeaders["Content-Type"] = "application/json";
271
571
  }
272
572
  }
273
-
274
- if (Buffer.isBuffer(body)) {
573
+ // Handle form data
574
+ else if (data !== null && typeof data === "object") {
575
+ body = new URLSearchParams(data).toString();
576
+ mergedHeaders = mergedHeaders || {};
577
+ if (!mergedHeaders["Content-Type"]) {
578
+ mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
579
+ }
580
+ }
581
+ // Handle Buffer body
582
+ else if (Buffer.isBuffer(body)) {
275
583
  body = body.toString("utf8");
276
584
  }
277
585
 
278
- const headersJson = headers ? JSON.stringify(headers) : null;
586
+ mergedHeaders = applyAuth(mergedHeaders, auth);
587
+
588
+ const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
279
589
  const result = this._lib.httpcloak_post(this._handle, url, body, headersJson);
280
590
  return parseResponse(result);
281
591
  }
282
592
 
283
593
  /**
284
594
  * Perform a synchronous custom HTTP request
285
- * @param {Object} options - Request options
286
- * @param {string} options.method - HTTP method
287
- * @param {string} options.url - Request URL
288
- * @param {Object} [options.headers] - Optional custom headers
289
- * @param {string|Buffer|Object} [options.body] - Optional request body
290
- * @param {number} [options.timeout] - Optional request timeout
595
+ * @param {string} method - HTTP method
596
+ * @param {string} url - Request URL
597
+ * @param {Object} [options] - Request options
598
+ * @param {Object} [options.files] - Files to upload as multipart/form-data
291
599
  * @returns {Response} Response object
292
600
  */
293
- requestSync(options) {
294
- let { method, url, headers = null, body = null, timeout = null } = options;
295
-
296
- if (typeof body === "object" && body !== null && !Buffer.isBuffer(body)) {
297
- body = JSON.stringify(body);
298
- headers = headers || {};
299
- if (!headers["Content-Type"]) {
300
- headers["Content-Type"] = "application/json";
601
+ requestSync(method, url, options = {}) {
602
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, auth = null, timeout = null } = options;
603
+
604
+ url = addParamsToUrl(url, params);
605
+ let mergedHeaders = this._mergeHeaders(headers);
606
+
607
+ // Handle multipart file upload
608
+ if (files !== null) {
609
+ const formData = (data !== null && typeof data === "object") ? data : null;
610
+ const multipart = encodeMultipart(formData, files);
611
+ body = multipart.body.toString("latin1"); // Preserve binary data
612
+ mergedHeaders = mergedHeaders || {};
613
+ mergedHeaders["Content-Type"] = multipart.contentType;
614
+ }
615
+ // Handle JSON body
616
+ else if (json !== null) {
617
+ body = JSON.stringify(json);
618
+ mergedHeaders = mergedHeaders || {};
619
+ if (!mergedHeaders["Content-Type"]) {
620
+ mergedHeaders["Content-Type"] = "application/json";
301
621
  }
302
622
  }
303
-
304
- if (Buffer.isBuffer(body)) {
623
+ // Handle form data
624
+ else if (data !== null && typeof data === "object") {
625
+ body = new URLSearchParams(data).toString();
626
+ mergedHeaders = mergedHeaders || {};
627
+ if (!mergedHeaders["Content-Type"]) {
628
+ mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
629
+ }
630
+ }
631
+ // Handle Buffer body
632
+ else if (Buffer.isBuffer(body)) {
305
633
  body = body.toString("utf8");
306
634
  }
307
635
 
636
+ mergedHeaders = applyAuth(mergedHeaders, auth);
637
+
308
638
  const requestConfig = {
309
639
  method: method.toUpperCase(),
310
640
  url,
311
641
  };
312
- if (headers) requestConfig.headers = headers;
642
+ if (mergedHeaders) requestConfig.headers = mergedHeaders;
313
643
  if (body) requestConfig.body = body;
314
644
  if (timeout) requestConfig.timeout = timeout;
315
645
 
@@ -327,14 +657,14 @@ class Session {
327
657
  /**
328
658
  * Perform an async GET request
329
659
  * @param {string} url - Request URL
330
- * @param {Object} [headers] - Optional custom headers
660
+ * @param {Object} [options] - Request options
331
661
  * @returns {Promise<Response>} Response object
332
662
  */
333
- get(url, headers = null) {
663
+ get(url, options = {}) {
334
664
  return new Promise((resolve, reject) => {
335
665
  setImmediate(() => {
336
666
  try {
337
- resolve(this.getSync(url, headers));
667
+ resolve(this.getSync(url, options));
338
668
  } catch (err) {
339
669
  reject(err);
340
670
  }
@@ -345,15 +675,14 @@ class Session {
345
675
  /**
346
676
  * Perform an async POST request
347
677
  * @param {string} url - Request URL
348
- * @param {string|Buffer|Object} [body] - Request body
349
- * @param {Object} [headers] - Optional custom headers
678
+ * @param {Object} [options] - Request options
350
679
  * @returns {Promise<Response>} Response object
351
680
  */
352
- post(url, body = null, headers = null) {
681
+ post(url, options = {}) {
353
682
  return new Promise((resolve, reject) => {
354
683
  setImmediate(() => {
355
684
  try {
356
- resolve(this.postSync(url, body, headers));
685
+ resolve(this.postSync(url, options));
357
686
  } catch (err) {
358
687
  reject(err);
359
688
  }
@@ -363,14 +692,16 @@ class Session {
363
692
 
364
693
  /**
365
694
  * Perform an async custom HTTP request
366
- * @param {Object} options - Request options
695
+ * @param {string} method - HTTP method
696
+ * @param {string} url - Request URL
697
+ * @param {Object} [options] - Request options
367
698
  * @returns {Promise<Response>} Response object
368
699
  */
369
- request(options) {
700
+ request(method, url, options = {}) {
370
701
  return new Promise((resolve, reject) => {
371
702
  setImmediate(() => {
372
703
  try {
373
- resolve(this.requestSync(options));
704
+ resolve(this.requestSync(method, url, options));
374
705
  } catch (err) {
375
706
  reject(err);
376
707
  }
@@ -380,133 +711,37 @@ class Session {
380
711
 
381
712
  /**
382
713
  * Perform an async PUT request
383
- * @param {string} url - Request URL
384
- * @param {string|Buffer|Object} [body] - Request body
385
- * @param {Object} [headers] - Optional custom headers
386
- * @returns {Promise<Response>} Response object
387
714
  */
388
- put(url, body = null, headers = null) {
389
- return this.request({ method: "PUT", url, body, headers });
715
+ put(url, options = {}) {
716
+ return this.request("PUT", url, options);
390
717
  }
391
718
 
392
719
  /**
393
720
  * Perform an async DELETE request
394
- * @param {string} url - Request URL
395
- * @param {Object} [headers] - Optional custom headers
396
- * @returns {Promise<Response>} Response object
397
721
  */
398
- delete(url, headers = null) {
399
- return this.request({ method: "DELETE", url, headers });
722
+ delete(url, options = {}) {
723
+ return this.request("DELETE", url, options);
400
724
  }
401
725
 
402
726
  /**
403
727
  * Perform an async PATCH request
404
- * @param {string} url - Request URL
405
- * @param {string|Buffer|Object} [body] - Request body
406
- * @param {Object} [headers] - Optional custom headers
407
- * @returns {Promise<Response>} Response object
408
728
  */
409
- patch(url, body = null, headers = null) {
410
- return this.request({ method: "PATCH", url, body, headers });
729
+ patch(url, options = {}) {
730
+ return this.request("PATCH", url, options);
411
731
  }
412
732
 
413
733
  /**
414
734
  * Perform an async HEAD request
415
- * @param {string} url - Request URL
416
- * @param {Object} [headers] - Optional custom headers
417
- * @returns {Promise<Response>} Response object
418
735
  */
419
- head(url, headers = null) {
420
- return this.request({ method: "HEAD", url, headers });
736
+ head(url, options = {}) {
737
+ return this.request("HEAD", url, options);
421
738
  }
422
739
 
423
740
  /**
424
741
  * Perform an async OPTIONS request
425
- * @param {string} url - Request URL
426
- * @param {Object} [headers] - Optional custom headers
427
- * @returns {Promise<Response>} Response object
428
742
  */
429
- options(url, headers = null) {
430
- return this.request({ method: "OPTIONS", url, headers });
431
- }
432
-
433
- // ===========================================================================
434
- // Callback-based Methods
435
- // ===========================================================================
436
-
437
- /**
438
- * Perform a GET request with callback
439
- * @param {string} url - Request URL
440
- * @param {Object|Function} [headersOrCallback] - Headers or callback
441
- * @param {Function} [callback] - Callback function (err, response)
442
- */
443
- getCb(url, headersOrCallback, callback) {
444
- let headers = null;
445
- let cb = callback;
446
-
447
- if (typeof headersOrCallback === "function") {
448
- cb = headersOrCallback;
449
- } else {
450
- headers = headersOrCallback;
451
- }
452
-
453
- setImmediate(() => {
454
- try {
455
- const response = this.getSync(url, headers);
456
- cb(null, response);
457
- } catch (err) {
458
- cb(err, null);
459
- }
460
- });
461
- }
462
-
463
- /**
464
- * Perform a POST request with callback
465
- * @param {string} url - Request URL
466
- * @param {string|Buffer|Object} [body] - Request body
467
- * @param {Object|Function} [headersOrCallback] - Headers or callback
468
- * @param {Function} [callback] - Callback function (err, response)
469
- */
470
- postCb(url, body, headersOrCallback, callback) {
471
- let headers = null;
472
- let cb = callback;
473
-
474
- if (typeof headersOrCallback === "function") {
475
- cb = headersOrCallback;
476
- } else {
477
- headers = headersOrCallback;
478
- cb = callback;
479
- }
480
-
481
- if (typeof body === "function") {
482
- cb = body;
483
- body = null;
484
- }
485
-
486
- setImmediate(() => {
487
- try {
488
- const response = this.postSync(url, body, headers);
489
- cb(null, response);
490
- } catch (err) {
491
- cb(err, null);
492
- }
493
- });
494
- }
495
-
496
- /**
497
- * Perform a custom request with callback
498
- * @param {Object} options - Request options
499
- * @param {Function} callback - Callback function (err, response)
500
- */
501
- requestCb(options, callback) {
502
- setImmediate(() => {
503
- try {
504
- const response = this.requestSync(options);
505
- callback(null, response);
506
- } catch (err) {
507
- callback(err, null);
508
- }
509
- });
743
+ options(url, options = {}) {
744
+ return this.request("OPTIONS", url, options);
510
745
  }
511
746
 
512
747
  // ===========================================================================
@@ -518,7 +753,8 @@ class Session {
518
753
  * @returns {Object} Cookies as key-value pairs
519
754
  */
520
755
  getCookies() {
521
- const result = this._lib.httpcloak_get_cookies(this._handle);
756
+ const resultPtr = this._lib.httpcloak_get_cookies(this._handle);
757
+ const result = ptrToString(resultPtr);
522
758
  if (result) {
523
759
  return JSON.parse(result);
524
760
  }
@@ -542,10 +778,198 @@ class Session {
542
778
  }
543
779
  }
544
780
 
781
+ // =============================================================================
782
+ // Module-level convenience functions
783
+ // =============================================================================
784
+
785
+ let _defaultSession = null;
786
+ let _defaultConfig = {};
787
+
788
+ /**
789
+ * Configure defaults for module-level functions
790
+ * @param {Object} options - Configuration options
791
+ * @param {string} [options.preset="chrome-143"] - Browser preset
792
+ * @param {Object} [options.headers] - Default headers
793
+ * @param {Array} [options.auth] - Default basic auth [username, password]
794
+ * @param {string} [options.proxy] - Proxy URL
795
+ * @param {number} [options.timeout=30] - Default timeout in seconds
796
+ * @param {string} [options.httpVersion="auto"] - HTTP version: "auto", "h1", "h2", "h3"
797
+ * @param {boolean} [options.verify=true] - SSL certificate verification
798
+ * @param {boolean} [options.allowRedirects=true] - Follow redirects
799
+ * @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
800
+ * @param {number} [options.retry=0] - Number of retries on failure
801
+ * @param {number[]} [options.retryOnStatus] - Status codes to retry on
802
+ */
803
+ function configure(options = {}) {
804
+ const {
805
+ preset = "chrome-143",
806
+ headers = null,
807
+ auth = null,
808
+ proxy = null,
809
+ timeout = 30,
810
+ httpVersion = "auto",
811
+ verify = true,
812
+ allowRedirects = true,
813
+ maxRedirects = 10,
814
+ retry = 0,
815
+ retryOnStatus = null,
816
+ } = options;
817
+
818
+ // Close existing session
819
+ if (_defaultSession) {
820
+ _defaultSession.close();
821
+ _defaultSession = null;
822
+ }
823
+
824
+ // Apply auth to headers
825
+ let finalHeaders = applyAuth(headers, auth) || {};
826
+
827
+ // Store config
828
+ _defaultConfig = {
829
+ preset,
830
+ proxy,
831
+ timeout,
832
+ httpVersion,
833
+ verify,
834
+ allowRedirects,
835
+ maxRedirects,
836
+ retry,
837
+ retryOnStatus,
838
+ headers: finalHeaders,
839
+ };
840
+
841
+ // Create new session
842
+ _defaultSession = new Session({
843
+ preset,
844
+ proxy,
845
+ timeout,
846
+ httpVersion,
847
+ verify,
848
+ allowRedirects,
849
+ maxRedirects,
850
+ retry,
851
+ retryOnStatus,
852
+ });
853
+ if (Object.keys(finalHeaders).length > 0) {
854
+ Object.assign(_defaultSession.headers, finalHeaders);
855
+ }
856
+ }
857
+
858
+ /**
859
+ * Get or create the default session
860
+ */
861
+ function _getDefaultSession() {
862
+ if (!_defaultSession) {
863
+ const preset = _defaultConfig.preset || "chrome-143";
864
+ const proxy = _defaultConfig.proxy || null;
865
+ const timeout = _defaultConfig.timeout || 30;
866
+ const httpVersion = _defaultConfig.httpVersion || "auto";
867
+ const verify = _defaultConfig.verify !== undefined ? _defaultConfig.verify : true;
868
+ const allowRedirects = _defaultConfig.allowRedirects !== undefined ? _defaultConfig.allowRedirects : true;
869
+ const maxRedirects = _defaultConfig.maxRedirects || 10;
870
+ const retry = _defaultConfig.retry || 0;
871
+ const retryOnStatus = _defaultConfig.retryOnStatus || null;
872
+ const headers = _defaultConfig.headers || {};
873
+
874
+ _defaultSession = new Session({
875
+ preset,
876
+ proxy,
877
+ timeout,
878
+ httpVersion,
879
+ verify,
880
+ allowRedirects,
881
+ maxRedirects,
882
+ retry,
883
+ retryOnStatus,
884
+ });
885
+ if (Object.keys(headers).length > 0) {
886
+ Object.assign(_defaultSession.headers, headers);
887
+ }
888
+ }
889
+ return _defaultSession;
890
+ }
891
+
892
+ /**
893
+ * Perform a GET request
894
+ * @param {string} url - Request URL
895
+ * @param {Object} [options] - Request options
896
+ * @returns {Promise<Response>}
897
+ */
898
+ function get(url, options = {}) {
899
+ return _getDefaultSession().get(url, options);
900
+ }
901
+
902
+ /**
903
+ * Perform a POST request
904
+ * @param {string} url - Request URL
905
+ * @param {Object} [options] - Request options
906
+ * @returns {Promise<Response>}
907
+ */
908
+ function post(url, options = {}) {
909
+ return _getDefaultSession().post(url, options);
910
+ }
911
+
912
+ /**
913
+ * Perform a PUT request
914
+ */
915
+ function put(url, options = {}) {
916
+ return _getDefaultSession().put(url, options);
917
+ }
918
+
919
+ /**
920
+ * Perform a DELETE request
921
+ */
922
+ function del(url, options = {}) {
923
+ return _getDefaultSession().delete(url, options);
924
+ }
925
+
926
+ /**
927
+ * Perform a PATCH request
928
+ */
929
+ function patch(url, options = {}) {
930
+ return _getDefaultSession().patch(url, options);
931
+ }
932
+
933
+ /**
934
+ * Perform a HEAD request
935
+ */
936
+ function head(url, options = {}) {
937
+ return _getDefaultSession().head(url, options);
938
+ }
939
+
940
+ /**
941
+ * Perform an OPTIONS request
942
+ */
943
+ function options(url, opts = {}) {
944
+ return _getDefaultSession().options(url, opts);
945
+ }
946
+
947
+ /**
948
+ * Perform a custom HTTP request
949
+ */
950
+ function request(method, url, options = {}) {
951
+ return _getDefaultSession().request(method, url, options);
952
+ }
953
+
545
954
  module.exports = {
955
+ // Classes
546
956
  Session,
547
957
  Response,
548
958
  HTTPCloakError,
959
+ // Presets
960
+ Preset,
961
+ // Configuration
962
+ configure,
963
+ // Module-level functions
964
+ get,
965
+ post,
966
+ put,
967
+ delete: del,
968
+ patch,
969
+ head,
970
+ options,
971
+ request,
972
+ // Utility
549
973
  version,
550
974
  availablePresets,
551
975
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/darwin-arm64",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "HTTPCloak native binary for darwin arm64",
5
5
  "os": [
6
6
  "darwin"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/darwin-x64",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "HTTPCloak native binary for darwin x64",
5
5
  "os": [
6
6
  "darwin"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/linux-arm64",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "HTTPCloak native binary for linux arm64",
5
5
  "os": [
6
6
  "linux"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/linux-x64",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "HTTPCloak native binary for linux x64",
5
5
  "os": [
6
6
  "linux"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/win32-arm64",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "HTTPCloak native binary for win32 arm64",
5
5
  "os": [
6
6
  "win32"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/win32-x64",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "HTTPCloak native binary for win32 x64",
5
5
  "os": [
6
6
  "win32"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "httpcloak",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Browser fingerprint emulation HTTP client with HTTP/1.1, HTTP/2, and HTTP/3 support",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -35,11 +35,11 @@
35
35
  "koffi": "^2.9.0"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@httpcloak/linux-x64": "1.0.0",
39
- "@httpcloak/linux-arm64": "1.0.0",
40
- "@httpcloak/darwin-x64": "1.0.0",
41
- "@httpcloak/darwin-arm64": "1.0.0",
42
- "@httpcloak/win32-x64": "1.0.0",
43
- "@httpcloak/win32-arm64": "1.0.0"
38
+ "@httpcloak/linux-x64": "1.0.2",
39
+ "@httpcloak/linux-arm64": "1.0.2",
40
+ "@httpcloak/darwin-x64": "1.0.2",
41
+ "@httpcloak/darwin-arm64": "1.0.2",
42
+ "@httpcloak/win32-x64": "1.0.2",
43
+ "@httpcloak/win32-arm64": "1.0.2"
44
44
  }
45
45
  }