httpcloak 1.6.1 → 1.6.6

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
@@ -241,6 +241,8 @@ export interface SessionOptions {
241
241
  enableSpeculativeTls?: boolean;
242
242
  /** Protocol to switch to after Refresh() (e.g., "h1", "h2", "h3") */
243
243
  switchProtocol?: string;
244
+ /** Disable internal cookie jar entirely — caller manages cookies via per-request headers (default: false) */
245
+ withoutCookieJar?: boolean;
244
246
  /** Custom JA3 fingerprint string (e.g., "771,4865-4866-4867-...,0-23-65281-...,29-23-24,0") */
245
247
  ja3?: string;
246
248
  /** Custom Akamai HTTP/2 fingerprint string (e.g., "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p") */
@@ -268,6 +270,19 @@ export interface RequestOptions {
268
270
  auth?: [string, string];
269
271
  /** Optional request timeout in seconds */
270
272
  timeout?: number;
273
+ /**
274
+ * Explicit Sec-Fetch-Mode/Dest override for requests where auto-sniffing isn't enough.
275
+ *
276
+ * Valid values:
277
+ * - `"cors"` - XHR/fetch() request (Sec-Fetch-Mode: cors, Sec-Fetch-Dest: empty, Sec-Fetch-Site: same-origin)
278
+ * - `"no-cors"` - Subresource load (image/script/stylesheet tag)
279
+ * - `"navigate"` - Top-level navigation (document load, classic form POST)
280
+ * - `"websocket"` - WebSocket upgrade
281
+ *
282
+ * When unset (default), httpcloak auto-detects based on method, Accept, Content-Type, and Sec-Fetch-Dest headers.
283
+ * Set this explicitly when the auto-sniff gets it wrong (e.g., POST to a CORS endpoint without a JSON Accept header).
284
+ */
285
+ fetchMode?: "cors" | "no-cors" | "navigate" | "websocket";
271
286
  }
272
287
 
273
288
  export class Session {
@@ -1020,3 +1035,66 @@ export function configureSessionCache(options: SessionCacheOptions): SessionCach
1020
1035
  * After calling this, new sessions will not use distributed caching.
1021
1036
  */
1022
1037
  export function clearSessionCache(): void;
1038
+
1039
+ /**
1040
+ * Load a custom preset from a JSON file and register it.
1041
+ * @param filePath - Path to the preset JSON file
1042
+ * @returns The registered preset name
1043
+ */
1044
+ export function loadPreset(filePath: string): string;
1045
+
1046
+ /**
1047
+ * Load a custom preset from a JSON string and register it.
1048
+ * @param jsonData - JSON string defining the preset
1049
+ * @returns The registered preset name
1050
+ */
1051
+ export function loadPresetFromJSON(jsonData: string): string;
1052
+
1053
+ /**
1054
+ * Unregister a custom preset by name.
1055
+ * @param name - The preset name to unregister
1056
+ */
1057
+ export function unregisterPreset(name: string): void;
1058
+
1059
+ /**
1060
+ * A pool of custom fingerprint presets for rotation.
1061
+ *
1062
+ * Pools load multiple presets from a single JSON file and provide
1063
+ * round-robin or random selection. All presets are auto-registered
1064
+ * on construction, so you can pass the returned name directly to
1065
+ * `new Session({ preset: name })`.
1066
+ */
1067
+ export class PresetPool {
1068
+ /**
1069
+ * Load a preset pool from a JSON file.
1070
+ * @param filePath - Path to the pool JSON file
1071
+ */
1072
+ constructor(filePath: string);
1073
+
1074
+ /**
1075
+ * Load a preset pool from a JSON string.
1076
+ * @param jsonData - JSON string defining the pool
1077
+ */
1078
+ static fromJSON(jsonData: string): PresetPool;
1079
+
1080
+ /** Pick a preset using the pool's configured strategy. */
1081
+ pick(): string;
1082
+
1083
+ /** Pick a random preset from the pool. */
1084
+ random(): string;
1085
+
1086
+ /** Pick the next preset in round-robin order. */
1087
+ next(): string;
1088
+
1089
+ /** Get a preset by index. */
1090
+ get(index: number): string;
1091
+
1092
+ /** Number of presets in the pool. */
1093
+ readonly size: number;
1094
+
1095
+ /** Name of the preset pool. */
1096
+ readonly name: string;
1097
+
1098
+ /** Free the pool handle and unregister all its presets. */
1099
+ close(): void;
1100
+ }
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,
@@ -1285,6 +1361,7 @@ class Session {
1285
1361
  keyLogFile = null,
1286
1362
  enableSpeculativeTls = false,
1287
1363
  switchProtocol = null,
1364
+ withoutCookieJar = false,
1288
1365
  ja3 = null,
1289
1366
  akamai = null,
1290
1367
  extraFp = null,
@@ -1359,6 +1436,9 @@ class Session {
1359
1436
  if (switchProtocol) {
1360
1437
  config.switch_protocol = switchProtocol;
1361
1438
  }
1439
+ if (withoutCookieJar) {
1440
+ config.without_cookie_jar = true;
1441
+ }
1362
1442
  if (ja3) {
1363
1443
  config.ja3 = ja3;
1364
1444
  }
@@ -1522,7 +1602,7 @@ class Session {
1522
1602
  * @returns {Response} Response object
1523
1603
  */
1524
1604
  getSync(url, options = {}) {
1525
- const { headers = null, params = null, cookies = null, auth = null } = options;
1605
+ const { headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
1526
1606
 
1527
1607
  url = addParamsToUrl(url, params);
1528
1608
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1536,12 +1616,18 @@ class Session {
1536
1616
  if (mergedHeaders) {
1537
1617
  reqOptions.headers = mergedHeaders;
1538
1618
  }
1619
+ if (fetchMode) {
1620
+ reqOptions.fetch_mode = fetchMode;
1621
+ }
1539
1622
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1540
1623
 
1541
1624
  const startTime = Date.now();
1542
- const result = this._lib.httpcloak_get(this._handle, url, optionsJson);
1625
+ const responseHandle = this._lib.httpcloak_get_raw(this._handle, url, optionsJson);
1543
1626
  const elapsed = Date.now() - startTime;
1544
- return parseResponse(result, elapsed);
1627
+ if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
1628
+ throw new HTTPCloakError("Request failed");
1629
+ }
1630
+ return parseRawResponse(this._lib, responseHandle, elapsed);
1545
1631
  }
1546
1632
 
1547
1633
  /**
@@ -1562,38 +1648,36 @@ class Session {
1562
1648
  * @returns {Response} Response object
1563
1649
  */
1564
1650
  postSync(url, options = {}) {
1565
- let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
1651
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
1566
1652
 
1567
1653
  url = addParamsToUrl(url, params);
1568
1654
  let mergedHeaders = this._mergeHeaders(headers);
1569
1655
 
1570
- // Handle multipart file upload
1656
+ // Normalize body to a Buffer so the raw path can pass (ptr, len) without
1657
+ // null-terminator truncation or utf8 mangling.
1658
+ let bodyBuffer = null;
1571
1659
  if (files !== null) {
1572
1660
  const formData = (data !== null && typeof data === "object") ? data : null;
1573
1661
  const multipart = encodeMultipart(formData, files);
1574
- body = multipart.body.toString("latin1"); // Preserve binary data
1662
+ bodyBuffer = multipart.body;
1575
1663
  mergedHeaders = mergedHeaders || {};
1576
1664
  mergedHeaders["Content-Type"] = multipart.contentType;
1577
- }
1578
- // Handle JSON body
1579
- else if (json !== null) {
1580
- body = JSON.stringify(json);
1665
+ } else if (json !== null) {
1666
+ bodyBuffer = Buffer.from(JSON.stringify(json), "utf8");
1581
1667
  mergedHeaders = mergedHeaders || {};
1582
1668
  if (!mergedHeaders["Content-Type"]) {
1583
1669
  mergedHeaders["Content-Type"] = "application/json";
1584
1670
  }
1585
- }
1586
- // Handle form data
1587
- else if (data !== null && typeof data === "object") {
1588
- body = new URLSearchParams(data).toString();
1671
+ } else if (data !== null && typeof data === "object") {
1672
+ bodyBuffer = Buffer.from(new URLSearchParams(data).toString(), "utf8");
1589
1673
  mergedHeaders = mergedHeaders || {};
1590
1674
  if (!mergedHeaders["Content-Type"]) {
1591
1675
  mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1592
1676
  }
1593
- }
1594
- // Handle Buffer body
1595
- else if (Buffer.isBuffer(body)) {
1596
- body = body.toString("utf8");
1677
+ } else if (Buffer.isBuffer(body)) {
1678
+ bodyBuffer = body;
1679
+ } else if (typeof body === "string") {
1680
+ bodyBuffer = Buffer.from(body, "utf8");
1597
1681
  }
1598
1682
 
1599
1683
  // Use request auth if provided, otherwise fall back to session auth
@@ -1606,12 +1690,21 @@ class Session {
1606
1690
  if (mergedHeaders) {
1607
1691
  reqOptions.headers = mergedHeaders;
1608
1692
  }
1693
+ if (fetchMode) {
1694
+ reqOptions.fetch_mode = fetchMode;
1695
+ }
1609
1696
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1610
1697
 
1698
+ const bodyPtr = bodyBuffer || Buffer.alloc(0);
1699
+ const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
1700
+
1611
1701
  const startTime = Date.now();
1612
- const result = this._lib.httpcloak_post(this._handle, url, body, optionsJson);
1702
+ const responseHandle = this._lib.httpcloak_post_raw(this._handle, url, bodyPtr, bodyLen, optionsJson);
1613
1703
  const elapsed = Date.now() - startTime;
1614
- return parseResponse(result, elapsed);
1704
+ if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
1705
+ throw new HTTPCloakError("Request failed");
1706
+ }
1707
+ return parseRawResponse(this._lib, responseHandle, elapsed);
1615
1708
  }
1616
1709
 
1617
1710
  /**
@@ -1624,38 +1717,35 @@ class Session {
1624
1717
  * @returns {Response} Response object
1625
1718
  */
1626
1719
  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;
1720
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
1628
1721
 
1629
1722
  url = addParamsToUrl(url, params);
1630
1723
  let mergedHeaders = this._mergeHeaders(headers);
1631
1724
 
1632
- // Handle multipart file upload
1725
+ // Normalize body to a Buffer so the raw path can pass (ptr, len).
1726
+ let bodyBuffer = null;
1633
1727
  if (files !== null) {
1634
1728
  const formData = (data !== null && typeof data === "object") ? data : null;
1635
1729
  const multipart = encodeMultipart(formData, files);
1636
- body = multipart.body.toString("latin1"); // Preserve binary data
1730
+ bodyBuffer = multipart.body;
1637
1731
  mergedHeaders = mergedHeaders || {};
1638
1732
  mergedHeaders["Content-Type"] = multipart.contentType;
1639
- }
1640
- // Handle JSON body
1641
- else if (json !== null) {
1642
- body = JSON.stringify(json);
1733
+ } else if (json !== null) {
1734
+ bodyBuffer = Buffer.from(JSON.stringify(json), "utf8");
1643
1735
  mergedHeaders = mergedHeaders || {};
1644
1736
  if (!mergedHeaders["Content-Type"]) {
1645
1737
  mergedHeaders["Content-Type"] = "application/json";
1646
1738
  }
1647
- }
1648
- // Handle form data
1649
- else if (data !== null && typeof data === "object") {
1650
- body = new URLSearchParams(data).toString();
1739
+ } else if (data !== null && typeof data === "object") {
1740
+ bodyBuffer = Buffer.from(new URLSearchParams(data).toString(), "utf8");
1651
1741
  mergedHeaders = mergedHeaders || {};
1652
1742
  if (!mergedHeaders["Content-Type"]) {
1653
1743
  mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1654
1744
  }
1655
- }
1656
- // Handle Buffer body
1657
- else if (Buffer.isBuffer(body)) {
1658
- body = body.toString("utf8");
1745
+ } else if (Buffer.isBuffer(body)) {
1746
+ bodyBuffer = body;
1747
+ } else if (typeof body === "string") {
1748
+ bodyBuffer = Buffer.from(body, "utf8");
1659
1749
  }
1660
1750
 
1661
1751
  // Use request auth if provided, otherwise fall back to session auth
@@ -1668,16 +1758,24 @@ class Session {
1668
1758
  url,
1669
1759
  };
1670
1760
  if (mergedHeaders) requestConfig.headers = mergedHeaders;
1671
- if (body) requestConfig.body = body;
1672
1761
  if (timeout) requestConfig.timeout = timeout;
1762
+ if (fetchMode) requestConfig.fetch_mode = fetchMode;
1763
+
1764
+ const bodyPtr = bodyBuffer || Buffer.alloc(0);
1765
+ const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
1673
1766
 
1674
1767
  const startTime = Date.now();
1675
- const result = this._lib.httpcloak_request(
1768
+ const responseHandle = this._lib.httpcloak_request_raw(
1676
1769
  this._handle,
1677
- JSON.stringify(requestConfig)
1770
+ JSON.stringify(requestConfig),
1771
+ bodyPtr,
1772
+ bodyLen
1678
1773
  );
1679
1774
  const elapsed = Date.now() - startTime;
1680
- return parseResponse(result, elapsed);
1775
+ if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
1776
+ throw new HTTPCloakError("Request failed");
1777
+ }
1778
+ return parseRawResponse(this._lib, responseHandle, elapsed);
1681
1779
  }
1682
1780
 
1683
1781
  // ===========================================================================
@@ -1692,7 +1790,7 @@ class Session {
1692
1790
  * @returns {Promise<Response>} Response object
1693
1791
  */
1694
1792
  get(url, options = {}) {
1695
- const { headers = null, params = null, cookies = null, auth = null } = options;
1793
+ const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
1696
1794
 
1697
1795
  url = addParamsToUrl(url, params);
1698
1796
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1706,6 +1804,15 @@ class Session {
1706
1804
  if (mergedHeaders) {
1707
1805
  reqOptions.headers = mergedHeaders;
1708
1806
  }
1807
+ if (fetchMode) {
1808
+ reqOptions.fetch_mode = fetchMode;
1809
+ }
1810
+ if (timeout) {
1811
+ // Public API: seconds (matches Session({ timeout }) and the
1812
+ // existing request()/put()/delete()/etc. methods which forward to
1813
+ // httpcloak_request_async — that path also uses time.Second).
1814
+ reqOptions.timeout = timeout;
1815
+ }
1709
1816
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1710
1817
 
1711
1818
  // Register async request with callback manager
@@ -1726,7 +1833,7 @@ class Session {
1726
1833
  * @returns {Promise<Response>} Response object
1727
1834
  */
1728
1835
  post(url, options = {}) {
1729
- let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
1836
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
1730
1837
 
1731
1838
  url = addParamsToUrl(url, params);
1732
1839
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1770,6 +1877,13 @@ class Session {
1770
1877
  if (mergedHeaders) {
1771
1878
  reqOptions.headers = mergedHeaders;
1772
1879
  }
1880
+ if (fetchMode) {
1881
+ reqOptions.fetch_mode = fetchMode;
1882
+ }
1883
+ if (timeout) {
1884
+ // Public API: seconds. clib post_async path enforces in seconds.
1885
+ reqOptions.timeout = timeout;
1886
+ }
1773
1887
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1774
1888
 
1775
1889
  // Register async request with callback manager
@@ -1791,7 +1905,7 @@ class Session {
1791
1905
  * @returns {Promise<Response>} Response object
1792
1906
  */
1793
1907
  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;
1908
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
1795
1909
 
1796
1910
  url = addParamsToUrl(url, params);
1797
1911
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1837,6 +1951,7 @@ class Session {
1837
1951
  if (mergedHeaders) requestConfig.headers = mergedHeaders;
1838
1952
  if (body) requestConfig.body = body;
1839
1953
  if (timeout) requestConfig.timeout = timeout;
1954
+ if (fetchMode) requestConfig.fetch_mode = fetchMode;
1840
1955
 
1841
1956
  // Register async request with callback manager
1842
1957
  const manager = getAsyncManager();
@@ -1902,25 +2017,15 @@ class Session {
1902
2017
  }
1903
2018
 
1904
2019
  /**
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
2020
+ * Get all cookies from the session with full metadata.
2021
+ *
2022
+ * Returns Cookie[] with domain/path/expiry/flags. For the older flat
2023
+ * name->value object, build it yourself:
2024
+ * `Object.fromEntries(session.getCookies().map(c => [c.name, c.value]))`.
2025
+ * @returns {Cookie[]}
1909
2026
  */
1910
2027
  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;
2028
+ return this.getCookiesDetailed();
1924
2029
  }
1925
2030
 
1926
2031
  /**
@@ -1934,22 +2039,15 @@ class Session {
1934
2039
  }
1935
2040
 
1936
2041
  /**
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.
2042
+ * Get a specific cookie by name.
2043
+ *
2044
+ * Returns a Cookie object (with domain/path/expiry/flags) or null. For just
2045
+ * the string value: `const c = session.getCookie('foo'); const v = c?.value`.
1940
2046
  * @param {string} name - Cookie name
1941
- * @returns {string|null} Cookie value or null if not found
2047
+ * @returns {Cookie|null}
1942
2048
  */
1943
2049
  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;
2050
+ return this.getCookieDetailed(name);
1953
2051
  }
1954
2052
 
1955
2053
  /**
@@ -2817,8 +2915,8 @@ let _defaultConfig = {};
2817
2915
  * @param {boolean} [options.verify=true] - SSL certificate verification
2818
2916
  * @param {boolean} [options.allowRedirects=true] - Follow redirects
2819
2917
  * @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
2918
+ * @param {number} [options.retry=0] - Number of retries on failure (default 0 = no retries; set positive to enable. See issue #57)
2919
+ * @param {number[]} [options.retryOnStatus] - Status codes to retry on (only consulted when retry > 0)
2822
2920
  */
2823
2921
  function configure(options = {}) {
2824
2922
  const {
@@ -2831,7 +2929,7 @@ function configure(options = {}) {
2831
2929
  verify = true,
2832
2930
  allowRedirects = true,
2833
2931
  maxRedirects = 10,
2834
- retry = 3,
2932
+ retry = 0,
2835
2933
  retryOnStatus = null,
2836
2934
  } = options;
2837
2935
 
@@ -2887,7 +2985,7 @@ function _getDefaultSession() {
2887
2985
  const verify = _defaultConfig.verify !== undefined ? _defaultConfig.verify : true;
2888
2986
  const allowRedirects = _defaultConfig.allowRedirects !== undefined ? _defaultConfig.allowRedirects : true;
2889
2987
  const maxRedirects = _defaultConfig.maxRedirects || 10;
2890
- const retry = _defaultConfig.retry !== undefined ? _defaultConfig.retry : 3;
2988
+ const retry = _defaultConfig.retry !== undefined ? _defaultConfig.retry : 0;
2891
2989
  const retryOnStatus = _defaultConfig.retryOnStatus || null;
2892
2990
  const headers = _defaultConfig.headers || {};
2893
2991
 
@@ -3534,10 +3632,247 @@ function clearSessionCache() {
3534
3632
  }
3535
3633
 
3536
3634
 
3635
+ // --- String Memory Management for Preset/Pool Functions ---
3636
+
3637
+ /**
3638
+ * Read a C string from a void pointer and free the native memory.
3639
+ * Returns null if the pointer is null/0.
3640
+ * @param {*} ptr - Native void pointer
3641
+ * @returns {string|null}
3642
+ */
3643
+ function readAndFreeString(ptr) {
3644
+ if (!ptr) return null;
3645
+ const lib = getLib();
3646
+ try {
3647
+ return koffi.decode(ptr, "char", -1);
3648
+ } finally {
3649
+ lib.httpcloak_free_string(ptr);
3650
+ }
3651
+ }
3652
+
3653
+ // --- Custom Preset Loading ---
3654
+
3655
+ /**
3656
+ * Load a custom preset from a JSON file and register it.
3657
+ * @param {string} filePath - Path to the preset JSON file
3658
+ * @returns {string} The registered preset name
3659
+ */
3660
+ function loadPreset(filePath) {
3661
+ const lib = getLib();
3662
+ const result = readAndFreeString(lib.httpcloak_preset_load_file(filePath));
3663
+ if (!result) {
3664
+ throw new HTTPCloakError("Failed to load preset from file");
3665
+ }
3666
+ const data = JSON.parse(result);
3667
+ if (data.error) {
3668
+ throw new HTTPCloakError(data.error);
3669
+ }
3670
+ return data.name;
3671
+ }
3672
+
3673
+ /**
3674
+ * Load a custom preset from a JSON string and register it.
3675
+ * @param {string} jsonData - JSON string defining the preset
3676
+ * @returns {string} The registered preset name
3677
+ */
3678
+ function loadPresetFromJSON(jsonData) {
3679
+ const lib = getLib();
3680
+ const result = readAndFreeString(lib.httpcloak_preset_load_json(jsonData));
3681
+ if (!result) {
3682
+ throw new HTTPCloakError("Failed to load preset from JSON");
3683
+ }
3684
+ const data = JSON.parse(result);
3685
+ if (data.error) {
3686
+ throw new HTTPCloakError(data.error);
3687
+ }
3688
+ return data.name;
3689
+ }
3690
+
3691
+ /**
3692
+ * Unregister a custom preset by name.
3693
+ * @param {string} name - The preset name to unregister
3694
+ */
3695
+ function unregisterPreset(name) {
3696
+ const lib = getLib();
3697
+ lib.httpcloak_preset_unregister(name);
3698
+ }
3699
+
3700
+ /**
3701
+ * Return a fully-resolved JSON document for the given preset.
3702
+ *
3703
+ * The output flattens any inheritance chain and resolves H2/H3 defaults
3704
+ * (Chrome unless the preset overrides) into explicit values, so the
3705
+ * result is suitable for saving, editing, and reloading via
3706
+ * loadPresetFromJSON. Two consecutive calls return byte-identical JSON.
3707
+ *
3708
+ * @param {string} name - The preset name (built-in or custom-registered).
3709
+ * @returns {string} JSON string of the form {"version":1,"preset":{...}}.
3710
+ * @throws {HTTPCloakError} If the preset is not registered or references
3711
+ * an unknown utls ClientHelloID.
3712
+ */
3713
+ function describePreset(name) {
3714
+ const lib = getLib();
3715
+ const result = lib.httpcloak_describe_preset(name);
3716
+ if (!result) {
3717
+ throw new HTTPCloakError(`Failed to describe preset: ${name}`);
3718
+ }
3719
+ // The error envelope ({"error": "..."}) is also a valid JSON document.
3720
+ // A successful describe always carries a top-level "preset" field, so
3721
+ // distinguishing the two is straightforward.
3722
+ let parsed;
3723
+ try {
3724
+ parsed = JSON.parse(result);
3725
+ } catch (err) {
3726
+ throw new HTTPCloakError(`Invalid describe_preset response: ${err.message}`);
3727
+ }
3728
+ if (parsed && parsed.error && !parsed.preset) {
3729
+ throw new HTTPCloakError(parsed.error);
3730
+ }
3731
+ return result;
3732
+ }
3733
+
3734
+ /**
3735
+ * A pool of custom fingerprint presets for rotation.
3736
+ *
3737
+ * Pools load multiple presets from a single JSON file and provide
3738
+ * round-robin or random selection. All presets are auto-registered
3739
+ * on construction, so you can pass the returned name directly to
3740
+ * Session({ preset: name }).
3741
+ */
3742
+ class PresetPool {
3743
+ /**
3744
+ * Load a preset pool from a JSON file.
3745
+ * @param {string} filePath - Path to the pool JSON file
3746
+ */
3747
+ constructor(filePath) {
3748
+ this._lib = getLib();
3749
+ const result = readAndFreeString(this._lib.httpcloak_pool_load_file(filePath));
3750
+ if (!result) {
3751
+ throw new HTTPCloakError(`Failed to load preset pool from file: ${filePath}`);
3752
+ }
3753
+ const data = JSON.parse(result);
3754
+ if (data.error) {
3755
+ throw new HTTPCloakError(data.error);
3756
+ }
3757
+ this._handle = BigInt(data.handle);
3758
+ }
3759
+
3760
+ /**
3761
+ * Load a preset pool from a JSON string.
3762
+ * @param {string} jsonData - JSON string defining the pool
3763
+ * @returns {PresetPool}
3764
+ */
3765
+ static fromJSON(jsonData) {
3766
+ const pool = Object.create(PresetPool.prototype);
3767
+ pool._lib = getLib();
3768
+ const result = readAndFreeString(pool._lib.httpcloak_pool_load_json(jsonData));
3769
+ if (!result) {
3770
+ throw new HTTPCloakError("Failed to load preset pool from JSON");
3771
+ }
3772
+ const data = JSON.parse(result);
3773
+ if (data.error) {
3774
+ throw new HTTPCloakError(data.error);
3775
+ }
3776
+ pool._handle = BigInt(data.handle);
3777
+ return pool;
3778
+ }
3779
+
3780
+ /**
3781
+ * Pick a preset using the pool's configured strategy.
3782
+ * @returns {string} Preset name
3783
+ */
3784
+ pick() {
3785
+ this._checkHandle();
3786
+ return this._parsePoolResult(this._lib.httpcloak_pool_pick(this._handle));
3787
+ }
3788
+
3789
+ /**
3790
+ * Pick a random preset from the pool.
3791
+ * @returns {string} Preset name
3792
+ */
3793
+ random() {
3794
+ this._checkHandle();
3795
+ return this._parsePoolResult(this._lib.httpcloak_pool_random(this._handle));
3796
+ }
3797
+
3798
+ /**
3799
+ * Pick the next preset in round-robin order.
3800
+ * @returns {string} Preset name
3801
+ */
3802
+ next() {
3803
+ this._checkHandle();
3804
+ return this._parsePoolResult(this._lib.httpcloak_pool_next(this._handle));
3805
+ }
3806
+
3807
+ /**
3808
+ * Get a preset by index.
3809
+ * @param {number} index - Zero-based index
3810
+ * @returns {string} Preset name
3811
+ */
3812
+ get(index) {
3813
+ this._checkHandle();
3814
+ return this._parsePoolResult(this._lib.httpcloak_pool_get(this._handle, index));
3815
+ }
3816
+
3817
+ /**
3818
+ * Number of presets in the pool.
3819
+ * @type {number}
3820
+ */
3821
+ get size() {
3822
+ this._checkHandle();
3823
+ const s = this._lib.httpcloak_pool_size(this._handle);
3824
+ if (s < 0) {
3825
+ throw new HTTPCloakError("Failed to get pool size");
3826
+ }
3827
+ return Number(s);
3828
+ }
3829
+
3830
+ /**
3831
+ * Name of the preset pool.
3832
+ * @type {string}
3833
+ */
3834
+ get name() {
3835
+ this._checkHandle();
3836
+ return this._parsePoolResult(this._lib.httpcloak_pool_name(this._handle));
3837
+ }
3838
+
3839
+ /**
3840
+ * Free the pool handle and unregister all its presets.
3841
+ */
3842
+ close() {
3843
+ if (this._handle && this._handle !== 0n && this._handle !== 0) {
3844
+ this._lib.httpcloak_pool_free(this._handle);
3845
+ this._handle = 0n;
3846
+ }
3847
+ }
3848
+
3849
+ _parsePoolResult(resultPtr) {
3850
+ const result = readAndFreeString(resultPtr);
3851
+ if (!result) {
3852
+ throw new HTTPCloakError("No result from preset pool");
3853
+ }
3854
+ // Error responses are JSON with {"error":"..."}
3855
+ if (result.startsWith("{")) {
3856
+ const data = JSON.parse(result);
3857
+ if (data.error) {
3858
+ throw new HTTPCloakError(data.error);
3859
+ }
3860
+ }
3861
+ return result;
3862
+ }
3863
+
3864
+ _checkHandle() {
3865
+ if (!this._handle || this._handle === 0n || this._handle === 0) {
3866
+ throw new HTTPCloakError("PresetPool is closed");
3867
+ }
3868
+ }
3869
+ }
3870
+
3537
3871
  module.exports = {
3538
3872
  // Classes
3539
3873
  Session,
3540
3874
  LocalProxy,
3875
+ PresetPool,
3541
3876
  Response,
3542
3877
  FastResponse,
3543
3878
  StreamResponse,
@@ -3547,6 +3882,11 @@ module.exports = {
3547
3882
  SessionCacheBackend,
3548
3883
  // Presets
3549
3884
  Preset,
3885
+ // Custom preset loading
3886
+ loadPreset,
3887
+ loadPresetFromJSON,
3888
+ unregisterPreset,
3889
+ describePreset,
3550
3890
  // Configuration
3551
3891
  configure,
3552
3892
  configureSessionCache,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/darwin-arm64",
3
- "version": "1.6.1",
3
+ "version": "1.6.6",
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.6",
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.6",
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.6",
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-x64",
3
- "version": "1.6.1",
3
+ "version": "1.6.6",
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.6",
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,11 @@
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.6",
53
+ "@httpcloak/darwin-x64": "1.6.6",
54
+ "@httpcloak/linux-arm64": "1.6.6",
55
+ "@httpcloak/linux-x64": "1.6.6",
56
+ "@httpcloak/win32-x64": "1.6.6"
58
57
  },
59
58
  "devDependencies": {
60
59
  "@types/node": "^25.1.0",
@@ -8,13 +8,17 @@ const path = require("path");
8
8
 
9
9
  const VERSION = "1.4.0";
10
10
 
11
+ // Platforms we actually build and publish via CI's npm-platform matrix.
12
+ // Keep in sync with .github/workflows/bindings.yml's `npm_platform` matrix —
13
+ // adding a row here without adding it to the matrix means npm install will
14
+ // fail with an unresolvable optional dependency on yarn classic / pnpm
15
+ // strict modes.
11
16
  const PLATFORMS = [
12
17
  { name: "linux-x64", os: "linux", cpu: "x64", libName: "libhttpcloak-linux-amd64.so" },
13
18
  { name: "linux-arm64", os: "linux", cpu: "arm64", libName: "libhttpcloak-linux-arm64.so" },
14
19
  { name: "darwin-x64", os: "darwin", cpu: "x64", libName: "libhttpcloak-darwin-amd64.dylib" },
15
20
  { name: "darwin-arm64", os: "darwin", cpu: "arm64", libName: "libhttpcloak-darwin-arm64.dylib" },
16
21
  { name: "win32-x64", os: "win32", cpu: "x64", libName: "libhttpcloak-windows-amd64.dll" },
17
- { name: "win32-arm64", os: "win32", cpu: "arm64", libName: "libhttpcloak-windows-arm64.dll" },
18
22
  ];
19
23
 
20
24
  const npmDir = path.join(__dirname, "..", "npm");
@@ -1,3 +0,0 @@
1
- // Auto-generated - exports path to native library
2
- const path = require("path");
3
- module.exports = path.join(__dirname, "libhttpcloak-windows-arm64.dll");
@@ -1,6 +0,0 @@
1
- // Auto-generated - exports path to native library (ESM)
2
- import { fileURLToPath } from "url";
3
- import { dirname, join } from "path";
4
-
5
- const __dirname = dirname(fileURLToPath(import.meta.url));
6
- export default join(__dirname, "libhttpcloak-windows-arm64.dll");
@@ -1,27 +0,0 @@
1
- {
2
- "name": "@httpcloak/win32-arm64",
3
- "version": "1.6.1",
4
- "description": "HTTPCloak native binary for win32 arm64",
5
- "os": [
6
- "win32"
7
- ],
8
- "cpu": [
9
- "arm64"
10
- ],
11
- "main": "lib.js",
12
- "module": "lib.mjs",
13
- "exports": {
14
- ".": {
15
- "import": "./lib.mjs",
16
- "require": "./lib.js"
17
- }
18
- },
19
- "license": "MIT",
20
- "repository": {
21
- "type": "git",
22
- "url": "https://github.com/sardanioss/httpcloak"
23
- },
24
- "publishConfig": {
25
- "access": "public"
26
- }
27
- }