httpcloak 1.5.0 → 1.5.1

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
@@ -69,17 +69,105 @@ const Preset = {
69
69
  },
70
70
  };
71
71
 
72
+ /**
73
+ * HTTP status reason phrases
74
+ */
75
+ const HTTP_STATUS_PHRASES = {
76
+ 100: "Continue", 101: "Switching Protocols", 102: "Processing",
77
+ 200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information",
78
+ 204: "No Content", 205: "Reset Content", 206: "Partial Content", 207: "Multi-Status",
79
+ 300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other",
80
+ 304: "Not Modified", 305: "Use Proxy", 307: "Temporary Redirect", 308: "Permanent Redirect",
81
+ 400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden",
82
+ 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable",
83
+ 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict",
84
+ 410: "Gone", 411: "Length Required", 412: "Precondition Failed",
85
+ 413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type",
86
+ 416: "Range Not Satisfiable", 417: "Expectation Failed", 418: "I'm a teapot",
87
+ 421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked",
88
+ 424: "Failed Dependency", 425: "Too Early", 426: "Upgrade Required",
89
+ 428: "Precondition Required", 429: "Too Many Requests",
90
+ 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons",
91
+ 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway",
92
+ 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported",
93
+ 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected",
94
+ 510: "Not Extended", 511: "Network Authentication Required",
95
+ };
96
+
97
+ /**
98
+ * Cookie object from Set-Cookie header
99
+ */
100
+ class Cookie {
101
+ /**
102
+ * @param {string} name - Cookie name
103
+ * @param {string} value - Cookie value
104
+ */
105
+ constructor(name, value) {
106
+ this.name = name;
107
+ this.value = value;
108
+ }
109
+
110
+ toString() {
111
+ return `Cookie(name=${this.name}, value=${this.value})`;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Redirect info from history
117
+ */
118
+ class RedirectInfo {
119
+ /**
120
+ * @param {number} statusCode - HTTP status code
121
+ * @param {string} url - Request URL
122
+ * @param {Object} headers - Response headers
123
+ */
124
+ constructor(statusCode, url, headers) {
125
+ this.statusCode = statusCode;
126
+ this.url = url;
127
+ this.headers = headers || {};
128
+ }
129
+
130
+ toString() {
131
+ return `RedirectInfo(statusCode=${this.statusCode}, url=${this.url})`;
132
+ }
133
+ }
134
+
72
135
  /**
73
136
  * Response object returned from HTTP requests
74
137
  */
75
138
  class Response {
76
- constructor(data) {
139
+ /**
140
+ * @param {Object} data - Response data from native library
141
+ * @param {number} [elapsed=0] - Elapsed time in milliseconds
142
+ */
143
+ constructor(data, elapsed = 0) {
77
144
  this.statusCode = data.status_code || 0;
78
145
  this.headers = data.headers || {};
79
146
  this._body = Buffer.from(data.body || "", "utf8");
80
147
  this._text = data.body || "";
81
148
  this.finalUrl = data.final_url || "";
82
149
  this.protocol = data.protocol || "";
150
+ this.elapsed = elapsed; // milliseconds
151
+
152
+ // Parse cookies from response
153
+ this._cookies = (data.cookies || []).map(c => new Cookie(c.name || "", c.value || ""));
154
+
155
+ // Parse redirect history
156
+ this._history = (data.history || []).map(h => new RedirectInfo(
157
+ h.status_code || 0,
158
+ h.url || "",
159
+ h.headers || {}
160
+ ));
161
+ }
162
+
163
+ /** Cookies set by this response */
164
+ get cookies() {
165
+ return this._cookies;
166
+ }
167
+
168
+ /** Redirect history (list of RedirectInfo objects) */
169
+ get history() {
170
+ return this._history;
83
171
  }
84
172
 
85
173
  /** Response body as string */
@@ -107,6 +195,29 @@ class Response {
107
195
  return this.statusCode < 400;
108
196
  }
109
197
 
198
+ /** HTTP status reason phrase (e.g., 'OK', 'Not Found') */
199
+ get reason() {
200
+ return HTTP_STATUS_PHRASES[this.statusCode] || "Unknown";
201
+ }
202
+
203
+ /**
204
+ * Response encoding from Content-Type header.
205
+ * Returns null if not specified.
206
+ */
207
+ get encoding() {
208
+ let contentType = this.headers["content-type"] || this.headers["Content-Type"] || "";
209
+ if (contentType.includes("charset=")) {
210
+ const parts = contentType.split(";");
211
+ for (const part of parts) {
212
+ const trimmed = part.trim();
213
+ if (trimmed.toLowerCase().startsWith("charset=")) {
214
+ return trimmed.split("=")[1].trim().replace(/['"]/g, "");
215
+ }
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+
110
221
  /**
111
222
  * Parse response body as JSON
112
223
  */
@@ -119,7 +230,7 @@ class Response {
119
230
  */
120
231
  raiseForStatus() {
121
232
  if (!this.ok) {
122
- throw new HTTPCloakError(`HTTP ${this.statusCode}`);
233
+ throw new HTTPCloakError(`HTTP ${this.statusCode}: ${this.reason}`);
123
234
  }
124
235
  }
125
236
  }
@@ -215,32 +326,158 @@ function getLibPath() {
215
326
  );
216
327
  }
217
328
 
329
+ // Define callback proto globally for koffi (must be before getLib)
330
+ const AsyncCallbackProto = koffi.proto("void AsyncCallback(int64 callbackId, str responseJson, str error)");
331
+
218
332
  // Load the native library
219
333
  let lib = null;
334
+ let nativeLibHandle = null;
220
335
 
221
336
  function getLib() {
222
337
  if (lib === null) {
223
338
  const libPath = getLibPath();
224
- const nativeLib = koffi.load(libPath);
339
+ nativeLibHandle = koffi.load(libPath);
225
340
 
226
341
  // Use str for string returns - koffi handles the string copy automatically
227
342
  // Note: The C strings allocated by Go are not freed, but Go's GC handles them
228
343
  lib = {
229
- httpcloak_session_new: nativeLib.func("httpcloak_session_new", "int64", ["str"]),
230
- httpcloak_session_free: nativeLib.func("httpcloak_session_free", "void", ["int64"]),
231
- httpcloak_get: nativeLib.func("httpcloak_get", "str", ["int64", "str", "str"]),
232
- httpcloak_post: nativeLib.func("httpcloak_post", "str", ["int64", "str", "str", "str"]),
233
- httpcloak_request: nativeLib.func("httpcloak_request", "str", ["int64", "str"]),
234
- httpcloak_get_cookies: nativeLib.func("httpcloak_get_cookies", "str", ["int64"]),
235
- httpcloak_set_cookie: nativeLib.func("httpcloak_set_cookie", "void", ["int64", "str", "str"]),
236
- httpcloak_free_string: nativeLib.func("httpcloak_free_string", "void", ["void*"]),
237
- httpcloak_version: nativeLib.func("httpcloak_version", "str", []),
238
- httpcloak_available_presets: nativeLib.func("httpcloak_available_presets", "str", []),
344
+ httpcloak_session_new: nativeLibHandle.func("httpcloak_session_new", "int64", ["str"]),
345
+ httpcloak_session_free: nativeLibHandle.func("httpcloak_session_free", "void", ["int64"]),
346
+ httpcloak_get: nativeLibHandle.func("httpcloak_get", "str", ["int64", "str", "str"]),
347
+ httpcloak_post: nativeLibHandle.func("httpcloak_post", "str", ["int64", "str", "str", "str"]),
348
+ httpcloak_request: nativeLibHandle.func("httpcloak_request", "str", ["int64", "str"]),
349
+ httpcloak_get_cookies: nativeLibHandle.func("httpcloak_get_cookies", "str", ["int64"]),
350
+ httpcloak_set_cookie: nativeLibHandle.func("httpcloak_set_cookie", "void", ["int64", "str", "str"]),
351
+ httpcloak_free_string: nativeLibHandle.func("httpcloak_free_string", "void", ["void*"]),
352
+ httpcloak_version: nativeLibHandle.func("httpcloak_version", "str", []),
353
+ httpcloak_available_presets: nativeLibHandle.func("httpcloak_available_presets", "str", []),
354
+ // Async functions
355
+ httpcloak_register_callback: nativeLibHandle.func("httpcloak_register_callback", "int64", [koffi.pointer(AsyncCallbackProto)]),
356
+ httpcloak_unregister_callback: nativeLibHandle.func("httpcloak_unregister_callback", "void", ["int64"]),
357
+ httpcloak_get_async: nativeLibHandle.func("httpcloak_get_async", "void", ["int64", "str", "str", "int64"]),
358
+ httpcloak_post_async: nativeLibHandle.func("httpcloak_post_async", "void", ["int64", "str", "str", "str", "int64"]),
359
+ httpcloak_request_async: nativeLibHandle.func("httpcloak_request_async", "void", ["int64", "str", "int64"]),
239
360
  };
240
361
  }
241
362
  return lib;
242
363
  }
243
364
 
365
+ /**
366
+ * Async callback manager for native Go goroutine-based async
367
+ *
368
+ * Each async request registers a callback with Go and receives a unique ID.
369
+ * When Go completes the request, it invokes the callback with that ID.
370
+ */
371
+ class AsyncCallbackManager {
372
+ constructor() {
373
+ // callbackId -> { resolve, reject, startTime }
374
+ this._pendingRequests = new Map();
375
+ this._callbackPtr = null;
376
+ this._refTimer = null; // Timer to keep event loop alive
377
+ }
378
+
379
+ /**
380
+ * Ref the event loop to prevent Node.js from exiting while requests are pending
381
+ */
382
+ _ref() {
383
+ if (this._refTimer === null) {
384
+ // Create a timer that keeps the event loop alive
385
+ this._refTimer = setInterval(() => {}, 2147483647); // Max interval
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Unref the event loop when no more pending requests
391
+ */
392
+ _unref() {
393
+ if (this._pendingRequests.size === 0 && this._refTimer !== null) {
394
+ clearInterval(this._refTimer);
395
+ this._refTimer = null;
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Ensure the callback is set up with koffi
401
+ */
402
+ _ensureCallback() {
403
+ if (this._callbackPtr !== null) {
404
+ return;
405
+ }
406
+
407
+ // Create callback function that will be invoked by Go
408
+ // koffi.register expects koffi.pointer(proto) as the type
409
+ this._callbackPtr = koffi.register((callbackId, responseJson, error) => {
410
+ const pending = this._pendingRequests.get(Number(callbackId));
411
+ if (!pending) {
412
+ return;
413
+ }
414
+ this._pendingRequests.delete(Number(callbackId));
415
+ this._unref(); // Check if we can release the event loop
416
+
417
+ const { resolve, reject, startTime } = pending;
418
+ const elapsed = Date.now() - startTime;
419
+
420
+ if (error && error !== "") {
421
+ let errMsg = error;
422
+ try {
423
+ const errData = JSON.parse(error);
424
+ errMsg = errData.error || error;
425
+ } catch (e) {
426
+ // Use raw error string
427
+ }
428
+ reject(new HTTPCloakError(errMsg));
429
+ } else if (responseJson) {
430
+ try {
431
+ const data = JSON.parse(responseJson);
432
+ if (data.error) {
433
+ reject(new HTTPCloakError(data.error));
434
+ } else {
435
+ resolve(new Response(data, elapsed));
436
+ }
437
+ } catch (e) {
438
+ reject(new HTTPCloakError(`Failed to parse response: ${e.message}`));
439
+ }
440
+ } else {
441
+ reject(new HTTPCloakError("No response received"));
442
+ }
443
+ }, koffi.pointer(AsyncCallbackProto));
444
+ }
445
+
446
+ /**
447
+ * Register a new async request
448
+ * @returns {{ callbackId: number, promise: Promise<Response> }}
449
+ */
450
+ registerRequest(nativeLib) {
451
+ this._ensureCallback();
452
+
453
+ // Register callback with Go (each request gets unique ID)
454
+ const callbackId = nativeLib.httpcloak_register_callback(this._callbackPtr);
455
+
456
+ // Create promise for this request with start time
457
+ let resolve, reject;
458
+ const promise = new Promise((res, rej) => {
459
+ resolve = res;
460
+ reject = rej;
461
+ });
462
+ const startTime = Date.now();
463
+
464
+ this._pendingRequests.set(Number(callbackId), { resolve, reject, startTime });
465
+ this._ref(); // Keep event loop alive
466
+
467
+ return { callbackId, promise };
468
+ }
469
+ }
470
+
471
+ // Global async callback manager
472
+ let asyncManager = null;
473
+
474
+ function getAsyncManager() {
475
+ if (asyncManager === null) {
476
+ asyncManager = new AsyncCallbackManager();
477
+ }
478
+ return asyncManager;
479
+ }
480
+
244
481
  /**
245
482
  * Convert result to string (handles both direct strings and null)
246
483
  * With "str" return type, koffi automatically handles the conversion
@@ -254,8 +491,11 @@ function resultToString(result) {
254
491
 
255
492
  /**
256
493
  * Parse response from the native library
494
+ * @param {string} resultPtr - Result pointer from native function
495
+ * @param {number} [elapsed=0] - Elapsed time in milliseconds
496
+ * @returns {Response}
257
497
  */
258
- function parseResponse(resultPtr) {
498
+ function parseResponse(resultPtr, elapsed = 0) {
259
499
  const result = resultToString(resultPtr);
260
500
  if (!result) {
261
501
  throw new HTTPCloakError("No response received");
@@ -267,7 +507,7 @@ function parseResponse(resultPtr) {
267
507
  throw new HTTPCloakError(data.error);
268
508
  }
269
509
 
270
- return new Response(data);
510
+ return new Response(data, elapsed);
271
511
  }
272
512
 
273
513
  /**
@@ -442,6 +682,7 @@ class Session {
442
682
  * @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
443
683
  * @param {number} [options.retry=3] - Number of retries on failure (set to 0 to disable)
444
684
  * @param {number[]} [options.retryOnStatus] - Status codes to retry on
685
+ * @param {Array} [options.auth] - Default auth [username, password] for all requests
445
686
  */
446
687
  constructor(options = {}) {
447
688
  const {
@@ -455,10 +696,12 @@ class Session {
455
696
  retry = 3,
456
697
  retryOnStatus = null,
457
698
  preferIpv4 = false,
699
+ auth = null,
458
700
  } = options;
459
701
 
460
702
  this._lib = getLib();
461
703
  this.headers = {}; // Default headers
704
+ this.auth = auth; // Default auth for all requests
462
705
 
463
706
  const config = {
464
707
  preset,
@@ -512,6 +755,31 @@ class Session {
512
755
  return { ...this.headers, ...headers };
513
756
  }
514
757
 
758
+ /**
759
+ * Apply cookies to headers
760
+ * @param {Object} headers - Existing headers
761
+ * @param {Object} cookies - Cookies to apply as key-value pairs
762
+ * @returns {Object} Headers with cookies applied
763
+ */
764
+ _applyCookies(headers, cookies) {
765
+ if (!cookies || Object.keys(cookies).length === 0) {
766
+ return headers;
767
+ }
768
+
769
+ const cookieStr = Object.entries(cookies)
770
+ .map(([k, v]) => `${k}=${v}`)
771
+ .join("; ");
772
+
773
+ headers = headers ? { ...headers } : {};
774
+ const existing = headers["Cookie"] || "";
775
+ if (existing) {
776
+ headers["Cookie"] = `${existing}; ${cookieStr}`;
777
+ } else {
778
+ headers["Cookie"] = cookieStr;
779
+ }
780
+ return headers;
781
+ }
782
+
515
783
  // ===========================================================================
516
784
  // Synchronous Methods
517
785
  // ===========================================================================
@@ -522,19 +790,25 @@ class Session {
522
790
  * @param {Object} [options] - Request options
523
791
  * @param {Object} [options.headers] - Custom headers
524
792
  * @param {Object} [options.params] - Query parameters
793
+ * @param {Object} [options.cookies] - Cookies to send with this request
525
794
  * @param {Array} [options.auth] - Basic auth [username, password]
526
795
  * @returns {Response} Response object
527
796
  */
528
797
  getSync(url, options = {}) {
529
- const { headers = null, params = null, auth = null } = options;
798
+ const { headers = null, params = null, cookies = null, auth = null } = options;
530
799
 
531
800
  url = addParamsToUrl(url, params);
532
801
  let mergedHeaders = this._mergeHeaders(headers);
533
- mergedHeaders = applyAuth(mergedHeaders, auth);
802
+ // Use request auth if provided, otherwise fall back to session auth
803
+ const effectiveAuth = auth !== null ? auth : this.auth;
804
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
805
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
534
806
 
535
807
  const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
808
+ const startTime = Date.now();
536
809
  const result = this._lib.httpcloak_get(this._handle, url, headersJson);
537
- return parseResponse(result);
810
+ const elapsed = Date.now() - startTime;
811
+ return parseResponse(result, elapsed);
538
812
  }
539
813
 
540
814
  /**
@@ -550,11 +824,12 @@ class Session {
550
824
  * - { filename, content, contentType? }: file with metadata
551
825
  * @param {Object} [options.headers] - Custom headers
552
826
  * @param {Object} [options.params] - Query parameters
827
+ * @param {Object} [options.cookies] - Cookies to send with this request
553
828
  * @param {Array} [options.auth] - Basic auth [username, password]
554
829
  * @returns {Response} Response object
555
830
  */
556
831
  postSync(url, options = {}) {
557
- let { body = null, json = null, data = null, files = null, headers = null, params = null, auth = null } = options;
832
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
558
833
 
559
834
  url = addParamsToUrl(url, params);
560
835
  let mergedHeaders = this._mergeHeaders(headers);
@@ -588,11 +863,16 @@ class Session {
588
863
  body = body.toString("utf8");
589
864
  }
590
865
 
591
- mergedHeaders = applyAuth(mergedHeaders, auth);
866
+ // Use request auth if provided, otherwise fall back to session auth
867
+ const effectiveAuth = auth !== null ? auth : this.auth;
868
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
869
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
592
870
 
593
871
  const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
872
+ const startTime = Date.now();
594
873
  const result = this._lib.httpcloak_post(this._handle, url, body, headersJson);
595
- return parseResponse(result);
874
+ const elapsed = Date.now() - startTime;
875
+ return parseResponse(result, elapsed);
596
876
  }
597
877
 
598
878
  /**
@@ -600,11 +880,12 @@ class Session {
600
880
  * @param {string} method - HTTP method
601
881
  * @param {string} url - Request URL
602
882
  * @param {Object} [options] - Request options
883
+ * @param {Object} [options.cookies] - Cookies to send with this request
603
884
  * @param {Object} [options.files] - Files to upload as multipart/form-data
604
885
  * @returns {Response} Response object
605
886
  */
606
887
  requestSync(method, url, options = {}) {
607
- let { body = null, json = null, data = null, files = null, headers = null, params = null, auth = null, timeout = null } = options;
888
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null } = options;
608
889
 
609
890
  url = addParamsToUrl(url, params);
610
891
  let mergedHeaders = this._mergeHeaders(headers);
@@ -638,7 +919,10 @@ class Session {
638
919
  body = body.toString("utf8");
639
920
  }
640
921
 
641
- mergedHeaders = applyAuth(mergedHeaders, auth);
922
+ // Use request auth if provided, otherwise fall back to session auth
923
+ const effectiveAuth = auth !== null ? auth : this.auth;
924
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
925
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
642
926
 
643
927
  const requestConfig = {
644
928
  method: method.toUpperCase(),
@@ -648,70 +932,171 @@ class Session {
648
932
  if (body) requestConfig.body = body;
649
933
  if (timeout) requestConfig.timeout = timeout;
650
934
 
935
+ const startTime = Date.now();
651
936
  const result = this._lib.httpcloak_request(
652
937
  this._handle,
653
938
  JSON.stringify(requestConfig)
654
939
  );
655
- return parseResponse(result);
940
+ const elapsed = Date.now() - startTime;
941
+ return parseResponse(result, elapsed);
656
942
  }
657
943
 
658
944
  // ===========================================================================
659
- // Promise-based Methods
945
+ // Promise-based Methods (Native async using Go goroutines)
660
946
  // ===========================================================================
661
947
 
662
948
  /**
663
- * Perform an async GET request
949
+ * Perform an async GET request using native Go goroutines
664
950
  * @param {string} url - Request URL
665
951
  * @param {Object} [options] - Request options
952
+ * @param {Object} [options.cookies] - Cookies to send with this request
666
953
  * @returns {Promise<Response>} Response object
667
954
  */
668
955
  get(url, options = {}) {
669
- return new Promise((resolve, reject) => {
670
- setImmediate(() => {
671
- try {
672
- resolve(this.getSync(url, options));
673
- } catch (err) {
674
- reject(err);
675
- }
676
- });
677
- });
956
+ const { headers = null, params = null, cookies = null, auth = null } = options;
957
+
958
+ url = addParamsToUrl(url, params);
959
+ let mergedHeaders = this._mergeHeaders(headers);
960
+ // Use request auth if provided, otherwise fall back to session auth
961
+ const effectiveAuth = auth !== null ? auth : this.auth;
962
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
963
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
964
+
965
+ const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
966
+
967
+ // Register async request with callback manager
968
+ const manager = getAsyncManager();
969
+ const { callbackId, promise } = manager.registerRequest(this._lib);
970
+
971
+ // Start async request
972
+ this._lib.httpcloak_get_async(this._handle, url, headersJson, callbackId);
973
+
974
+ return promise;
678
975
  }
679
976
 
680
977
  /**
681
- * Perform an async POST request
978
+ * Perform an async POST request using native Go goroutines
682
979
  * @param {string} url - Request URL
683
980
  * @param {Object} [options] - Request options
981
+ * @param {Object} [options.cookies] - Cookies to send with this request
684
982
  * @returns {Promise<Response>} Response object
685
983
  */
686
984
  post(url, options = {}) {
687
- return new Promise((resolve, reject) => {
688
- setImmediate(() => {
689
- try {
690
- resolve(this.postSync(url, options));
691
- } catch (err) {
692
- reject(err);
693
- }
694
- });
695
- });
985
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
986
+
987
+ url = addParamsToUrl(url, params);
988
+ let mergedHeaders = this._mergeHeaders(headers);
989
+
990
+ // Handle multipart file upload
991
+ if (files !== null) {
992
+ const formData = (data !== null && typeof data === "object") ? data : null;
993
+ const multipart = encodeMultipart(formData, files);
994
+ body = multipart.body.toString("latin1");
995
+ mergedHeaders = mergedHeaders || {};
996
+ mergedHeaders["Content-Type"] = multipart.contentType;
997
+ }
998
+ // Handle JSON body
999
+ else if (json !== null) {
1000
+ body = JSON.stringify(json);
1001
+ mergedHeaders = mergedHeaders || {};
1002
+ if (!mergedHeaders["Content-Type"]) {
1003
+ mergedHeaders["Content-Type"] = "application/json";
1004
+ }
1005
+ }
1006
+ // Handle form data
1007
+ else if (data !== null && typeof data === "object") {
1008
+ body = new URLSearchParams(data).toString();
1009
+ mergedHeaders = mergedHeaders || {};
1010
+ if (!mergedHeaders["Content-Type"]) {
1011
+ mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1012
+ }
1013
+ }
1014
+ // Handle Buffer body
1015
+ else if (Buffer.isBuffer(body)) {
1016
+ body = body.toString("utf8");
1017
+ }
1018
+
1019
+ // Use request auth if provided, otherwise fall back to session auth
1020
+ const effectiveAuth = auth !== null ? auth : this.auth;
1021
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
1022
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
1023
+
1024
+ const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
1025
+
1026
+ // Register async request with callback manager
1027
+ const manager = getAsyncManager();
1028
+ const { callbackId, promise } = manager.registerRequest(this._lib);
1029
+
1030
+ // Start async request
1031
+ this._lib.httpcloak_post_async(this._handle, url, body, headersJson, callbackId);
1032
+
1033
+ return promise;
696
1034
  }
697
1035
 
698
1036
  /**
699
- * Perform an async custom HTTP request
1037
+ * Perform an async custom HTTP request using native Go goroutines
700
1038
  * @param {string} method - HTTP method
701
1039
  * @param {string} url - Request URL
702
1040
  * @param {Object} [options] - Request options
1041
+ * @param {Object} [options.cookies] - Cookies to send with this request
703
1042
  * @returns {Promise<Response>} Response object
704
1043
  */
705
1044
  request(method, url, options = {}) {
706
- return new Promise((resolve, reject) => {
707
- setImmediate(() => {
708
- try {
709
- resolve(this.requestSync(method, url, options));
710
- } catch (err) {
711
- reject(err);
712
- }
713
- });
714
- });
1045
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null } = options;
1046
+
1047
+ url = addParamsToUrl(url, params);
1048
+ let mergedHeaders = this._mergeHeaders(headers);
1049
+
1050
+ // Handle multipart file upload
1051
+ if (files !== null) {
1052
+ const formData = (data !== null && typeof data === "object") ? data : null;
1053
+ const multipart = encodeMultipart(formData, files);
1054
+ body = multipart.body.toString("latin1");
1055
+ mergedHeaders = mergedHeaders || {};
1056
+ mergedHeaders["Content-Type"] = multipart.contentType;
1057
+ }
1058
+ // Handle JSON body
1059
+ else if (json !== null) {
1060
+ body = JSON.stringify(json);
1061
+ mergedHeaders = mergedHeaders || {};
1062
+ if (!mergedHeaders["Content-Type"]) {
1063
+ mergedHeaders["Content-Type"] = "application/json";
1064
+ }
1065
+ }
1066
+ // Handle form data
1067
+ else if (data !== null && typeof data === "object") {
1068
+ body = new URLSearchParams(data).toString();
1069
+ mergedHeaders = mergedHeaders || {};
1070
+ if (!mergedHeaders["Content-Type"]) {
1071
+ mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1072
+ }
1073
+ }
1074
+ // Handle Buffer body
1075
+ else if (Buffer.isBuffer(body)) {
1076
+ body = body.toString("utf8");
1077
+ }
1078
+
1079
+ // Use request auth if provided, otherwise fall back to session auth
1080
+ const effectiveAuth = auth !== null ? auth : this.auth;
1081
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
1082
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
1083
+
1084
+ const requestConfig = {
1085
+ method: method.toUpperCase(),
1086
+ url,
1087
+ };
1088
+ if (mergedHeaders) requestConfig.headers = mergedHeaders;
1089
+ if (body) requestConfig.body = body;
1090
+ if (timeout) requestConfig.timeout = timeout;
1091
+
1092
+ // Register async request with callback manager
1093
+ const manager = getAsyncManager();
1094
+ const { callbackId, promise } = manager.registerRequest(this._lib);
1095
+
1096
+ // Start async request
1097
+ this._lib.httpcloak_request_async(this._handle, JSON.stringify(requestConfig), callbackId);
1098
+
1099
+ return promise;
715
1100
  }
716
1101
 
717
1102
  /**
@@ -766,6 +1151,16 @@ class Session {
766
1151
  return {};
767
1152
  }
768
1153
 
1154
+ /**
1155
+ * Get a specific cookie by name
1156
+ * @param {string} name - Cookie name
1157
+ * @returns {string|null} Cookie value or null if not found
1158
+ */
1159
+ getCookie(name) {
1160
+ const cookies = this.getCookies();
1161
+ return cookies[name] || null;
1162
+ }
1163
+
769
1164
  /**
770
1165
  * Set a cookie in the session
771
1166
  * @param {string} name - Cookie name
@@ -775,6 +1170,25 @@ class Session {
775
1170
  this._lib.httpcloak_set_cookie(this._handle, name, value);
776
1171
  }
777
1172
 
1173
+ /**
1174
+ * Delete a specific cookie by name
1175
+ * @param {string} name - Cookie name to delete
1176
+ */
1177
+ deleteCookie(name) {
1178
+ // Set cookie to empty value - effectively deletes it
1179
+ this._lib.httpcloak_set_cookie(this._handle, name, "");
1180
+ }
1181
+
1182
+ /**
1183
+ * Clear all cookies from the session
1184
+ */
1185
+ clearCookies() {
1186
+ const cookies = this.getCookies();
1187
+ for (const name of Object.keys(cookies)) {
1188
+ this.deleteCookie(name);
1189
+ }
1190
+ }
1191
+
778
1192
  /**
779
1193
  * Get cookies as a property
780
1194
  */
@@ -960,6 +1374,8 @@ module.exports = {
960
1374
  // Classes
961
1375
  Session,
962
1376
  Response,
1377
+ Cookie,
1378
+ RedirectInfo,
963
1379
  HTTPCloakError,
964
1380
  // Presets
965
1381
  Preset,
package/lib/index.mjs ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * HTTPCloak Node.js Client - ESM Module
3
+ *
4
+ * A fetch/axios-compatible HTTP client with browser fingerprint emulation.
5
+ * Provides TLS fingerprinting for HTTP requests.
6
+ */
7
+
8
+ import { createRequire } from "module";
9
+ const require = createRequire(import.meta.url);
10
+
11
+ // Import the CommonJS module
12
+ const cjs = require("./index.js");
13
+
14
+ // Re-export all named exports
15
+ export const Session = cjs.Session;
16
+ export const Response = cjs.Response;
17
+ export const HTTPCloakError = cjs.HTTPCloakError;
18
+ export const Preset = cjs.Preset;
19
+ export const configure = cjs.configure;
20
+ export const get = cjs.get;
21
+ export const post = cjs.post;
22
+ export const put = cjs.put;
23
+ export const patch = cjs.patch;
24
+ export const head = cjs.head;
25
+ export const options = cjs.options;
26
+ export const request = cjs.request;
27
+ export const version = cjs.version;
28
+ export const availablePresets = cjs.availablePresets;
29
+
30
+ // 'delete' is a reserved word in ESM, so we export it specially
31
+ const del = cjs.delete;
32
+ export { del as delete };
33
+
34
+ // Default export (the entire module)
35
+ export default cjs;
@@ -0,0 +1,6 @@
1
+ // Auto-generated - exports path to native library (ESM)
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join } from "path";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ export default join(__dirname, "libhttpcloak-darwin-arm64.dylib");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/darwin-arm64",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "HTTPCloak native binary for darwin arm64",
5
5
  "os": [
6
6
  "darwin"
@@ -9,6 +9,13 @@
9
9
  "arm64"
10
10
  ],
11
11
  "main": "lib.js",
12
+ "module": "lib.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib.mjs",
16
+ "require": "./lib.js"
17
+ }
18
+ },
12
19
  "license": "MIT",
13
20
  "repository": {
14
21
  "type": "git",
@@ -0,0 +1,6 @@
1
+ // Auto-generated - exports path to native library (ESM)
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join } from "path";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ export default join(__dirname, "libhttpcloak-darwin-amd64.dylib");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/darwin-x64",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "HTTPCloak native binary for darwin x64",
5
5
  "os": [
6
6
  "darwin"
@@ -9,6 +9,13 @@
9
9
  "x64"
10
10
  ],
11
11
  "main": "lib.js",
12
+ "module": "lib.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib.mjs",
16
+ "require": "./lib.js"
17
+ }
18
+ },
12
19
  "license": "MIT",
13
20
  "repository": {
14
21
  "type": "git",
@@ -0,0 +1,6 @@
1
+ // Auto-generated - exports path to native library (ESM)
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join } from "path";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ export default join(__dirname, "libhttpcloak-linux-arm64.so");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/linux-arm64",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "HTTPCloak native binary for linux arm64",
5
5
  "os": [
6
6
  "linux"
@@ -9,6 +9,13 @@
9
9
  "arm64"
10
10
  ],
11
11
  "main": "lib.js",
12
+ "module": "lib.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib.mjs",
16
+ "require": "./lib.js"
17
+ }
18
+ },
12
19
  "license": "MIT",
13
20
  "repository": {
14
21
  "type": "git",
@@ -0,0 +1,6 @@
1
+ // Auto-generated - exports path to native library (ESM)
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join } from "path";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ export default join(__dirname, "libhttpcloak-linux-amd64.so");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/linux-x64",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "HTTPCloak native binary for linux x64",
5
5
  "os": [
6
6
  "linux"
@@ -9,6 +9,13 @@
9
9
  "x64"
10
10
  ],
11
11
  "main": "lib.js",
12
+ "module": "lib.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib.mjs",
16
+ "require": "./lib.js"
17
+ }
18
+ },
12
19
  "license": "MIT",
13
20
  "repository": {
14
21
  "type": "git",
@@ -0,0 +1,6 @@
1
+ // Auto-generated - exports path to native library (ESM)
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join } from "path";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ export default join(__dirname, "libhttpcloak-windows-arm64.dll");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/win32-arm64",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "HTTPCloak native binary for win32 arm64",
5
5
  "os": [
6
6
  "win32"
@@ -9,6 +9,13 @@
9
9
  "arm64"
10
10
  ],
11
11
  "main": "lib.js",
12
+ "module": "lib.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib.mjs",
16
+ "require": "./lib.js"
17
+ }
18
+ },
12
19
  "license": "MIT",
13
20
  "repository": {
14
21
  "type": "git",
@@ -0,0 +1,6 @@
1
+ // Auto-generated - exports path to native library (ESM)
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join } from "path";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ export default join(__dirname, "libhttpcloak-windows-amd64.dll");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/win32-x64",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "HTTPCloak native binary for win32 x64",
5
5
  "os": [
6
6
  "win32"
@@ -9,6 +9,13 @@
9
9
  "x64"
10
10
  ],
11
11
  "main": "lib.js",
12
+ "module": "lib.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib.mjs",
16
+ "require": "./lib.js"
17
+ }
18
+ },
12
19
  "license": "MIT",
13
20
  "repository": {
14
21
  "type": "git",
package/package.json CHANGED
@@ -1,11 +1,25 @@
1
1
  {
2
2
  "name": "httpcloak",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
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
+ "module": "lib/index.mjs",
6
7
  "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./lib/index.d.ts",
12
+ "default": "./lib/index.mjs"
13
+ },
14
+ "require": {
15
+ "types": "./lib/index.d.ts",
16
+ "default": "./lib/index.js"
17
+ }
18
+ }
19
+ },
7
20
  "scripts": {
8
21
  "test": "node test.js",
22
+ "test:esm": "node test.mjs",
9
23
  "setup-packages": "node scripts/setup-npm-packages.js"
10
24
  },
11
25
  "keywords": [
@@ -35,11 +49,11 @@
35
49
  "koffi": "^2.9.0"
36
50
  },
37
51
  "optionalDependencies": {
38
- "@httpcloak/linux-x64": "1.5.0",
39
- "@httpcloak/linux-arm64": "1.5.0",
40
- "@httpcloak/darwin-x64": "1.5.0",
41
- "@httpcloak/darwin-arm64": "1.5.0",
42
- "@httpcloak/win32-x64": "1.5.0",
43
- "@httpcloak/win32-arm64": "1.5.0"
52
+ "@httpcloak/linux-x64": "1.5.1",
53
+ "@httpcloak/linux-arm64": "1.5.1",
54
+ "@httpcloak/darwin-x64": "1.5.1",
55
+ "@httpcloak/darwin-arm64": "1.5.1",
56
+ "@httpcloak/win32-x64": "1.5.1",
57
+ "@httpcloak/win32-arm64": "1.5.1"
44
58
  }
45
59
  }