httpcloak 1.6.1-beta.3 → 1.6.5

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
@@ -31,7 +31,13 @@ class HTTPCloakError extends Error {
31
31
  * const session = new httpcloak.Session({ preset: httpcloak.Preset.FIREFOX_133 });
32
32
  */
33
33
  const Preset = {
34
- // Chrome 145 (latest)
34
+ // Chrome 146 (latest)
35
+ CHROME_146: "chrome-146",
36
+ CHROME_146_WINDOWS: "chrome-146-windows",
37
+ CHROME_146_LINUX: "chrome-146-linux",
38
+ CHROME_146_MACOS: "chrome-146-macos",
39
+
40
+ // Chrome 145
35
41
  CHROME_145: "chrome-145",
36
42
  CHROME_145_WINDOWS: "chrome-145-windows",
37
43
  CHROME_145_LINUX: "chrome-145-linux",
@@ -59,9 +65,11 @@ const Preset = {
59
65
  CHROME_143_IOS: "chrome-143-ios",
60
66
  CHROME_144_IOS: "chrome-144-ios",
61
67
  CHROME_145_IOS: "chrome-145-ios",
68
+ CHROME_146_IOS: "chrome-146-ios",
62
69
  CHROME_143_ANDROID: "chrome-143-android",
63
70
  CHROME_144_ANDROID: "chrome-144-android",
64
71
  CHROME_145_ANDROID: "chrome-145-android",
72
+ CHROME_146_ANDROID: "chrome-146-android",
65
73
 
66
74
  // Firefox
67
75
  FIREFOX_133: "firefox-133",
@@ -75,9 +83,11 @@ const Preset = {
75
83
  IOS_CHROME_143: "chrome-143-ios",
76
84
  IOS_CHROME_144: "chrome-144-ios",
77
85
  IOS_CHROME_145: "chrome-145-ios",
86
+ IOS_CHROME_146: "chrome-146-ios",
78
87
  ANDROID_CHROME_143: "chrome-143-android",
79
88
  ANDROID_CHROME_144: "chrome-144-android",
80
89
  ANDROID_CHROME_145: "chrome-145-android",
90
+ ANDROID_CHROME_146: "chrome-146-android",
81
91
  IOS_SAFARI_17: "safari-17-ios",
82
92
  IOS_SAFARI_18: "safari-18-ios",
83
93
 
@@ -87,12 +97,13 @@ const Preset = {
87
97
  */
88
98
  all() {
89
99
  return [
100
+ this.CHROME_146, this.CHROME_146_WINDOWS, this.CHROME_146_LINUX, this.CHROME_146_MACOS,
90
101
  this.CHROME_145, this.CHROME_145_WINDOWS, this.CHROME_145_LINUX, this.CHROME_145_MACOS,
91
102
  this.CHROME_144, this.CHROME_144_WINDOWS, this.CHROME_144_LINUX, this.CHROME_144_MACOS,
92
103
  this.CHROME_143, this.CHROME_143_WINDOWS, this.CHROME_143_LINUX, this.CHROME_143_MACOS,
93
104
  this.CHROME_141, this.CHROME_133,
94
- this.CHROME_145_IOS, this.CHROME_144_IOS, this.CHROME_143_IOS,
95
- this.CHROME_145_ANDROID, this.CHROME_144_ANDROID, this.CHROME_143_ANDROID,
105
+ this.CHROME_146_IOS, this.CHROME_145_IOS, this.CHROME_144_IOS, this.CHROME_143_IOS,
106
+ this.CHROME_146_ANDROID, this.CHROME_145_ANDROID, this.CHROME_144_ANDROID, this.CHROME_143_ANDROID,
96
107
  this.FIREFOX_133,
97
108
  this.SAFARI_18, this.SAFARI_17_IOS, this.SAFARI_18_IOS,
98
109
  ];
@@ -205,11 +216,28 @@ class Response {
205
216
  * @param {Object} data - Response data from native library
206
217
  * @param {number} [elapsed=0] - Elapsed time in milliseconds
207
218
  */
208
- constructor(data, elapsed = 0) {
219
+ constructor(data, elapsed = 0, rawBody = null) {
209
220
  this.statusCode = data.status_code || 0;
210
221
  this.headers = data.headers || {};
211
- this._body = Buffer.from(data.body || "", "utf8");
212
- this._text = data.body || "";
222
+ if (rawBody !== null) {
223
+ // Raw-path: body arrived as binary bytes alongside metadata JSON
224
+ // (httpcloak_*_raw + response_finalize). No base64 round trip — fastest
225
+ // and loss-free for PDFs/images/etc.
226
+ this._body = rawBody;
227
+ this._text = rawBody.toString("utf8"); // best-effort text view
228
+ } else {
229
+ // JSON path: body is a string inside the JSON blob. Non-UTF-8 bodies
230
+ // arrive base64-encoded (Go side since 98e4228). Valid UTF-8 passes
231
+ // through as plain text.
232
+ const bodyStr = data.body || "";
233
+ if (data.body_encoding === "base64") {
234
+ this._body = Buffer.from(bodyStr, "base64");
235
+ this._text = this._body.toString("utf8");
236
+ } else {
237
+ this._body = Buffer.from(bodyStr, "utf8");
238
+ this._text = bodyStr;
239
+ }
240
+ }
213
241
  this.finalUrl = data.final_url || "";
214
242
  this.protocol = data.protocol || "";
215
243
  this.elapsed = elapsed; // milliseconds
@@ -776,25 +804,37 @@ function getLib() {
776
804
  const libPath = getLibPath();
777
805
  nativeLibHandle = koffi.load(libPath);
778
806
 
779
- // Use str for string returns - koffi handles the string copy automatically
780
- // Note: The C strings allocated by Go are not freed, but Go's GC handles them
807
+ // Declare the free function first so we can hand it to koffi.disposable.
808
+ // httpcloak_free_string is NULL-safe on the Go side (no-op on nil).
809
+ const httpcloakFreeString = nativeLibHandle.func("httpcloak_free_string", "void", ["void*"]);
810
+
811
+ // Named disposable type derived from "str": koffi decodes the C string
812
+ // into a JS string AND then calls httpcloakFreeString on the original
813
+ // malloc'd pointer. Every function below that returned plain "str"
814
+ // previously leaked that malloc (issue #48: customers reported ~48MB/h
815
+ // RSS growth in sustained production traffic). Using HeapStr closes the
816
+ // leak without any call-site changes.
817
+ const HeapStr = koffi.disposable("HeapStr", "str", httpcloakFreeString);
818
+
781
819
  lib = {
782
820
  httpcloak_session_new: nativeLibHandle.func("httpcloak_session_new", "int64", ["str"]),
783
821
  httpcloak_session_free: nativeLibHandle.func("httpcloak_session_free", "void", ["int64"]),
784
822
  httpcloak_session_refresh: nativeLibHandle.func("httpcloak_session_refresh", "void", ["int64"]),
785
- httpcloak_session_refresh_protocol: nativeLibHandle.func("httpcloak_session_refresh_protocol", "str", ["int64", "str"]),
786
- httpcloak_session_warmup: nativeLibHandle.func("httpcloak_session_warmup", "str", ["int64", "str", "int64"]),
823
+ httpcloak_session_refresh_protocol: nativeLibHandle.func("httpcloak_session_refresh_protocol", HeapStr, ["int64", "str"]),
824
+ httpcloak_session_warmup: nativeLibHandle.func("httpcloak_session_warmup", HeapStr, ["int64", "str", "int64"]),
787
825
  httpcloak_session_fork: nativeLibHandle.func("httpcloak_session_fork", "int64", ["int64"]),
788
- httpcloak_get: nativeLibHandle.func("httpcloak_get", "str", ["int64", "str", "str"]),
789
- httpcloak_post: nativeLibHandle.func("httpcloak_post", "str", ["int64", "str", "str", "str"]),
790
- httpcloak_request: nativeLibHandle.func("httpcloak_request", "str", ["int64", "str"]),
791
- httpcloak_get_cookies: nativeLibHandle.func("httpcloak_get_cookies", "str", ["int64"]),
792
- httpcloak_set_cookie: nativeLibHandle.func("httpcloak_set_cookie", "void", ["int64", "str", "str"]),
793
- httpcloak_free_string: nativeLibHandle.func("httpcloak_free_string", "void", ["void*"]),
794
- httpcloak_version: nativeLibHandle.func("httpcloak_version", "str", []),
795
- httpcloak_available_presets: nativeLibHandle.func("httpcloak_available_presets", "str", []),
796
- httpcloak_set_ech_dns_servers: nativeLibHandle.func("httpcloak_set_ech_dns_servers", "str", ["str"]),
797
- httpcloak_get_ech_dns_servers: nativeLibHandle.func("httpcloak_get_ech_dns_servers", "str", []),
826
+ httpcloak_get: nativeLibHandle.func("httpcloak_get", HeapStr, ["int64", "str", "str"]),
827
+ httpcloak_post: nativeLibHandle.func("httpcloak_post", HeapStr, ["int64", "str", "str", "str"]),
828
+ httpcloak_request: nativeLibHandle.func("httpcloak_request", HeapStr, ["int64", "str"]),
829
+ httpcloak_get_cookies: nativeLibHandle.func("httpcloak_get_cookies", HeapStr, ["int64"]),
830
+ httpcloak_set_cookie: nativeLibHandle.func("httpcloak_set_cookie", "void", ["int64", "str"]),
831
+ httpcloak_delete_cookie: nativeLibHandle.func("httpcloak_delete_cookie", "void", ["int64", "str", "str"]),
832
+ httpcloak_clear_cookies: nativeLibHandle.func("httpcloak_clear_cookies", "void", ["int64"]),
833
+ httpcloak_free_string: httpcloakFreeString,
834
+ httpcloak_version: nativeLibHandle.func("httpcloak_version", HeapStr, []),
835
+ httpcloak_available_presets: nativeLibHandle.func("httpcloak_available_presets", HeapStr, []),
836
+ httpcloak_set_ech_dns_servers: nativeLibHandle.func("httpcloak_set_ech_dns_servers", HeapStr, ["str"]),
837
+ httpcloak_get_ech_dns_servers: nativeLibHandle.func("httpcloak_get_ech_dns_servers", HeapStr, []),
798
838
  // Async functions
799
839
  httpcloak_register_callback: nativeLibHandle.func("httpcloak_register_callback", "int64", [koffi.pointer(AsyncCallbackProto)]),
800
840
  httpcloak_unregister_callback: nativeLibHandle.func("httpcloak_unregister_callback", "void", ["int64"]),
@@ -805,41 +845,41 @@ function getLib() {
805
845
  httpcloak_stream_get: nativeLibHandle.func("httpcloak_stream_get", "int64", ["int64", "str", "str"]),
806
846
  httpcloak_stream_post: nativeLibHandle.func("httpcloak_stream_post", "int64", ["int64", "str", "str", "str"]),
807
847
  httpcloak_stream_request: nativeLibHandle.func("httpcloak_stream_request", "int64", ["int64", "str"]),
808
- httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata", "str", ["int64"]),
809
- httpcloak_stream_read: nativeLibHandle.func("httpcloak_stream_read", "str", ["int64", "int64"]),
848
+ httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata", HeapStr, ["int64"]),
849
+ httpcloak_stream_read: nativeLibHandle.func("httpcloak_stream_read", HeapStr, ["int64", "int64"]),
810
850
  httpcloak_stream_close: nativeLibHandle.func("httpcloak_stream_close", "void", ["int64"]),
811
851
  // Raw response functions for fast-path (zero-copy)
812
852
  httpcloak_get_raw: nativeLibHandle.func("httpcloak_get_raw", "int64", ["int64", "str", "str"]),
813
853
  httpcloak_post_raw: nativeLibHandle.func("httpcloak_post_raw", "int64", ["int64", "str", "void*", "int", "str"]),
814
854
  httpcloak_request_raw: nativeLibHandle.func("httpcloak_request_raw", "int64", ["int64", "str", "void*", "int"]),
815
- httpcloak_response_get_metadata: nativeLibHandle.func("httpcloak_response_get_metadata", "str", ["int64"]),
855
+ httpcloak_response_get_metadata: nativeLibHandle.func("httpcloak_response_get_metadata", HeapStr, ["int64"]),
816
856
  httpcloak_response_get_body_len: nativeLibHandle.func("httpcloak_response_get_body_len", "int", ["int64"]),
817
857
  httpcloak_response_copy_body_to: nativeLibHandle.func("httpcloak_response_copy_body_to", "int", ["int64", "void*", "int"]),
818
858
  httpcloak_response_free: nativeLibHandle.func("httpcloak_response_free", "void", ["int64"]),
819
859
  // Combined finalize function (copy + metadata + free in one call)
820
- httpcloak_response_finalize: nativeLibHandle.func("httpcloak_response_finalize", "str", ["int64", "void*", "int"]),
860
+ httpcloak_response_finalize: nativeLibHandle.func("httpcloak_response_finalize", HeapStr, ["int64", "void*", "int"]),
821
861
  // Session persistence functions
822
- httpcloak_session_save: nativeLibHandle.func("httpcloak_session_save", "str", ["int64", "str"]),
862
+ httpcloak_session_save: nativeLibHandle.func("httpcloak_session_save", HeapStr, ["int64", "str"]),
823
863
  httpcloak_session_load: nativeLibHandle.func("httpcloak_session_load", "int64", ["str"]),
824
- httpcloak_session_marshal: nativeLibHandle.func("httpcloak_session_marshal", "str", ["int64"]),
864
+ httpcloak_session_marshal: nativeLibHandle.func("httpcloak_session_marshal", HeapStr, ["int64"]),
825
865
  httpcloak_session_unmarshal: nativeLibHandle.func("httpcloak_session_unmarshal", "int64", ["str"]),
826
866
  // Proxy management functions
827
- httpcloak_session_set_proxy: nativeLibHandle.func("httpcloak_session_set_proxy", "str", ["int64", "str"]),
828
- httpcloak_session_set_tcp_proxy: nativeLibHandle.func("httpcloak_session_set_tcp_proxy", "str", ["int64", "str"]),
829
- httpcloak_session_set_udp_proxy: nativeLibHandle.func("httpcloak_session_set_udp_proxy", "str", ["int64", "str"]),
830
- httpcloak_session_get_proxy: nativeLibHandle.func("httpcloak_session_get_proxy", "str", ["int64"]),
831
- httpcloak_session_get_tcp_proxy: nativeLibHandle.func("httpcloak_session_get_tcp_proxy", "str", ["int64"]),
832
- httpcloak_session_get_udp_proxy: nativeLibHandle.func("httpcloak_session_get_udp_proxy", "str", ["int64"]),
867
+ httpcloak_session_set_proxy: nativeLibHandle.func("httpcloak_session_set_proxy", HeapStr, ["int64", "str"]),
868
+ httpcloak_session_set_tcp_proxy: nativeLibHandle.func("httpcloak_session_set_tcp_proxy", HeapStr, ["int64", "str"]),
869
+ httpcloak_session_set_udp_proxy: nativeLibHandle.func("httpcloak_session_set_udp_proxy", HeapStr, ["int64", "str"]),
870
+ httpcloak_session_get_proxy: nativeLibHandle.func("httpcloak_session_get_proxy", HeapStr, ["int64"]),
871
+ httpcloak_session_get_tcp_proxy: nativeLibHandle.func("httpcloak_session_get_tcp_proxy", HeapStr, ["int64"]),
872
+ httpcloak_session_get_udp_proxy: nativeLibHandle.func("httpcloak_session_get_udp_proxy", HeapStr, ["int64"]),
833
873
  // Header order customization
834
- httpcloak_session_set_header_order: nativeLibHandle.func("httpcloak_session_set_header_order", "str", ["int64", "str"]),
835
- httpcloak_session_get_header_order: nativeLibHandle.func("httpcloak_session_get_header_order", "str", ["int64"]),
874
+ httpcloak_session_set_header_order: nativeLibHandle.func("httpcloak_session_set_header_order", HeapStr, ["int64", "str"]),
875
+ httpcloak_session_get_header_order: nativeLibHandle.func("httpcloak_session_get_header_order", HeapStr, ["int64"]),
836
876
  // Local proxy functions
837
877
  httpcloak_local_proxy_start: nativeLibHandle.func("httpcloak_local_proxy_start", "int64", ["str"]),
838
878
  httpcloak_local_proxy_stop: nativeLibHandle.func("httpcloak_local_proxy_stop", "void", ["int64"]),
839
879
  httpcloak_local_proxy_get_port: nativeLibHandle.func("httpcloak_local_proxy_get_port", "int", ["int64"]),
840
880
  httpcloak_local_proxy_is_running: nativeLibHandle.func("httpcloak_local_proxy_is_running", "int", ["int64"]),
841
- httpcloak_local_proxy_get_stats: nativeLibHandle.func("httpcloak_local_proxy_get_stats", "str", ["int64"]),
842
- httpcloak_local_proxy_register_session: nativeLibHandle.func("httpcloak_local_proxy_register_session", "str", ["int64", "str", "int64"]),
881
+ httpcloak_local_proxy_get_stats: nativeLibHandle.func("httpcloak_local_proxy_get_stats", HeapStr, ["int64"]),
882
+ httpcloak_local_proxy_register_session: nativeLibHandle.func("httpcloak_local_proxy_register_session", HeapStr, ["int64", "str", "int64"]),
843
883
  httpcloak_local_proxy_unregister_session: nativeLibHandle.func("httpcloak_local_proxy_unregister_session", "int", ["int64", "str"]),
844
884
  // Session cache callbacks
845
885
  httpcloak_set_session_cache_callbacks: nativeLibHandle.func("httpcloak_set_session_cache_callbacks", "void", [
@@ -863,6 +903,23 @@ function getLib() {
863
903
  // Async cache result functions (called by JS to provide results to Go)
864
904
  httpcloak_async_cache_get_result: nativeLibHandle.func("httpcloak_async_cache_get_result", "void", ["int64", "str"]),
865
905
  httpcloak_async_cache_op_result: nativeLibHandle.func("httpcloak_async_cache_op_result", "void", ["int64", "int"]),
906
+ // Custom preset loading (void* returns for manual free)
907
+ httpcloak_preset_load_file: nativeLibHandle.func("httpcloak_preset_load_file", "void*", ["str"]),
908
+ httpcloak_preset_load_json: nativeLibHandle.func("httpcloak_preset_load_json", "void*", ["str"]),
909
+ httpcloak_preset_unregister: nativeLibHandle.func("httpcloak_preset_unregister", "void", ["str"]),
910
+ // Describe uses HeapStr (issue #48 leak-safe disposable) — koffi
911
+ // auto-frees the malloc'd C string after decode.
912
+ httpcloak_describe_preset: nativeLibHandle.func("httpcloak_describe_preset", HeapStr, ["str"]),
913
+ // Preset pool functions (void* returns for manual free)
914
+ httpcloak_pool_load_file: nativeLibHandle.func("httpcloak_pool_load_file", "void*", ["str"]),
915
+ httpcloak_pool_load_json: nativeLibHandle.func("httpcloak_pool_load_json", "void*", ["str"]),
916
+ httpcloak_pool_pick: nativeLibHandle.func("httpcloak_pool_pick", "void*", ["int64"]),
917
+ httpcloak_pool_random: nativeLibHandle.func("httpcloak_pool_random", "void*", ["int64"]),
918
+ httpcloak_pool_next: nativeLibHandle.func("httpcloak_pool_next", "void*", ["int64"]),
919
+ httpcloak_pool_get: nativeLibHandle.func("httpcloak_pool_get", "void*", ["int64", "int64"]),
920
+ httpcloak_pool_size: nativeLibHandle.func("httpcloak_pool_size", "int64", ["int64"]),
921
+ httpcloak_pool_name: nativeLibHandle.func("httpcloak_pool_name", "void*", ["int64"]),
922
+ httpcloak_pool_free: nativeLibHandle.func("httpcloak_pool_free", "void", ["int64"]),
866
923
  };
867
924
  }
868
925
  return lib;
@@ -1016,6 +1073,38 @@ function parseResponse(resultPtr, elapsed = 0) {
1016
1073
  return new Response(data, elapsed);
1017
1074
  }
1018
1075
 
1076
+ /**
1077
+ * Parse a raw response handle returned by httpcloak_*_raw into a Response
1078
+ * with a binary Buffer body. Avoids the base64 round trip used by the JSON
1079
+ * path — binary bodies (PDFs, images, streams) land in memory once.
1080
+ * @param {Object} lib - Native library binding
1081
+ * @param {number|bigint} responseHandle
1082
+ * @param {number} [elapsed=0] - Elapsed milliseconds
1083
+ * @returns {Response}
1084
+ */
1085
+ function parseRawResponse(lib, responseHandle, elapsed = 0) {
1086
+ const bodyLen = lib.httpcloak_response_get_body_len(responseHandle);
1087
+ if (bodyLen < 0) {
1088
+ lib.httpcloak_response_free(responseHandle);
1089
+ throw new HTTPCloakError("Failed to get response body length");
1090
+ }
1091
+
1092
+ const buffer = Buffer.allocUnsafe(bodyLen);
1093
+ // finalize() copies body into buffer, returns metadata JSON (no body), and
1094
+ // frees the handle — a single FFI call instead of three.
1095
+ const metadataStr = lib.httpcloak_response_finalize(responseHandle, buffer, bodyLen);
1096
+ if (!metadataStr) {
1097
+ throw new HTTPCloakError("Failed to finalize response");
1098
+ }
1099
+
1100
+ const metadata = JSON.parse(metadataStr);
1101
+ if (metadata.error) {
1102
+ throw new HTTPCloakError(metadata.error);
1103
+ }
1104
+
1105
+ return new Response(metadata, elapsed, buffer);
1106
+ }
1107
+
1019
1108
  /**
1020
1109
  * Add query parameters to URL
1021
1110
  */
@@ -1162,7 +1251,7 @@ function version() {
1162
1251
  /**
1163
1252
  * Get available browser presets with their supported protocols.
1164
1253
  * Returns an object mapping preset names to their info:
1165
- * { "chrome-145": { protocols: ["h1", "h2", "h3"] }, ... }
1254
+ * { "chrome-146": { protocols: ["h1", "h2", "h3"] }, ... }
1166
1255
  */
1167
1256
  function availablePresets() {
1168
1257
  const nativeLib = getLib();
@@ -1228,7 +1317,7 @@ class Session {
1228
1317
  /**
1229
1318
  * Create a new session
1230
1319
  * @param {Object} options - Session options
1231
- * @param {string} [options.preset="chrome-145"] - Browser preset to use
1320
+ * @param {string} [options.preset="chrome-146"] - Browser preset to use
1232
1321
  * @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port")
1233
1322
  * @param {string} [options.tcpProxy] - Proxy URL for TCP protocols (HTTP/1.1, HTTP/2) - use with udpProxy for split config
1234
1323
  * @param {string} [options.udpProxy] - Proxy URL for UDP protocols (HTTP/3 via MASQUE) - use with tcpProxy for split config
@@ -1237,8 +1326,8 @@ class Session {
1237
1326
  * @param {boolean} [options.verify=true] - SSL certificate verification
1238
1327
  * @param {boolean} [options.allowRedirects=true] - Follow redirects
1239
1328
  * @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
1240
- * @param {number} [options.retry=3] - Number of retries on failure (set to 0 to disable)
1241
- * @param {number[]} [options.retryOnStatus] - Status codes to retry on
1329
+ * @param {number} [options.retry=0] - Number of retries on failure (default 0 = no retries; set positive to enable. Issue #57: prior default 3 silently retried POST/PUT/PATCH on 5xx, violating idempotency expectations)
1330
+ * @param {number[]} [options.retryOnStatus] - Status codes to retry on (only consulted when retry > 0)
1242
1331
  * @param {number} [options.retryWaitMin=500] - Minimum wait time between retries in milliseconds
1243
1332
  * @param {number} [options.retryWaitMax=10000] - Maximum wait time between retries in milliseconds
1244
1333
  * @param {Array} [options.auth] - Default auth [username, password] for all requests
@@ -1249,7 +1338,7 @@ class Session {
1249
1338
  */
1250
1339
  constructor(options = {}) {
1251
1340
  const {
1252
- preset = "chrome-145",
1341
+ preset = "chrome-146",
1253
1342
  proxy = null,
1254
1343
  tcpProxy = null,
1255
1344
  udpProxy = null,
@@ -1258,7 +1347,7 @@ class Session {
1258
1347
  verify = true,
1259
1348
  allowRedirects = true,
1260
1349
  maxRedirects = 10,
1261
- retry = 3,
1350
+ retry = 0,
1262
1351
  retryOnStatus = null,
1263
1352
  retryWaitMin = 500,
1264
1353
  retryWaitMax = 10000,
@@ -1279,6 +1368,7 @@ class Session {
1279
1368
  tcpMss = null,
1280
1369
  tcpWindowSize = null,
1281
1370
  tcpWindowScale = null,
1371
+ tcpDf = null,
1282
1372
  } = options;
1283
1373
 
1284
1374
  this._lib = getLib();
@@ -1366,6 +1456,9 @@ class Session {
1366
1456
  if (tcpWindowScale != null) {
1367
1457
  config.tcp_window_scale = tcpWindowScale;
1368
1458
  }
1459
+ if (tcpDf != null) {
1460
+ config.tcp_df = tcpDf;
1461
+ }
1369
1462
 
1370
1463
  this._handle = this._lib.httpcloak_session_new(JSON.stringify(config));
1371
1464
 
@@ -1505,7 +1598,7 @@ class Session {
1505
1598
  * @returns {Response} Response object
1506
1599
  */
1507
1600
  getSync(url, options = {}) {
1508
- const { headers = null, params = null, cookies = null, auth = null } = options;
1601
+ const { headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
1509
1602
 
1510
1603
  url = addParamsToUrl(url, params);
1511
1604
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1519,12 +1612,18 @@ class Session {
1519
1612
  if (mergedHeaders) {
1520
1613
  reqOptions.headers = mergedHeaders;
1521
1614
  }
1615
+ if (fetchMode) {
1616
+ reqOptions.fetch_mode = fetchMode;
1617
+ }
1522
1618
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1523
1619
 
1524
1620
  const startTime = Date.now();
1525
- const result = this._lib.httpcloak_get(this._handle, url, optionsJson);
1621
+ const responseHandle = this._lib.httpcloak_get_raw(this._handle, url, optionsJson);
1526
1622
  const elapsed = Date.now() - startTime;
1527
- return parseResponse(result, elapsed);
1623
+ if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
1624
+ throw new HTTPCloakError("Request failed");
1625
+ }
1626
+ return parseRawResponse(this._lib, responseHandle, elapsed);
1528
1627
  }
1529
1628
 
1530
1629
  /**
@@ -1545,38 +1644,36 @@ class Session {
1545
1644
  * @returns {Response} Response object
1546
1645
  */
1547
1646
  postSync(url, options = {}) {
1548
- let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
1647
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
1549
1648
 
1550
1649
  url = addParamsToUrl(url, params);
1551
1650
  let mergedHeaders = this._mergeHeaders(headers);
1552
1651
 
1553
- // Handle multipart file upload
1652
+ // Normalize body to a Buffer so the raw path can pass (ptr, len) without
1653
+ // null-terminator truncation or utf8 mangling.
1654
+ let bodyBuffer = null;
1554
1655
  if (files !== null) {
1555
1656
  const formData = (data !== null && typeof data === "object") ? data : null;
1556
1657
  const multipart = encodeMultipart(formData, files);
1557
- body = multipart.body.toString("latin1"); // Preserve binary data
1658
+ bodyBuffer = multipart.body;
1558
1659
  mergedHeaders = mergedHeaders || {};
1559
1660
  mergedHeaders["Content-Type"] = multipart.contentType;
1560
- }
1561
- // Handle JSON body
1562
- else if (json !== null) {
1563
- body = JSON.stringify(json);
1661
+ } else if (json !== null) {
1662
+ bodyBuffer = Buffer.from(JSON.stringify(json), "utf8");
1564
1663
  mergedHeaders = mergedHeaders || {};
1565
1664
  if (!mergedHeaders["Content-Type"]) {
1566
1665
  mergedHeaders["Content-Type"] = "application/json";
1567
1666
  }
1568
- }
1569
- // Handle form data
1570
- else if (data !== null && typeof data === "object") {
1571
- body = new URLSearchParams(data).toString();
1667
+ } else if (data !== null && typeof data === "object") {
1668
+ bodyBuffer = Buffer.from(new URLSearchParams(data).toString(), "utf8");
1572
1669
  mergedHeaders = mergedHeaders || {};
1573
1670
  if (!mergedHeaders["Content-Type"]) {
1574
1671
  mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1575
1672
  }
1576
- }
1577
- // Handle Buffer body
1578
- else if (Buffer.isBuffer(body)) {
1579
- body = body.toString("utf8");
1673
+ } else if (Buffer.isBuffer(body)) {
1674
+ bodyBuffer = body;
1675
+ } else if (typeof body === "string") {
1676
+ bodyBuffer = Buffer.from(body, "utf8");
1580
1677
  }
1581
1678
 
1582
1679
  // Use request auth if provided, otherwise fall back to session auth
@@ -1589,12 +1686,21 @@ class Session {
1589
1686
  if (mergedHeaders) {
1590
1687
  reqOptions.headers = mergedHeaders;
1591
1688
  }
1689
+ if (fetchMode) {
1690
+ reqOptions.fetch_mode = fetchMode;
1691
+ }
1592
1692
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1593
1693
 
1694
+ const bodyPtr = bodyBuffer || Buffer.alloc(0);
1695
+ const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
1696
+
1594
1697
  const startTime = Date.now();
1595
- const result = this._lib.httpcloak_post(this._handle, url, body, optionsJson);
1698
+ const responseHandle = this._lib.httpcloak_post_raw(this._handle, url, bodyPtr, bodyLen, optionsJson);
1596
1699
  const elapsed = Date.now() - startTime;
1597
- return parseResponse(result, elapsed);
1700
+ if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
1701
+ throw new HTTPCloakError("Request failed");
1702
+ }
1703
+ return parseRawResponse(this._lib, responseHandle, elapsed);
1598
1704
  }
1599
1705
 
1600
1706
  /**
@@ -1607,38 +1713,35 @@ class Session {
1607
1713
  * @returns {Response} Response object
1608
1714
  */
1609
1715
  requestSync(method, url, options = {}) {
1610
- let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null } = options;
1716
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
1611
1717
 
1612
1718
  url = addParamsToUrl(url, params);
1613
1719
  let mergedHeaders = this._mergeHeaders(headers);
1614
1720
 
1615
- // Handle multipart file upload
1721
+ // Normalize body to a Buffer so the raw path can pass (ptr, len).
1722
+ let bodyBuffer = null;
1616
1723
  if (files !== null) {
1617
1724
  const formData = (data !== null && typeof data === "object") ? data : null;
1618
1725
  const multipart = encodeMultipart(formData, files);
1619
- body = multipart.body.toString("latin1"); // Preserve binary data
1726
+ bodyBuffer = multipart.body;
1620
1727
  mergedHeaders = mergedHeaders || {};
1621
1728
  mergedHeaders["Content-Type"] = multipart.contentType;
1622
- }
1623
- // Handle JSON body
1624
- else if (json !== null) {
1625
- body = JSON.stringify(json);
1729
+ } else if (json !== null) {
1730
+ bodyBuffer = Buffer.from(JSON.stringify(json), "utf8");
1626
1731
  mergedHeaders = mergedHeaders || {};
1627
1732
  if (!mergedHeaders["Content-Type"]) {
1628
1733
  mergedHeaders["Content-Type"] = "application/json";
1629
1734
  }
1630
- }
1631
- // Handle form data
1632
- else if (data !== null && typeof data === "object") {
1633
- body = new URLSearchParams(data).toString();
1735
+ } else if (data !== null && typeof data === "object") {
1736
+ bodyBuffer = Buffer.from(new URLSearchParams(data).toString(), "utf8");
1634
1737
  mergedHeaders = mergedHeaders || {};
1635
1738
  if (!mergedHeaders["Content-Type"]) {
1636
1739
  mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1637
1740
  }
1638
- }
1639
- // Handle Buffer body
1640
- else if (Buffer.isBuffer(body)) {
1641
- body = body.toString("utf8");
1741
+ } else if (Buffer.isBuffer(body)) {
1742
+ bodyBuffer = body;
1743
+ } else if (typeof body === "string") {
1744
+ bodyBuffer = Buffer.from(body, "utf8");
1642
1745
  }
1643
1746
 
1644
1747
  // Use request auth if provided, otherwise fall back to session auth
@@ -1651,16 +1754,24 @@ class Session {
1651
1754
  url,
1652
1755
  };
1653
1756
  if (mergedHeaders) requestConfig.headers = mergedHeaders;
1654
- if (body) requestConfig.body = body;
1655
1757
  if (timeout) requestConfig.timeout = timeout;
1758
+ if (fetchMode) requestConfig.fetch_mode = fetchMode;
1759
+
1760
+ const bodyPtr = bodyBuffer || Buffer.alloc(0);
1761
+ const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
1656
1762
 
1657
1763
  const startTime = Date.now();
1658
- const result = this._lib.httpcloak_request(
1764
+ const responseHandle = this._lib.httpcloak_request_raw(
1659
1765
  this._handle,
1660
- JSON.stringify(requestConfig)
1766
+ JSON.stringify(requestConfig),
1767
+ bodyPtr,
1768
+ bodyLen
1661
1769
  );
1662
1770
  const elapsed = Date.now() - startTime;
1663
- return parseResponse(result, elapsed);
1771
+ if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
1772
+ throw new HTTPCloakError("Request failed");
1773
+ }
1774
+ return parseRawResponse(this._lib, responseHandle, elapsed);
1664
1775
  }
1665
1776
 
1666
1777
  // ===========================================================================
@@ -1675,7 +1786,7 @@ class Session {
1675
1786
  * @returns {Promise<Response>} Response object
1676
1787
  */
1677
1788
  get(url, options = {}) {
1678
- const { headers = null, params = null, cookies = null, auth = null } = options;
1789
+ const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
1679
1790
 
1680
1791
  url = addParamsToUrl(url, params);
1681
1792
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1689,6 +1800,15 @@ class Session {
1689
1800
  if (mergedHeaders) {
1690
1801
  reqOptions.headers = mergedHeaders;
1691
1802
  }
1803
+ if (fetchMode) {
1804
+ reqOptions.fetch_mode = fetchMode;
1805
+ }
1806
+ if (timeout) {
1807
+ // Public API: seconds (matches Session({ timeout }) and the
1808
+ // existing request()/put()/delete()/etc. methods which forward to
1809
+ // httpcloak_request_async — that path also uses time.Second).
1810
+ reqOptions.timeout = timeout;
1811
+ }
1692
1812
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1693
1813
 
1694
1814
  // Register async request with callback manager
@@ -1709,7 +1829,7 @@ class Session {
1709
1829
  * @returns {Promise<Response>} Response object
1710
1830
  */
1711
1831
  post(url, options = {}) {
1712
- let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
1832
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
1713
1833
 
1714
1834
  url = addParamsToUrl(url, params);
1715
1835
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1753,6 +1873,13 @@ class Session {
1753
1873
  if (mergedHeaders) {
1754
1874
  reqOptions.headers = mergedHeaders;
1755
1875
  }
1876
+ if (fetchMode) {
1877
+ reqOptions.fetch_mode = fetchMode;
1878
+ }
1879
+ if (timeout) {
1880
+ // Public API: seconds. clib post_async path enforces in seconds.
1881
+ reqOptions.timeout = timeout;
1882
+ }
1756
1883
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1757
1884
 
1758
1885
  // Register async request with callback manager
@@ -1774,7 +1901,7 @@ class Session {
1774
1901
  * @returns {Promise<Response>} Response object
1775
1902
  */
1776
1903
  request(method, url, options = {}) {
1777
- let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null } = options;
1904
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
1778
1905
 
1779
1906
  url = addParamsToUrl(url, params);
1780
1907
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1820,6 +1947,7 @@ class Session {
1820
1947
  if (mergedHeaders) requestConfig.headers = mergedHeaders;
1821
1948
  if (body) requestConfig.body = body;
1822
1949
  if (timeout) requestConfig.timeout = timeout;
1950
+ if (fetchMode) requestConfig.fetch_mode = fetchMode;
1823
1951
 
1824
1952
  // Register async request with callback manager
1825
1953
  const manager = getAsyncManager();
@@ -1871,58 +1999,101 @@ class Session {
1871
1999
  // ===========================================================================
1872
2000
 
1873
2001
  /**
1874
- * Get all cookies from the session
1875
- * @returns {Object} Cookies as key-value pairs
2002
+ * Get all cookies with full metadata (domain, path, expiry, flags).
2003
+ * @returns {Cookie[]} Array of Cookie objects
1876
2004
  */
1877
- getCookies() {
2005
+ getCookiesDetailed() {
1878
2006
  const resultPtr = this._lib.httpcloak_get_cookies(this._handle);
1879
2007
  const result = resultToString(resultPtr);
1880
2008
  if (result) {
1881
- return JSON.parse(result);
2009
+ const parsed = JSON.parse(result);
2010
+ return parsed.map(c => new Cookie(c));
1882
2011
  }
1883
- return {};
2012
+ return [];
2013
+ }
2014
+
2015
+ /**
2016
+ * Get all cookies from the session with full metadata.
2017
+ *
2018
+ * Returns Cookie[] with domain/path/expiry/flags. For the older flat
2019
+ * name->value object, build it yourself:
2020
+ * `Object.fromEntries(session.getCookies().map(c => [c.name, c.value]))`.
2021
+ * @returns {Cookie[]}
2022
+ */
2023
+ getCookies() {
2024
+ return this.getCookiesDetailed();
1884
2025
  }
1885
2026
 
1886
2027
  /**
1887
- * Get a specific cookie by name
2028
+ * Get a specific cookie by name with full metadata.
1888
2029
  * @param {string} name - Cookie name
1889
- * @returns {string|null} Cookie value or null if not found
2030
+ * @returns {Cookie|null} Cookie object or null if not found
2031
+ */
2032
+ getCookieDetailed(name) {
2033
+ const cookies = this.getCookiesDetailed();
2034
+ return cookies.find(c => c.name === name) || null;
2035
+ }
2036
+
2037
+ /**
2038
+ * Get a specific cookie by name.
2039
+ *
2040
+ * Returns a Cookie object (with domain/path/expiry/flags) or null. For just
2041
+ * the string value: `const c = session.getCookie('foo'); const v = c?.value`.
2042
+ * @param {string} name - Cookie name
2043
+ * @returns {Cookie|null}
1890
2044
  */
1891
2045
  getCookie(name) {
1892
- const cookies = this.getCookies();
1893
- return cookies[name] || null;
2046
+ return this.getCookieDetailed(name);
1894
2047
  }
1895
2048
 
1896
2049
  /**
1897
2050
  * Set a cookie in the session
1898
2051
  * @param {string} name - Cookie name
1899
2052
  * @param {string} value - Cookie value
1900
- */
1901
- setCookie(name, value) {
1902
- this._lib.httpcloak_set_cookie(this._handle, name, value);
2053
+ * @param {Object} [options] - Cookie options
2054
+ * @param {string} [options.domain] - Cookie domain
2055
+ * @param {string} [options.path] - Cookie path (default: "/")
2056
+ * @param {boolean} [options.secure] - Secure flag
2057
+ * @param {boolean} [options.httpOnly] - HttpOnly flag
2058
+ * @param {string} [options.sameSite] - SameSite attribute (Strict, Lax, None)
2059
+ * @param {number} [options.maxAge] - Max age in seconds (0 means not set)
2060
+ * @param {string} [options.expires] - Expiration date (RFC1123 format)
2061
+ */
2062
+ setCookie(name, value, options = {}) {
2063
+ const cookie = {
2064
+ name,
2065
+ value,
2066
+ domain: options.domain || "",
2067
+ path: options.path || "/",
2068
+ secure: options.secure || false,
2069
+ http_only: options.httpOnly || false,
2070
+ same_site: options.sameSite || "",
2071
+ max_age: options.maxAge || 0,
2072
+ expires: options.expires || "",
2073
+ };
2074
+ this._lib.httpcloak_set_cookie(this._handle, JSON.stringify(cookie));
1903
2075
  }
1904
2076
 
1905
2077
  /**
1906
2078
  * Delete a specific cookie by name
1907
2079
  * @param {string} name - Cookie name to delete
2080
+ * @param {string} [domain] - Domain to delete from (omit to delete from all domains)
1908
2081
  */
1909
- deleteCookie(name) {
1910
- // Set cookie to empty value - effectively deletes it
1911
- this._lib.httpcloak_set_cookie(this._handle, name, "");
2082
+ deleteCookie(name, domain = "") {
2083
+ this._lib.httpcloak_delete_cookie(this._handle, name, domain);
1912
2084
  }
1913
2085
 
1914
2086
  /**
1915
2087
  * Clear all cookies from the session
1916
2088
  */
1917
2089
  clearCookies() {
1918
- const cookies = this.getCookies();
1919
- for (const name of Object.keys(cookies)) {
1920
- this.deleteCookie(name);
1921
- }
2090
+ this._lib.httpcloak_clear_cookies(this._handle);
1922
2091
  }
1923
2092
 
1924
2093
  /**
1925
- * Get cookies as a property
2094
+ * Get cookies as a property.
2095
+ * @deprecated This property will return Cookie[] with full metadata in a future release.
2096
+ * @returns {Object} Cookies as key-value pairs
1926
2097
  */
1927
2098
  get cookies() {
1928
2099
  return this.getCookies();
@@ -2093,7 +2264,7 @@ class Session {
2093
2264
  * @param {string} path - Path to save the session file
2094
2265
  *
2095
2266
  * Example:
2096
- * const session = new httpcloak.Session({ preset: "chrome-145" });
2267
+ * const session = new httpcloak.Session({ preset: "chrome-146" });
2097
2268
  * await session.get("https://example.com"); // Acquire cookies
2098
2269
  * session.save("session.json");
2099
2270
  *
@@ -2731,7 +2902,7 @@ let _defaultConfig = {};
2731
2902
  /**
2732
2903
  * Configure defaults for module-level functions
2733
2904
  * @param {Object} options - Configuration options
2734
- * @param {string} [options.preset="chrome-145"] - Browser preset
2905
+ * @param {string} [options.preset="chrome-146"] - Browser preset
2735
2906
  * @param {Object} [options.headers] - Default headers
2736
2907
  * @param {Array} [options.auth] - Default basic auth [username, password]
2737
2908
  * @param {string} [options.proxy] - Proxy URL
@@ -2740,12 +2911,12 @@ let _defaultConfig = {};
2740
2911
  * @param {boolean} [options.verify=true] - SSL certificate verification
2741
2912
  * @param {boolean} [options.allowRedirects=true] - Follow redirects
2742
2913
  * @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
2743
- * @param {number} [options.retry=3] - Number of retries on failure (set to 0 to disable)
2744
- * @param {number[]} [options.retryOnStatus] - Status codes to retry on
2914
+ * @param {number} [options.retry=0] - Number of retries on failure (default 0 = no retries; set positive to enable. See issue #57)
2915
+ * @param {number[]} [options.retryOnStatus] - Status codes to retry on (only consulted when retry > 0)
2745
2916
  */
2746
2917
  function configure(options = {}) {
2747
2918
  const {
2748
- preset = "chrome-145",
2919
+ preset = "chrome-146",
2749
2920
  headers = null,
2750
2921
  auth = null,
2751
2922
  proxy = null,
@@ -2754,7 +2925,7 @@ function configure(options = {}) {
2754
2925
  verify = true,
2755
2926
  allowRedirects = true,
2756
2927
  maxRedirects = 10,
2757
- retry = 3,
2928
+ retry = 0,
2758
2929
  retryOnStatus = null,
2759
2930
  } = options;
2760
2931
 
@@ -2803,14 +2974,14 @@ function configure(options = {}) {
2803
2974
  */
2804
2975
  function _getDefaultSession() {
2805
2976
  if (!_defaultSession) {
2806
- const preset = _defaultConfig.preset || "chrome-145";
2977
+ const preset = _defaultConfig.preset || "chrome-146";
2807
2978
  const proxy = _defaultConfig.proxy || null;
2808
2979
  const timeout = _defaultConfig.timeout || 30;
2809
2980
  const httpVersion = _defaultConfig.httpVersion || "auto";
2810
2981
  const verify = _defaultConfig.verify !== undefined ? _defaultConfig.verify : true;
2811
2982
  const allowRedirects = _defaultConfig.allowRedirects !== undefined ? _defaultConfig.allowRedirects : true;
2812
2983
  const maxRedirects = _defaultConfig.maxRedirects || 10;
2813
- const retry = _defaultConfig.retry !== undefined ? _defaultConfig.retry : 3;
2984
+ const retry = _defaultConfig.retry !== undefined ? _defaultConfig.retry : 0;
2814
2985
  const retryOnStatus = _defaultConfig.retryOnStatus || null;
2815
2986
  const headers = _defaultConfig.headers || {};
2816
2987
 
@@ -2906,7 +3077,7 @@ function request(method, url, options = {}) {
2906
3077
  * Without registration, cache callbacks will not be triggered for that session.
2907
3078
  *
2908
3079
  * @example
2909
- * const proxy = new LocalProxy({ preset: "chrome-145", tlsOnly: true });
3080
+ * const proxy = new LocalProxy({ preset: "chrome-146", tlsOnly: true });
2910
3081
  * console.log(`Proxy running on ${proxy.proxyUrl}`);
2911
3082
  *
2912
3083
  * // Use with any HTTP client pointing to the proxy
@@ -2935,7 +3106,7 @@ class LocalProxy {
2935
3106
  * Create and start a local HTTP proxy server.
2936
3107
  * @param {Object} options - Proxy configuration options
2937
3108
  * @param {number} [options.port=0] - Port to listen on (0 = auto-select)
2938
- * @param {string} [options.preset="chrome-145"] - Browser fingerprint preset
3109
+ * @param {string} [options.preset="chrome-146"] - Browser fingerprint preset
2939
3110
  * @param {number} [options.timeout=30] - Request timeout in seconds
2940
3111
  * @param {number} [options.maxConnections=1000] - Maximum concurrent connections
2941
3112
  * @param {string} [options.tcpProxy] - Default upstream TCP proxy URL
@@ -2945,7 +3116,7 @@ class LocalProxy {
2945
3116
  constructor(options = {}) {
2946
3117
  const {
2947
3118
  port = 0,
2948
- preset = "chrome-145",
3119
+ preset = "chrome-146",
2949
3120
  timeout = 30,
2950
3121
  maxConnections = 1000,
2951
3122
  tcpProxy = null,
@@ -3457,10 +3628,247 @@ function clearSessionCache() {
3457
3628
  }
3458
3629
 
3459
3630
 
3631
+ // --- String Memory Management for Preset/Pool Functions ---
3632
+
3633
+ /**
3634
+ * Read a C string from a void pointer and free the native memory.
3635
+ * Returns null if the pointer is null/0.
3636
+ * @param {*} ptr - Native void pointer
3637
+ * @returns {string|null}
3638
+ */
3639
+ function readAndFreeString(ptr) {
3640
+ if (!ptr) return null;
3641
+ const lib = getLib();
3642
+ try {
3643
+ return koffi.decode(ptr, "char", -1);
3644
+ } finally {
3645
+ lib.httpcloak_free_string(ptr);
3646
+ }
3647
+ }
3648
+
3649
+ // --- Custom Preset Loading ---
3650
+
3651
+ /**
3652
+ * Load a custom preset from a JSON file and register it.
3653
+ * @param {string} filePath - Path to the preset JSON file
3654
+ * @returns {string} The registered preset name
3655
+ */
3656
+ function loadPreset(filePath) {
3657
+ const lib = getLib();
3658
+ const result = readAndFreeString(lib.httpcloak_preset_load_file(filePath));
3659
+ if (!result) {
3660
+ throw new HTTPCloakError("Failed to load preset from file");
3661
+ }
3662
+ const data = JSON.parse(result);
3663
+ if (data.error) {
3664
+ throw new HTTPCloakError(data.error);
3665
+ }
3666
+ return data.name;
3667
+ }
3668
+
3669
+ /**
3670
+ * Load a custom preset from a JSON string and register it.
3671
+ * @param {string} jsonData - JSON string defining the preset
3672
+ * @returns {string} The registered preset name
3673
+ */
3674
+ function loadPresetFromJSON(jsonData) {
3675
+ const lib = getLib();
3676
+ const result = readAndFreeString(lib.httpcloak_preset_load_json(jsonData));
3677
+ if (!result) {
3678
+ throw new HTTPCloakError("Failed to load preset from JSON");
3679
+ }
3680
+ const data = JSON.parse(result);
3681
+ if (data.error) {
3682
+ throw new HTTPCloakError(data.error);
3683
+ }
3684
+ return data.name;
3685
+ }
3686
+
3687
+ /**
3688
+ * Unregister a custom preset by name.
3689
+ * @param {string} name - The preset name to unregister
3690
+ */
3691
+ function unregisterPreset(name) {
3692
+ const lib = getLib();
3693
+ lib.httpcloak_preset_unregister(name);
3694
+ }
3695
+
3696
+ /**
3697
+ * Return a fully-resolved JSON document for the given preset.
3698
+ *
3699
+ * The output flattens any inheritance chain and resolves H2/H3 defaults
3700
+ * (Chrome unless the preset overrides) into explicit values, so the
3701
+ * result is suitable for saving, editing, and reloading via
3702
+ * loadPresetFromJSON. Two consecutive calls return byte-identical JSON.
3703
+ *
3704
+ * @param {string} name - The preset name (built-in or custom-registered).
3705
+ * @returns {string} JSON string of the form {"version":1,"preset":{...}}.
3706
+ * @throws {HTTPCloakError} If the preset is not registered or references
3707
+ * an unknown utls ClientHelloID.
3708
+ */
3709
+ function describePreset(name) {
3710
+ const lib = getLib();
3711
+ const result = lib.httpcloak_describe_preset(name);
3712
+ if (!result) {
3713
+ throw new HTTPCloakError(`Failed to describe preset: ${name}`);
3714
+ }
3715
+ // The error envelope ({"error": "..."}) is also a valid JSON document.
3716
+ // A successful describe always carries a top-level "preset" field, so
3717
+ // distinguishing the two is straightforward.
3718
+ let parsed;
3719
+ try {
3720
+ parsed = JSON.parse(result);
3721
+ } catch (err) {
3722
+ throw new HTTPCloakError(`Invalid describe_preset response: ${err.message}`);
3723
+ }
3724
+ if (parsed && parsed.error && !parsed.preset) {
3725
+ throw new HTTPCloakError(parsed.error);
3726
+ }
3727
+ return result;
3728
+ }
3729
+
3730
+ /**
3731
+ * A pool of custom fingerprint presets for rotation.
3732
+ *
3733
+ * Pools load multiple presets from a single JSON file and provide
3734
+ * round-robin or random selection. All presets are auto-registered
3735
+ * on construction, so you can pass the returned name directly to
3736
+ * Session({ preset: name }).
3737
+ */
3738
+ class PresetPool {
3739
+ /**
3740
+ * Load a preset pool from a JSON file.
3741
+ * @param {string} filePath - Path to the pool JSON file
3742
+ */
3743
+ constructor(filePath) {
3744
+ this._lib = getLib();
3745
+ const result = readAndFreeString(this._lib.httpcloak_pool_load_file(filePath));
3746
+ if (!result) {
3747
+ throw new HTTPCloakError(`Failed to load preset pool from file: ${filePath}`);
3748
+ }
3749
+ const data = JSON.parse(result);
3750
+ if (data.error) {
3751
+ throw new HTTPCloakError(data.error);
3752
+ }
3753
+ this._handle = BigInt(data.handle);
3754
+ }
3755
+
3756
+ /**
3757
+ * Load a preset pool from a JSON string.
3758
+ * @param {string} jsonData - JSON string defining the pool
3759
+ * @returns {PresetPool}
3760
+ */
3761
+ static fromJSON(jsonData) {
3762
+ const pool = Object.create(PresetPool.prototype);
3763
+ pool._lib = getLib();
3764
+ const result = readAndFreeString(pool._lib.httpcloak_pool_load_json(jsonData));
3765
+ if (!result) {
3766
+ throw new HTTPCloakError("Failed to load preset pool from JSON");
3767
+ }
3768
+ const data = JSON.parse(result);
3769
+ if (data.error) {
3770
+ throw new HTTPCloakError(data.error);
3771
+ }
3772
+ pool._handle = BigInt(data.handle);
3773
+ return pool;
3774
+ }
3775
+
3776
+ /**
3777
+ * Pick a preset using the pool's configured strategy.
3778
+ * @returns {string} Preset name
3779
+ */
3780
+ pick() {
3781
+ this._checkHandle();
3782
+ return this._parsePoolResult(this._lib.httpcloak_pool_pick(this._handle));
3783
+ }
3784
+
3785
+ /**
3786
+ * Pick a random preset from the pool.
3787
+ * @returns {string} Preset name
3788
+ */
3789
+ random() {
3790
+ this._checkHandle();
3791
+ return this._parsePoolResult(this._lib.httpcloak_pool_random(this._handle));
3792
+ }
3793
+
3794
+ /**
3795
+ * Pick the next preset in round-robin order.
3796
+ * @returns {string} Preset name
3797
+ */
3798
+ next() {
3799
+ this._checkHandle();
3800
+ return this._parsePoolResult(this._lib.httpcloak_pool_next(this._handle));
3801
+ }
3802
+
3803
+ /**
3804
+ * Get a preset by index.
3805
+ * @param {number} index - Zero-based index
3806
+ * @returns {string} Preset name
3807
+ */
3808
+ get(index) {
3809
+ this._checkHandle();
3810
+ return this._parsePoolResult(this._lib.httpcloak_pool_get(this._handle, index));
3811
+ }
3812
+
3813
+ /**
3814
+ * Number of presets in the pool.
3815
+ * @type {number}
3816
+ */
3817
+ get size() {
3818
+ this._checkHandle();
3819
+ const s = this._lib.httpcloak_pool_size(this._handle);
3820
+ if (s < 0) {
3821
+ throw new HTTPCloakError("Failed to get pool size");
3822
+ }
3823
+ return Number(s);
3824
+ }
3825
+
3826
+ /**
3827
+ * Name of the preset pool.
3828
+ * @type {string}
3829
+ */
3830
+ get name() {
3831
+ this._checkHandle();
3832
+ return this._parsePoolResult(this._lib.httpcloak_pool_name(this._handle));
3833
+ }
3834
+
3835
+ /**
3836
+ * Free the pool handle and unregister all its presets.
3837
+ */
3838
+ close() {
3839
+ if (this._handle && this._handle !== 0n && this._handle !== 0) {
3840
+ this._lib.httpcloak_pool_free(this._handle);
3841
+ this._handle = 0n;
3842
+ }
3843
+ }
3844
+
3845
+ _parsePoolResult(resultPtr) {
3846
+ const result = readAndFreeString(resultPtr);
3847
+ if (!result) {
3848
+ throw new HTTPCloakError("No result from preset pool");
3849
+ }
3850
+ // Error responses are JSON with {"error":"..."}
3851
+ if (result.startsWith("{")) {
3852
+ const data = JSON.parse(result);
3853
+ if (data.error) {
3854
+ throw new HTTPCloakError(data.error);
3855
+ }
3856
+ }
3857
+ return result;
3858
+ }
3859
+
3860
+ _checkHandle() {
3861
+ if (!this._handle || this._handle === 0n || this._handle === 0) {
3862
+ throw new HTTPCloakError("PresetPool is closed");
3863
+ }
3864
+ }
3865
+ }
3866
+
3460
3867
  module.exports = {
3461
3868
  // Classes
3462
3869
  Session,
3463
3870
  LocalProxy,
3871
+ PresetPool,
3464
3872
  Response,
3465
3873
  FastResponse,
3466
3874
  StreamResponse,
@@ -3470,6 +3878,11 @@ module.exports = {
3470
3878
  SessionCacheBackend,
3471
3879
  // Presets
3472
3880
  Preset,
3881
+ // Custom preset loading
3882
+ loadPreset,
3883
+ loadPresetFromJSON,
3884
+ unregisterPreset,
3885
+ describePreset,
3473
3886
  // Configuration
3474
3887
  configure,
3475
3888
  configureSessionCache,