httpcloak 1.6.1 → 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.d.ts CHANGED
@@ -268,6 +268,19 @@ export interface RequestOptions {
268
268
  auth?: [string, string];
269
269
  /** Optional request timeout in seconds */
270
270
  timeout?: number;
271
+ /**
272
+ * Explicit Sec-Fetch-Mode/Dest override for requests where auto-sniffing isn't enough.
273
+ *
274
+ * Valid values:
275
+ * - `"cors"` - XHR/fetch() request (Sec-Fetch-Mode: cors, Sec-Fetch-Dest: empty, Sec-Fetch-Site: same-origin)
276
+ * - `"no-cors"` - Subresource load (image/script/stylesheet tag)
277
+ * - `"navigate"` - Top-level navigation (document load, classic form POST)
278
+ * - `"websocket"` - WebSocket upgrade
279
+ *
280
+ * When unset (default), httpcloak auto-detects based on method, Accept, Content-Type, and Sec-Fetch-Dest headers.
281
+ * Set this explicitly when the auto-sniff gets it wrong (e.g., POST to a CORS endpoint without a JSON Accept header).
282
+ */
283
+ fetchMode?: "cors" | "no-cors" | "navigate" | "websocket";
271
284
  }
272
285
 
273
286
  export class Session {
@@ -1020,3 +1033,66 @@ export function configureSessionCache(options: SessionCacheOptions): SessionCach
1020
1033
  * After calling this, new sessions will not use distributed caching.
1021
1034
  */
1022
1035
  export function clearSessionCache(): void;
1036
+
1037
+ /**
1038
+ * Load a custom preset from a JSON file and register it.
1039
+ * @param filePath - Path to the preset JSON file
1040
+ * @returns The registered preset name
1041
+ */
1042
+ export function loadPreset(filePath: string): string;
1043
+
1044
+ /**
1045
+ * Load a custom preset from a JSON string and register it.
1046
+ * @param jsonData - JSON string defining the preset
1047
+ * @returns The registered preset name
1048
+ */
1049
+ export function loadPresetFromJSON(jsonData: string): string;
1050
+
1051
+ /**
1052
+ * Unregister a custom preset by name.
1053
+ * @param name - The preset name to unregister
1054
+ */
1055
+ export function unregisterPreset(name: string): void;
1056
+
1057
+ /**
1058
+ * A pool of custom fingerprint presets for rotation.
1059
+ *
1060
+ * Pools load multiple presets from a single JSON file and provide
1061
+ * round-robin or random selection. All presets are auto-registered
1062
+ * on construction, so you can pass the returned name directly to
1063
+ * `new Session({ preset: name })`.
1064
+ */
1065
+ export class PresetPool {
1066
+ /**
1067
+ * Load a preset pool from a JSON file.
1068
+ * @param filePath - Path to the pool JSON file
1069
+ */
1070
+ constructor(filePath: string);
1071
+
1072
+ /**
1073
+ * Load a preset pool from a JSON string.
1074
+ * @param jsonData - JSON string defining the pool
1075
+ */
1076
+ static fromJSON(jsonData: string): PresetPool;
1077
+
1078
+ /** Pick a preset using the pool's configured strategy. */
1079
+ pick(): string;
1080
+
1081
+ /** Pick a random preset from the pool. */
1082
+ random(): string;
1083
+
1084
+ /** Pick the next preset in round-robin order. */
1085
+ next(): string;
1086
+
1087
+ /** Get a preset by index. */
1088
+ get(index: number): string;
1089
+
1090
+ /** Number of presets in the pool. */
1091
+ readonly size: number;
1092
+
1093
+ /** Name of the preset pool. */
1094
+ readonly name: string;
1095
+
1096
+ /** Free the pool handle and unregister all its presets. */
1097
+ close(): void;
1098
+ }
package/lib/index.js CHANGED
@@ -216,11 +216,28 @@ class Response {
216
216
  * @param {Object} data - Response data from native library
217
217
  * @param {number} [elapsed=0] - Elapsed time in milliseconds
218
218
  */
219
- constructor(data, elapsed = 0) {
219
+ constructor(data, elapsed = 0, rawBody = null) {
220
220
  this.statusCode = data.status_code || 0;
221
221
  this.headers = data.headers || {};
222
- this._body = Buffer.from(data.body || "", "utf8");
223
- 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
+ }
224
241
  this.finalUrl = data.final_url || "";
225
242
  this.protocol = data.protocol || "";
226
243
  this.elapsed = elapsed; // milliseconds
@@ -787,27 +804,37 @@ function getLib() {
787
804
  const libPath = getLibPath();
788
805
  nativeLibHandle = koffi.load(libPath);
789
806
 
790
- // Use str for string returns - koffi handles the string copy automatically
791
- // 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
+
792
819
  lib = {
793
820
  httpcloak_session_new: nativeLibHandle.func("httpcloak_session_new", "int64", ["str"]),
794
821
  httpcloak_session_free: nativeLibHandle.func("httpcloak_session_free", "void", ["int64"]),
795
822
  httpcloak_session_refresh: nativeLibHandle.func("httpcloak_session_refresh", "void", ["int64"]),
796
- httpcloak_session_refresh_protocol: nativeLibHandle.func("httpcloak_session_refresh_protocol", "str", ["int64", "str"]),
797
- 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"]),
798
825
  httpcloak_session_fork: nativeLibHandle.func("httpcloak_session_fork", "int64", ["int64"]),
799
- httpcloak_get: nativeLibHandle.func("httpcloak_get", "str", ["int64", "str", "str"]),
800
- httpcloak_post: nativeLibHandle.func("httpcloak_post", "str", ["int64", "str", "str", "str"]),
801
- httpcloak_request: nativeLibHandle.func("httpcloak_request", "str", ["int64", "str"]),
802
- httpcloak_get_cookies: nativeLibHandle.func("httpcloak_get_cookies", "str", ["int64"]),
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"]),
803
830
  httpcloak_set_cookie: nativeLibHandle.func("httpcloak_set_cookie", "void", ["int64", "str"]),
804
831
  httpcloak_delete_cookie: nativeLibHandle.func("httpcloak_delete_cookie", "void", ["int64", "str", "str"]),
805
832
  httpcloak_clear_cookies: nativeLibHandle.func("httpcloak_clear_cookies", "void", ["int64"]),
806
- httpcloak_free_string: nativeLibHandle.func("httpcloak_free_string", "void", ["void*"]),
807
- httpcloak_version: nativeLibHandle.func("httpcloak_version", "str", []),
808
- httpcloak_available_presets: nativeLibHandle.func("httpcloak_available_presets", "str", []),
809
- httpcloak_set_ech_dns_servers: nativeLibHandle.func("httpcloak_set_ech_dns_servers", "str", ["str"]),
810
- httpcloak_get_ech_dns_servers: nativeLibHandle.func("httpcloak_get_ech_dns_servers", "str", []),
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, []),
811
838
  // Async functions
812
839
  httpcloak_register_callback: nativeLibHandle.func("httpcloak_register_callback", "int64", [koffi.pointer(AsyncCallbackProto)]),
813
840
  httpcloak_unregister_callback: nativeLibHandle.func("httpcloak_unregister_callback", "void", ["int64"]),
@@ -818,41 +845,41 @@ function getLib() {
818
845
  httpcloak_stream_get: nativeLibHandle.func("httpcloak_stream_get", "int64", ["int64", "str", "str"]),
819
846
  httpcloak_stream_post: nativeLibHandle.func("httpcloak_stream_post", "int64", ["int64", "str", "str", "str"]),
820
847
  httpcloak_stream_request: nativeLibHandle.func("httpcloak_stream_request", "int64", ["int64", "str"]),
821
- httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata", "str", ["int64"]),
822
- 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"]),
823
850
  httpcloak_stream_close: nativeLibHandle.func("httpcloak_stream_close", "void", ["int64"]),
824
851
  // Raw response functions for fast-path (zero-copy)
825
852
  httpcloak_get_raw: nativeLibHandle.func("httpcloak_get_raw", "int64", ["int64", "str", "str"]),
826
853
  httpcloak_post_raw: nativeLibHandle.func("httpcloak_post_raw", "int64", ["int64", "str", "void*", "int", "str"]),
827
854
  httpcloak_request_raw: nativeLibHandle.func("httpcloak_request_raw", "int64", ["int64", "str", "void*", "int"]),
828
- 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"]),
829
856
  httpcloak_response_get_body_len: nativeLibHandle.func("httpcloak_response_get_body_len", "int", ["int64"]),
830
857
  httpcloak_response_copy_body_to: nativeLibHandle.func("httpcloak_response_copy_body_to", "int", ["int64", "void*", "int"]),
831
858
  httpcloak_response_free: nativeLibHandle.func("httpcloak_response_free", "void", ["int64"]),
832
859
  // Combined finalize function (copy + metadata + free in one call)
833
- 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"]),
834
861
  // Session persistence functions
835
- httpcloak_session_save: nativeLibHandle.func("httpcloak_session_save", "str", ["int64", "str"]),
862
+ httpcloak_session_save: nativeLibHandle.func("httpcloak_session_save", HeapStr, ["int64", "str"]),
836
863
  httpcloak_session_load: nativeLibHandle.func("httpcloak_session_load", "int64", ["str"]),
837
- httpcloak_session_marshal: nativeLibHandle.func("httpcloak_session_marshal", "str", ["int64"]),
864
+ httpcloak_session_marshal: nativeLibHandle.func("httpcloak_session_marshal", HeapStr, ["int64"]),
838
865
  httpcloak_session_unmarshal: nativeLibHandle.func("httpcloak_session_unmarshal", "int64", ["str"]),
839
866
  // Proxy management functions
840
- httpcloak_session_set_proxy: nativeLibHandle.func("httpcloak_session_set_proxy", "str", ["int64", "str"]),
841
- httpcloak_session_set_tcp_proxy: nativeLibHandle.func("httpcloak_session_set_tcp_proxy", "str", ["int64", "str"]),
842
- httpcloak_session_set_udp_proxy: nativeLibHandle.func("httpcloak_session_set_udp_proxy", "str", ["int64", "str"]),
843
- httpcloak_session_get_proxy: nativeLibHandle.func("httpcloak_session_get_proxy", "str", ["int64"]),
844
- httpcloak_session_get_tcp_proxy: nativeLibHandle.func("httpcloak_session_get_tcp_proxy", "str", ["int64"]),
845
- 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"]),
846
873
  // Header order customization
847
- httpcloak_session_set_header_order: nativeLibHandle.func("httpcloak_session_set_header_order", "str", ["int64", "str"]),
848
- 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"]),
849
876
  // Local proxy functions
850
877
  httpcloak_local_proxy_start: nativeLibHandle.func("httpcloak_local_proxy_start", "int64", ["str"]),
851
878
  httpcloak_local_proxy_stop: nativeLibHandle.func("httpcloak_local_proxy_stop", "void", ["int64"]),
852
879
  httpcloak_local_proxy_get_port: nativeLibHandle.func("httpcloak_local_proxy_get_port", "int", ["int64"]),
853
880
  httpcloak_local_proxy_is_running: nativeLibHandle.func("httpcloak_local_proxy_is_running", "int", ["int64"]),
854
- httpcloak_local_proxy_get_stats: nativeLibHandle.func("httpcloak_local_proxy_get_stats", "str", ["int64"]),
855
- 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"]),
856
883
  httpcloak_local_proxy_unregister_session: nativeLibHandle.func("httpcloak_local_proxy_unregister_session", "int", ["int64", "str"]),
857
884
  // Session cache callbacks
858
885
  httpcloak_set_session_cache_callbacks: nativeLibHandle.func("httpcloak_set_session_cache_callbacks", "void", [
@@ -876,6 +903,23 @@ function getLib() {
876
903
  // Async cache result functions (called by JS to provide results to Go)
877
904
  httpcloak_async_cache_get_result: nativeLibHandle.func("httpcloak_async_cache_get_result", "void", ["int64", "str"]),
878
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"]),
879
923
  };
880
924
  }
881
925
  return lib;
@@ -1029,6 +1073,38 @@ function parseResponse(resultPtr, elapsed = 0) {
1029
1073
  return new Response(data, elapsed);
1030
1074
  }
1031
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
+
1032
1108
  /**
1033
1109
  * Add query parameters to URL
1034
1110
  */
@@ -1250,8 +1326,8 @@ class Session {
1250
1326
  * @param {boolean} [options.verify=true] - SSL certificate verification
1251
1327
  * @param {boolean} [options.allowRedirects=true] - Follow redirects
1252
1328
  * @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
1253
- * @param {number} [options.retry=3] - Number of retries on failure (set to 0 to disable)
1254
- * @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)
1255
1331
  * @param {number} [options.retryWaitMin=500] - Minimum wait time between retries in milliseconds
1256
1332
  * @param {number} [options.retryWaitMax=10000] - Maximum wait time between retries in milliseconds
1257
1333
  * @param {Array} [options.auth] - Default auth [username, password] for all requests
@@ -1271,7 +1347,7 @@ class Session {
1271
1347
  verify = true,
1272
1348
  allowRedirects = true,
1273
1349
  maxRedirects = 10,
1274
- retry = 3,
1350
+ retry = 0,
1275
1351
  retryOnStatus = null,
1276
1352
  retryWaitMin = 500,
1277
1353
  retryWaitMax = 10000,
@@ -1522,7 +1598,7 @@ class Session {
1522
1598
  * @returns {Response} Response object
1523
1599
  */
1524
1600
  getSync(url, options = {}) {
1525
- const { headers = null, params = null, cookies = null, auth = null } = options;
1601
+ const { headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
1526
1602
 
1527
1603
  url = addParamsToUrl(url, params);
1528
1604
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1536,12 +1612,18 @@ class Session {
1536
1612
  if (mergedHeaders) {
1537
1613
  reqOptions.headers = mergedHeaders;
1538
1614
  }
1615
+ if (fetchMode) {
1616
+ reqOptions.fetch_mode = fetchMode;
1617
+ }
1539
1618
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1540
1619
 
1541
1620
  const startTime = Date.now();
1542
- const result = this._lib.httpcloak_get(this._handle, url, optionsJson);
1621
+ const responseHandle = this._lib.httpcloak_get_raw(this._handle, url, optionsJson);
1543
1622
  const elapsed = Date.now() - startTime;
1544
- 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);
1545
1627
  }
1546
1628
 
1547
1629
  /**
@@ -1562,38 +1644,36 @@ class Session {
1562
1644
  * @returns {Response} Response object
1563
1645
  */
1564
1646
  postSync(url, options = {}) {
1565
- 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;
1566
1648
 
1567
1649
  url = addParamsToUrl(url, params);
1568
1650
  let mergedHeaders = this._mergeHeaders(headers);
1569
1651
 
1570
- // 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;
1571
1655
  if (files !== null) {
1572
1656
  const formData = (data !== null && typeof data === "object") ? data : null;
1573
1657
  const multipart = encodeMultipart(formData, files);
1574
- body = multipart.body.toString("latin1"); // Preserve binary data
1658
+ bodyBuffer = multipart.body;
1575
1659
  mergedHeaders = mergedHeaders || {};
1576
1660
  mergedHeaders["Content-Type"] = multipart.contentType;
1577
- }
1578
- // Handle JSON body
1579
- else if (json !== null) {
1580
- body = JSON.stringify(json);
1661
+ } else if (json !== null) {
1662
+ bodyBuffer = Buffer.from(JSON.stringify(json), "utf8");
1581
1663
  mergedHeaders = mergedHeaders || {};
1582
1664
  if (!mergedHeaders["Content-Type"]) {
1583
1665
  mergedHeaders["Content-Type"] = "application/json";
1584
1666
  }
1585
- }
1586
- // Handle form data
1587
- else if (data !== null && typeof data === "object") {
1588
- body = new URLSearchParams(data).toString();
1667
+ } else if (data !== null && typeof data === "object") {
1668
+ bodyBuffer = Buffer.from(new URLSearchParams(data).toString(), "utf8");
1589
1669
  mergedHeaders = mergedHeaders || {};
1590
1670
  if (!mergedHeaders["Content-Type"]) {
1591
1671
  mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1592
1672
  }
1593
- }
1594
- // Handle Buffer body
1595
- else if (Buffer.isBuffer(body)) {
1596
- 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");
1597
1677
  }
1598
1678
 
1599
1679
  // Use request auth if provided, otherwise fall back to session auth
@@ -1606,12 +1686,21 @@ class Session {
1606
1686
  if (mergedHeaders) {
1607
1687
  reqOptions.headers = mergedHeaders;
1608
1688
  }
1689
+ if (fetchMode) {
1690
+ reqOptions.fetch_mode = fetchMode;
1691
+ }
1609
1692
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1610
1693
 
1694
+ const bodyPtr = bodyBuffer || Buffer.alloc(0);
1695
+ const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
1696
+
1611
1697
  const startTime = Date.now();
1612
- 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);
1613
1699
  const elapsed = Date.now() - startTime;
1614
- 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);
1615
1704
  }
1616
1705
 
1617
1706
  /**
@@ -1624,38 +1713,35 @@ class Session {
1624
1713
  * @returns {Response} Response object
1625
1714
  */
1626
1715
  requestSync(method, url, options = {}) {
1627
- 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;
1628
1717
 
1629
1718
  url = addParamsToUrl(url, params);
1630
1719
  let mergedHeaders = this._mergeHeaders(headers);
1631
1720
 
1632
- // Handle multipart file upload
1721
+ // Normalize body to a Buffer so the raw path can pass (ptr, len).
1722
+ let bodyBuffer = null;
1633
1723
  if (files !== null) {
1634
1724
  const formData = (data !== null && typeof data === "object") ? data : null;
1635
1725
  const multipart = encodeMultipart(formData, files);
1636
- body = multipart.body.toString("latin1"); // Preserve binary data
1726
+ bodyBuffer = multipart.body;
1637
1727
  mergedHeaders = mergedHeaders || {};
1638
1728
  mergedHeaders["Content-Type"] = multipart.contentType;
1639
- }
1640
- // Handle JSON body
1641
- else if (json !== null) {
1642
- body = JSON.stringify(json);
1729
+ } else if (json !== null) {
1730
+ bodyBuffer = Buffer.from(JSON.stringify(json), "utf8");
1643
1731
  mergedHeaders = mergedHeaders || {};
1644
1732
  if (!mergedHeaders["Content-Type"]) {
1645
1733
  mergedHeaders["Content-Type"] = "application/json";
1646
1734
  }
1647
- }
1648
- // Handle form data
1649
- else if (data !== null && typeof data === "object") {
1650
- body = new URLSearchParams(data).toString();
1735
+ } else if (data !== null && typeof data === "object") {
1736
+ bodyBuffer = Buffer.from(new URLSearchParams(data).toString(), "utf8");
1651
1737
  mergedHeaders = mergedHeaders || {};
1652
1738
  if (!mergedHeaders["Content-Type"]) {
1653
1739
  mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1654
1740
  }
1655
- }
1656
- // Handle Buffer body
1657
- else if (Buffer.isBuffer(body)) {
1658
- 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");
1659
1745
  }
1660
1746
 
1661
1747
  // Use request auth if provided, otherwise fall back to session auth
@@ -1668,16 +1754,24 @@ class Session {
1668
1754
  url,
1669
1755
  };
1670
1756
  if (mergedHeaders) requestConfig.headers = mergedHeaders;
1671
- if (body) requestConfig.body = body;
1672
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;
1673
1762
 
1674
1763
  const startTime = Date.now();
1675
- const result = this._lib.httpcloak_request(
1764
+ const responseHandle = this._lib.httpcloak_request_raw(
1676
1765
  this._handle,
1677
- JSON.stringify(requestConfig)
1766
+ JSON.stringify(requestConfig),
1767
+ bodyPtr,
1768
+ bodyLen
1678
1769
  );
1679
1770
  const elapsed = Date.now() - startTime;
1680
- 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);
1681
1775
  }
1682
1776
 
1683
1777
  // ===========================================================================
@@ -1692,7 +1786,7 @@ class Session {
1692
1786
  * @returns {Promise<Response>} Response object
1693
1787
  */
1694
1788
  get(url, options = {}) {
1695
- 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;
1696
1790
 
1697
1791
  url = addParamsToUrl(url, params);
1698
1792
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1706,6 +1800,15 @@ class Session {
1706
1800
  if (mergedHeaders) {
1707
1801
  reqOptions.headers = mergedHeaders;
1708
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
+ }
1709
1812
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1710
1813
 
1711
1814
  // Register async request with callback manager
@@ -1726,7 +1829,7 @@ class Session {
1726
1829
  * @returns {Promise<Response>} Response object
1727
1830
  */
1728
1831
  post(url, options = {}) {
1729
- 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;
1730
1833
 
1731
1834
  url = addParamsToUrl(url, params);
1732
1835
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1770,6 +1873,13 @@ class Session {
1770
1873
  if (mergedHeaders) {
1771
1874
  reqOptions.headers = mergedHeaders;
1772
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
+ }
1773
1883
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1774
1884
 
1775
1885
  // Register async request with callback manager
@@ -1791,7 +1901,7 @@ class Session {
1791
1901
  * @returns {Promise<Response>} Response object
1792
1902
  */
1793
1903
  request(method, url, options = {}) {
1794
- 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;
1795
1905
 
1796
1906
  url = addParamsToUrl(url, params);
1797
1907
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1837,6 +1947,7 @@ class Session {
1837
1947
  if (mergedHeaders) requestConfig.headers = mergedHeaders;
1838
1948
  if (body) requestConfig.body = body;
1839
1949
  if (timeout) requestConfig.timeout = timeout;
1950
+ if (fetchMode) requestConfig.fetch_mode = fetchMode;
1840
1951
 
1841
1952
  // Register async request with callback manager
1842
1953
  const manager = getAsyncManager();
@@ -1902,25 +2013,15 @@ class Session {
1902
2013
  }
1903
2014
 
1904
2015
  /**
1905
- * Get all cookies as a flat name-value object.
1906
- * @deprecated getCookies() will return Cookie[] with full metadata (domain, path, expiry) in a future release.
1907
- * Use getCookiesDetailed() if you want the new format now.
1908
- * @returns {Object} Cookies as key-value pairs
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[]}
1909
2022
  */
1910
2023
  getCookies() {
1911
- if (!Session._getCookiesDeprecated) {
1912
- Session._getCookiesDeprecated = true;
1913
- process.emitWarning(
1914
- 'getCookies() currently returns a flat {name: value} object. In a future release, it will return Cookie[] with full metadata (domain, path, expiry, etc.), same as getCookiesDetailed(). Update your code accordingly.',
1915
- 'DeprecationWarning'
1916
- );
1917
- }
1918
- const cookies = this.getCookiesDetailed();
1919
- const result = {};
1920
- for (const c of cookies) {
1921
- result[c.name] = c.value;
1922
- }
1923
- return result;
2024
+ return this.getCookiesDetailed();
1924
2025
  }
1925
2026
 
1926
2027
  /**
@@ -1934,22 +2035,15 @@ class Session {
1934
2035
  }
1935
2036
 
1936
2037
  /**
1937
- * Get a specific cookie value by name.
1938
- * @deprecated getCookie() will return a Cookie object (with domain, path, expiry) instead of a string in a future release.
1939
- * Use getCookieDetailed() if you want the new format now.
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`.
1940
2042
  * @param {string} name - Cookie name
1941
- * @returns {string|null} Cookie value or null if not found
2043
+ * @returns {Cookie|null}
1942
2044
  */
1943
2045
  getCookie(name) {
1944
- if (!Session._getCookieDeprecated) {
1945
- Session._getCookieDeprecated = true;
1946
- process.emitWarning(
1947
- 'getCookie() currently returns a string value. In a future release, it will return a Cookie object with full metadata (domain, path, expiry, etc.), same as getCookieDetailed(). Update your code accordingly.',
1948
- 'DeprecationWarning'
1949
- );
1950
- }
1951
- const cookie = this.getCookieDetailed(name);
1952
- return cookie ? cookie.value : null;
2046
+ return this.getCookieDetailed(name);
1953
2047
  }
1954
2048
 
1955
2049
  /**
@@ -2817,8 +2911,8 @@ let _defaultConfig = {};
2817
2911
  * @param {boolean} [options.verify=true] - SSL certificate verification
2818
2912
  * @param {boolean} [options.allowRedirects=true] - Follow redirects
2819
2913
  * @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
2820
- * @param {number} [options.retry=3] - Number of retries on failure (set to 0 to disable)
2821
- * @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)
2822
2916
  */
2823
2917
  function configure(options = {}) {
2824
2918
  const {
@@ -2831,7 +2925,7 @@ function configure(options = {}) {
2831
2925
  verify = true,
2832
2926
  allowRedirects = true,
2833
2927
  maxRedirects = 10,
2834
- retry = 3,
2928
+ retry = 0,
2835
2929
  retryOnStatus = null,
2836
2930
  } = options;
2837
2931
 
@@ -2887,7 +2981,7 @@ function _getDefaultSession() {
2887
2981
  const verify = _defaultConfig.verify !== undefined ? _defaultConfig.verify : true;
2888
2982
  const allowRedirects = _defaultConfig.allowRedirects !== undefined ? _defaultConfig.allowRedirects : true;
2889
2983
  const maxRedirects = _defaultConfig.maxRedirects || 10;
2890
- const retry = _defaultConfig.retry !== undefined ? _defaultConfig.retry : 3;
2984
+ const retry = _defaultConfig.retry !== undefined ? _defaultConfig.retry : 0;
2891
2985
  const retryOnStatus = _defaultConfig.retryOnStatus || null;
2892
2986
  const headers = _defaultConfig.headers || {};
2893
2987
 
@@ -3534,10 +3628,247 @@ function clearSessionCache() {
3534
3628
  }
3535
3629
 
3536
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
+
3537
3867
  module.exports = {
3538
3868
  // Classes
3539
3869
  Session,
3540
3870
  LocalProxy,
3871
+ PresetPool,
3541
3872
  Response,
3542
3873
  FastResponse,
3543
3874
  StreamResponse,
@@ -3547,6 +3878,11 @@ module.exports = {
3547
3878
  SessionCacheBackend,
3548
3879
  // Presets
3549
3880
  Preset,
3881
+ // Custom preset loading
3882
+ loadPreset,
3883
+ loadPresetFromJSON,
3884
+ unregisterPreset,
3885
+ describePreset,
3550
3886
  // Configuration
3551
3887
  configure,
3552
3888
  configureSessionCache,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/darwin-arm64",
3
- "version": "1.6.1",
3
+ "version": "1.6.5",
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.6.1",
3
+ "version": "1.6.5",
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.6.1",
3
+ "version": "1.6.5",
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.6.1",
3
+ "version": "1.6.5",
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.6.1",
3
+ "version": "1.6.5",
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.6.1",
3
+ "version": "1.6.5",
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.6.1",
3
+ "version": "1.6.5",
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
  "module": "lib/index.mjs",
@@ -49,12 +49,12 @@
49
49
  "koffi": "^2.9.0"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@httpcloak/darwin-arm64": "1.6.1",
53
- "@httpcloak/darwin-x64": "1.6.1",
54
- "@httpcloak/linux-arm64": "1.6.1",
55
- "@httpcloak/linux-x64": "1.6.1",
56
- "@httpcloak/win32-arm64": "1.6.1",
57
- "@httpcloak/win32-x64": "1.6.1"
52
+ "@httpcloak/darwin-arm64": "1.6.5",
53
+ "@httpcloak/darwin-x64": "1.6.5",
54
+ "@httpcloak/linux-arm64": "1.6.5",
55
+ "@httpcloak/linux-x64": "1.6.5",
56
+ "@httpcloak/win32-arm64": "1.6.5",
57
+ "@httpcloak/win32-x64": "1.6.5"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^25.1.0",