httpcloak 1.6.6 → 1.6.8-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -21,91 +21,143 @@ class HTTPCloakError extends Error {
21
21
  }
22
22
 
23
23
  /**
24
- * Available browser presets for TLS fingerprinting.
24
+ * Available browser presets for TLS fingerprinting. Mirrors the runtime
25
+ * registry shipped by the linked libhttpcloak. Use Preset.CHROME_LATEST to
26
+ * auto-track newest stable Chrome, or pin to a specific major version for
27
+ * reproducibility.
25
28
  *
26
- * Use these constants instead of typing preset strings manually:
27
29
  * const httpcloak = require("httpcloak");
28
- * httpcloak.configure({ preset: httpcloak.Preset.CHROME_143 });
30
+ * httpcloak.configure({ preset: httpcloak.Preset.CHROME_LATEST });
29
31
  *
30
- * // Or with Session
31
- * const session = new httpcloak.Session({ preset: httpcloak.Preset.FIREFOX_133 });
32
+ * const session = new httpcloak.Session({ preset: httpcloak.Preset.FIREFOX_LATEST });
32
33
  */
33
34
  const Preset = {
34
- // Chrome 146 (latest)
35
+ // Chrome latest (auto-resolves to newest shipped Chrome)
36
+ CHROME_LATEST: "chrome-latest",
37
+ CHROME_LATEST_WINDOWS: "chrome-latest-windows",
38
+ CHROME_LATEST_LINUX: "chrome-latest-linux",
39
+ CHROME_LATEST_MACOS: "chrome-latest-macos",
40
+ CHROME_LATEST_IOS: "chrome-latest-ios",
41
+ CHROME_LATEST_ANDROID: "chrome-latest-android",
42
+
43
+ // Chrome 149 (desktop; wire fingerprint identical to 148)
44
+ CHROME_149: "chrome-149",
45
+ CHROME_149_WINDOWS: "chrome-149-windows",
46
+ CHROME_149_LINUX: "chrome-149-linux",
47
+ CHROME_149_MACOS: "chrome-149-macos",
48
+
49
+ // Chrome 148
50
+ CHROME_148: "chrome-148",
51
+ CHROME_148_WINDOWS: "chrome-148-windows",
52
+ CHROME_148_LINUX: "chrome-148-linux",
53
+ CHROME_148_MACOS: "chrome-148-macos",
54
+ CHROME_148_IOS: "chrome-148-ios",
55
+ CHROME_148_ANDROID: "chrome-148-android",
56
+
57
+ // Chrome 147
58
+ CHROME_147: "chrome-147",
59
+ CHROME_147_WINDOWS: "chrome-147-windows",
60
+ CHROME_147_LINUX: "chrome-147-linux",
61
+ CHROME_147_MACOS: "chrome-147-macos",
62
+ CHROME_147_IOS: "chrome-147-ios",
63
+ CHROME_147_ANDROID: "chrome-147-android",
64
+
65
+ // Chrome 146
35
66
  CHROME_146: "chrome-146",
36
67
  CHROME_146_WINDOWS: "chrome-146-windows",
37
68
  CHROME_146_LINUX: "chrome-146-linux",
38
69
  CHROME_146_MACOS: "chrome-146-macos",
70
+ CHROME_146_IOS: "chrome-146-ios",
71
+ CHROME_146_ANDROID: "chrome-146-android",
39
72
 
40
73
  // Chrome 145
41
74
  CHROME_145: "chrome-145",
42
75
  CHROME_145_WINDOWS: "chrome-145-windows",
43
76
  CHROME_145_LINUX: "chrome-145-linux",
44
77
  CHROME_145_MACOS: "chrome-145-macos",
78
+ CHROME_145_IOS: "chrome-145-ios",
79
+ CHROME_145_ANDROID: "chrome-145-android",
45
80
 
46
81
  // Chrome 144
47
82
  CHROME_144: "chrome-144",
48
83
  CHROME_144_WINDOWS: "chrome-144-windows",
49
84
  CHROME_144_LINUX: "chrome-144-linux",
50
85
  CHROME_144_MACOS: "chrome-144-macos",
86
+ CHROME_144_IOS: "chrome-144-ios",
87
+ CHROME_144_ANDROID: "chrome-144-android",
51
88
 
52
89
  // Chrome 143
53
90
  CHROME_143: "chrome-143",
54
91
  CHROME_143_WINDOWS: "chrome-143-windows",
55
92
  CHROME_143_LINUX: "chrome-143-linux",
56
93
  CHROME_143_MACOS: "chrome-143-macos",
94
+ CHROME_143_IOS: "chrome-143-ios",
95
+ CHROME_143_ANDROID: "chrome-143-android",
57
96
 
58
- // Chrome 141
97
+ // Older Chrome (H1/H2 only, no H3)
59
98
  CHROME_141: "chrome-141",
60
-
61
- // Chrome 133
62
99
  CHROME_133: "chrome-133",
63
100
 
64
- // Mobile Chrome
65
- CHROME_143_IOS: "chrome-143-ios",
66
- CHROME_144_IOS: "chrome-144-ios",
67
- CHROME_145_IOS: "chrome-145-ios",
68
- CHROME_146_IOS: "chrome-146-ios",
69
- CHROME_143_ANDROID: "chrome-143-android",
70
- CHROME_144_ANDROID: "chrome-144-android",
71
- CHROME_145_ANDROID: "chrome-145-android",
72
- CHROME_146_ANDROID: "chrome-146-android",
73
-
74
101
  // Firefox
102
+ FIREFOX_LATEST: "firefox-latest",
103
+ FIREFOX_148: "firefox-148",
75
104
  FIREFOX_133: "firefox-133",
76
105
 
77
106
  // Safari (desktop and mobile)
107
+ SAFARI_LATEST: "safari-latest",
78
108
  SAFARI_18: "safari-18",
79
109
  SAFARI_17_IOS: "safari-17-ios",
80
110
  SAFARI_18_IOS: "safari-18-ios",
111
+ SAFARI_LATEST_IOS: "safari-latest-ios",
81
112
 
82
- // Backwards compatibility aliases (old naming convention)
113
+ // Backwards compatibility aliases (old "ios-chrome" / "android-chrome" naming)
83
114
  IOS_CHROME_143: "chrome-143-ios",
84
115
  IOS_CHROME_144: "chrome-144-ios",
85
116
  IOS_CHROME_145: "chrome-145-ios",
86
117
  IOS_CHROME_146: "chrome-146-ios",
118
+ IOS_CHROME_147: "chrome-147-ios",
119
+ IOS_CHROME_148: "chrome-148-ios",
120
+ IOS_CHROME_LATEST: "chrome-latest-ios",
87
121
  ANDROID_CHROME_143: "chrome-143-android",
88
122
  ANDROID_CHROME_144: "chrome-144-android",
89
123
  ANDROID_CHROME_145: "chrome-145-android",
90
124
  ANDROID_CHROME_146: "chrome-146-android",
125
+ ANDROID_CHROME_147: "chrome-147-android",
126
+ ANDROID_CHROME_148: "chrome-148-android",
127
+ ANDROID_CHROME_LATEST: "chrome-latest-android",
91
128
  IOS_SAFARI_17: "safari-17-ios",
92
129
  IOS_SAFARI_18: "safari-18-ios",
130
+ IOS_SAFARI_LATEST: "safari-latest-ios",
93
131
 
94
132
  /**
95
- * Get all available preset names
96
- * @returns {string[]} List of all preset names
133
+ * Get all built-in preset names known to this binding version.
134
+ *
135
+ * For the authoritative live list (which may include custom presets
136
+ * loaded at runtime), call `availablePresets()` instead.
137
+ *
138
+ * @returns {string[]} List of preset names
97
139
  */
98
140
  all() {
99
141
  return [
142
+ this.CHROME_LATEST, this.CHROME_LATEST_WINDOWS, this.CHROME_LATEST_LINUX,
143
+ this.CHROME_LATEST_MACOS, this.CHROME_LATEST_IOS, this.CHROME_LATEST_ANDROID,
144
+ this.CHROME_149, this.CHROME_149_WINDOWS, this.CHROME_149_LINUX, this.CHROME_149_MACOS,
145
+ this.CHROME_148, this.CHROME_148_WINDOWS, this.CHROME_148_LINUX, this.CHROME_148_MACOS,
146
+ this.CHROME_148_IOS, this.CHROME_148_ANDROID,
147
+ this.CHROME_147, this.CHROME_147_WINDOWS, this.CHROME_147_LINUX, this.CHROME_147_MACOS,
148
+ this.CHROME_147_IOS, this.CHROME_147_ANDROID,
100
149
  this.CHROME_146, this.CHROME_146_WINDOWS, this.CHROME_146_LINUX, this.CHROME_146_MACOS,
150
+ this.CHROME_146_IOS, this.CHROME_146_ANDROID,
101
151
  this.CHROME_145, this.CHROME_145_WINDOWS, this.CHROME_145_LINUX, this.CHROME_145_MACOS,
152
+ this.CHROME_145_IOS, this.CHROME_145_ANDROID,
102
153
  this.CHROME_144, this.CHROME_144_WINDOWS, this.CHROME_144_LINUX, this.CHROME_144_MACOS,
154
+ this.CHROME_144_IOS, this.CHROME_144_ANDROID,
103
155
  this.CHROME_143, this.CHROME_143_WINDOWS, this.CHROME_143_LINUX, this.CHROME_143_MACOS,
156
+ this.CHROME_143_IOS, this.CHROME_143_ANDROID,
104
157
  this.CHROME_141, this.CHROME_133,
105
- this.CHROME_146_IOS, this.CHROME_145_IOS, this.CHROME_144_IOS, this.CHROME_143_IOS,
106
- this.CHROME_146_ANDROID, this.CHROME_145_ANDROID, this.CHROME_144_ANDROID, this.CHROME_143_ANDROID,
107
- this.FIREFOX_133,
108
- this.SAFARI_18, this.SAFARI_17_IOS, this.SAFARI_18_IOS,
158
+ this.FIREFOX_LATEST, this.FIREFOX_148, this.FIREFOX_133,
159
+ this.SAFARI_LATEST, this.SAFARI_18, this.SAFARI_17_IOS, this.SAFARI_18_IOS,
160
+ this.SAFARI_LATEST_IOS,
109
161
  ];
110
162
  },
111
163
  };
@@ -830,6 +882,22 @@ function getLib() {
830
882
  httpcloak_set_cookie: nativeLibHandle.func("httpcloak_set_cookie", "void", ["int64", "str"]),
831
883
  httpcloak_delete_cookie: nativeLibHandle.func("httpcloak_delete_cookie", "void", ["int64", "str", "str"]),
832
884
  httpcloak_clear_cookies: nativeLibHandle.func("httpcloak_clear_cookies", "void", ["int64"]),
885
+ httpcloak_session_clear_cache: nativeLibHandle.func("httpcloak_session_clear_cache", "void", ["int64"]),
886
+ httpcloak_session_stats: nativeLibHandle.func("httpcloak_session_stats", HeapStr, ["int64"]),
887
+ httpcloak_session_idle_time: nativeLibHandle.func("httpcloak_session_idle_time", "int64", ["int64"]),
888
+ httpcloak_session_is_active: nativeLibHandle.func("httpcloak_session_is_active", "int", ["int64"]),
889
+ httpcloak_session_touch: nativeLibHandle.func("httpcloak_session_touch", "void", ["int64"]),
890
+ httpcloak_session_set_conditional_cache: nativeLibHandle.func("httpcloak_session_set_conditional_cache", "void", ["int64", "int"]),
891
+ httpcloak_session_get_conditional_cache: nativeLibHandle.func("httpcloak_session_get_conditional_cache", "int", ["int64"]),
892
+ httpcloak_session_set_client_hints: nativeLibHandle.func("httpcloak_session_set_client_hints", "void", ["int64", "int"]),
893
+ httpcloak_session_get_client_hints: nativeLibHandle.func("httpcloak_session_get_client_hints", "int", ["int64"]),
894
+ httpcloak_session_set_high_entropy_client_hints: nativeLibHandle.func("httpcloak_session_set_high_entropy_client_hints", "void", ["int64", "int"]),
895
+ httpcloak_session_get_high_entropy_client_hints: nativeLibHandle.func("httpcloak_session_get_high_entropy_client_hints", "int", ["int64"]),
896
+ httpcloak_session_set_follow_redirects: nativeLibHandle.func("httpcloak_session_set_follow_redirects", "void", ["int64", "int"]),
897
+ httpcloak_session_get_follow_redirects: nativeLibHandle.func("httpcloak_session_get_follow_redirects", "int", ["int64"]),
898
+ httpcloak_session_set_max_redirects: nativeLibHandle.func("httpcloak_session_set_max_redirects", "void", ["int64", "int"]),
899
+ httpcloak_session_get_max_redirects: nativeLibHandle.func("httpcloak_session_get_max_redirects", "int", ["int64"]),
900
+ httpcloak_session_set_identifier: nativeLibHandle.func("httpcloak_session_set_identifier", "void", ["int64", "str"]),
833
901
  httpcloak_free_string: httpcloakFreeString,
834
902
  httpcloak_version: nativeLibHandle.func("httpcloak_version", HeapStr, []),
835
903
  httpcloak_available_presets: nativeLibHandle.func("httpcloak_available_presets", HeapStr, []),
@@ -841,6 +909,7 @@ function getLib() {
841
909
  httpcloak_get_async: nativeLibHandle.func("httpcloak_get_async", "void", ["int64", "str", "str", "int64"]),
842
910
  httpcloak_post_async: nativeLibHandle.func("httpcloak_post_async", "void", ["int64", "str", "str", "str", "int64"]),
843
911
  httpcloak_request_async: nativeLibHandle.func("httpcloak_request_async", "void", ["int64", "str", "int64"]),
912
+ httpcloak_cancel_request: nativeLibHandle.func("httpcloak_cancel_request", "void", ["int64"]),
844
913
  // Streaming functions
845
914
  httpcloak_stream_get: nativeLibHandle.func("httpcloak_stream_get", "int64", ["int64", "str", "str"]),
846
915
  httpcloak_stream_post: nativeLibHandle.func("httpcloak_stream_post", "int64", ["int64", "str", "str", "str"]),
@@ -848,6 +917,15 @@ function getLib() {
848
917
  httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata", HeapStr, ["int64"]),
849
918
  httpcloak_stream_read: nativeLibHandle.func("httpcloak_stream_read", HeapStr, ["int64", "int64"]),
850
919
  httpcloak_stream_close: nativeLibHandle.func("httpcloak_stream_close", "void", ["int64"]),
920
+ // Chunked upload state machine (mirrors Python's stream_upload). Lets
921
+ // bindings ship arbitrarily-large bodies without buffering in memory:
922
+ // upload_start opens a pipe to the Go side, upload_write_raw streams
923
+ // chunks straight to the wire, upload_finish reads the response,
924
+ // upload_cancel tears down a partial upload on error.
925
+ httpcloak_upload_start: nativeLibHandle.func("httpcloak_upload_start", "int64", ["int64", "str", "str"]),
926
+ httpcloak_upload_write_raw: nativeLibHandle.func("httpcloak_upload_write_raw", "int", ["int64", "void*", "int"]),
927
+ httpcloak_upload_finish: nativeLibHandle.func("httpcloak_upload_finish", HeapStr, ["int64"]),
928
+ httpcloak_upload_cancel: nativeLibHandle.func("httpcloak_upload_cancel", "void", ["int64"]),
851
929
  // Raw response functions for fast-path (zero-copy)
852
930
  httpcloak_get_raw: nativeLibHandle.func("httpcloak_get_raw", "int64", ["int64", "str", "str"]),
853
931
  httpcloak_post_raw: nativeLibHandle.func("httpcloak_post_raw", "int64", ["int64", "str", "void*", "int", "str"]),
@@ -881,6 +959,8 @@ function getLib() {
881
959
  httpcloak_local_proxy_get_stats: nativeLibHandle.func("httpcloak_local_proxy_get_stats", HeapStr, ["int64"]),
882
960
  httpcloak_local_proxy_register_session: nativeLibHandle.func("httpcloak_local_proxy_register_session", HeapStr, ["int64", "str", "int64"]),
883
961
  httpcloak_local_proxy_unregister_session: nativeLibHandle.func("httpcloak_local_proxy_unregister_session", "int", ["int64", "str"]),
962
+ httpcloak_local_proxy_list_sessions: nativeLibHandle.func("httpcloak_local_proxy_list_sessions", HeapStr, ["int64"]),
963
+ httpcloak_local_proxy_has_session: nativeLibHandle.func("httpcloak_local_proxy_has_session", "int", ["int64", "str"]),
884
964
  // Session cache callbacks
885
965
  httpcloak_set_session_cache_callbacks: nativeLibHandle.func("httpcloak_set_session_cache_callbacks", "void", [
886
966
  koffi.pointer(SessionCacheGetProto),
@@ -1010,7 +1090,7 @@ class AsyncCallbackManager {
1010
1090
  * Register a new async request
1011
1091
  * @returns {{ callbackId: number, promise: Promise<Response> }}
1012
1092
  */
1013
- registerRequest(nativeLib) {
1093
+ registerRequest(nativeLib, signal) {
1014
1094
  this._ensureCallback();
1015
1095
 
1016
1096
  // Register callback with Go (each request gets unique ID)
@@ -1027,6 +1107,43 @@ class AsyncCallbackManager {
1027
1107
  this._pendingRequests.set(Number(callbackId), { resolve, reject, startTime });
1028
1108
  this._ref(); // Keep event loop alive
1029
1109
 
1110
+ // AbortSignal integration. If the caller supplies a signal that is
1111
+ // already aborted, cancel synchronously; otherwise install a listener
1112
+ // that fires once. Pre-aborted signals never deliver the "abort" event,
1113
+ // so the explicit early-path matters.
1114
+ //
1115
+ // Order matters in onAbort:
1116
+ // 1) cancel_request → unblocks the Go goroutine (ctx.Done fires)
1117
+ // 2) unregister_callback → removes the entry from Go's asyncCallbacks
1118
+ // map so the goroutine's final invokeCallback() finds !exists and
1119
+ // returns silently. Without this, Go would still relay an error
1120
+ // callback through koffi after the JS side has already settled
1121
+ // and torn down its callback context, which crashes node with
1122
+ // "Error::ThrowAsJavaScriptException napi_throw" during exit.
1123
+ if (signal && typeof signal === "object" && "aborted" in signal) {
1124
+ const onAbort = () => {
1125
+ const cid = Number(callbackId);
1126
+ try {
1127
+ nativeLib.httpcloak_cancel_request(cid);
1128
+ nativeLib.httpcloak_unregister_callback(cid);
1129
+ } catch {
1130
+ // Best-effort: the request may have already settled.
1131
+ }
1132
+ const pending = this._pendingRequests.get(cid);
1133
+ if (pending) {
1134
+ this._pendingRequests.delete(cid);
1135
+ this._unref();
1136
+ const reason = signal.reason ?? new Error("AbortError");
1137
+ pending.reject(reason);
1138
+ }
1139
+ };
1140
+ if (signal.aborted) {
1141
+ onAbort();
1142
+ } else if (typeof signal.addEventListener === "function") {
1143
+ signal.addEventListener("abort", onAbort, { once: true });
1144
+ }
1145
+ }
1146
+
1030
1147
  return { callbackId, promise };
1031
1148
  }
1032
1149
  }
@@ -1362,6 +1479,11 @@ class Session {
1362
1479
  enableSpeculativeTls = false,
1363
1480
  switchProtocol = null,
1364
1481
  withoutCookieJar = false,
1482
+ withoutConditionalCache = false,
1483
+ withoutClientHints = false,
1484
+ withoutHighEntropyClientHints = false,
1485
+ disableEch = false,
1486
+ disableHttp3 = false,
1365
1487
  ja3 = null,
1366
1488
  akamai = null,
1367
1489
  extraFp = null,
@@ -1439,6 +1561,23 @@ class Session {
1439
1561
  if (withoutCookieJar) {
1440
1562
  config.without_cookie_jar = true;
1441
1563
  }
1564
+ if (withoutConditionalCache) {
1565
+ config.without_conditional_cache = true;
1566
+ }
1567
+ // Full strip: drop ALL sec-ch-ua client hints, including the always-on trio.
1568
+ if (withoutClientHints) {
1569
+ config.without_client_hints = true;
1570
+ }
1571
+ // High-entropy strip: keep the always-on trio, drop only the Accept-CH hints.
1572
+ if (withoutHighEntropyClientHints) {
1573
+ config.without_high_entropy_client_hints = true;
1574
+ }
1575
+ if (disableEch) {
1576
+ config.disable_ech = true;
1577
+ }
1578
+ if (disableHttp3) {
1579
+ config.disable_http3 = true;
1580
+ }
1442
1581
  if (ja3) {
1443
1582
  config.ja3 = ja3;
1444
1583
  }
@@ -1602,7 +1741,7 @@ class Session {
1602
1741
  * @returns {Response} Response object
1603
1742
  */
1604
1743
  getSync(url, options = {}) {
1605
- const { headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
1744
+ const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false } = options;
1606
1745
 
1607
1746
  url = addParamsToUrl(url, params);
1608
1747
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1619,6 +1758,18 @@ class Session {
1619
1758
  if (fetchMode) {
1620
1759
  reqOptions.fetch_mode = fetchMode;
1621
1760
  }
1761
+ if (allowRedirects !== null && allowRedirects !== undefined) {
1762
+ reqOptions.follow_redirects = !!allowRedirects;
1763
+ }
1764
+ if (disableConditionalCache) {
1765
+ reqOptions.disable_conditional_cache = true;
1766
+ }
1767
+ if (disableClientHints) {
1768
+ reqOptions.disable_client_hints = true;
1769
+ }
1770
+ if (disableHighEntropyClientHints) {
1771
+ reqOptions.disable_high_entropy_client_hints = true;
1772
+ }
1622
1773
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1623
1774
 
1624
1775
  const startTime = Date.now();
@@ -1648,7 +1799,7 @@ class Session {
1648
1799
  * @returns {Response} Response object
1649
1800
  */
1650
1801
  postSync(url, options = {}) {
1651
- let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
1802
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false } = options;
1652
1803
 
1653
1804
  url = addParamsToUrl(url, params);
1654
1805
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1693,6 +1844,18 @@ class Session {
1693
1844
  if (fetchMode) {
1694
1845
  reqOptions.fetch_mode = fetchMode;
1695
1846
  }
1847
+ if (allowRedirects !== null && allowRedirects !== undefined) {
1848
+ reqOptions.follow_redirects = !!allowRedirects;
1849
+ }
1850
+ if (disableConditionalCache) {
1851
+ reqOptions.disable_conditional_cache = true;
1852
+ }
1853
+ if (disableClientHints) {
1854
+ reqOptions.disable_client_hints = true;
1855
+ }
1856
+ if (disableHighEntropyClientHints) {
1857
+ reqOptions.disable_high_entropy_client_hints = true;
1858
+ }
1696
1859
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1697
1860
 
1698
1861
  const bodyPtr = bodyBuffer || Buffer.alloc(0);
@@ -1717,7 +1880,7 @@ class Session {
1717
1880
  * @returns {Response} Response object
1718
1881
  */
1719
1882
  requestSync(method, url, options = {}) {
1720
- let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
1883
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false } = options;
1721
1884
 
1722
1885
  url = addParamsToUrl(url, params);
1723
1886
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1760,6 +1923,10 @@ class Session {
1760
1923
  if (mergedHeaders) requestConfig.headers = mergedHeaders;
1761
1924
  if (timeout) requestConfig.timeout = timeout;
1762
1925
  if (fetchMode) requestConfig.fetch_mode = fetchMode;
1926
+ if (allowRedirects !== null && allowRedirects !== undefined) requestConfig.follow_redirects = !!allowRedirects;
1927
+ if (disableConditionalCache) requestConfig.disable_conditional_cache = true;
1928
+ if (disableClientHints) requestConfig.disable_client_hints = true;
1929
+ if (disableHighEntropyClientHints) requestConfig.disable_high_entropy_client_hints = true;
1763
1930
 
1764
1931
  const bodyPtr = bodyBuffer || Buffer.alloc(0);
1765
1932
  const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
@@ -1790,7 +1957,7 @@ class Session {
1790
1957
  * @returns {Promise<Response>} Response object
1791
1958
  */
1792
1959
  get(url, options = {}) {
1793
- const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
1960
+ const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false, signal = null } = options;
1794
1961
 
1795
1962
  url = addParamsToUrl(url, params);
1796
1963
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1813,11 +1980,23 @@ class Session {
1813
1980
  // httpcloak_request_async — that path also uses time.Second).
1814
1981
  reqOptions.timeout = timeout;
1815
1982
  }
1983
+ if (allowRedirects !== null && allowRedirects !== undefined) {
1984
+ reqOptions.follow_redirects = !!allowRedirects;
1985
+ }
1986
+ if (disableConditionalCache) {
1987
+ reqOptions.disable_conditional_cache = true;
1988
+ }
1989
+ if (disableClientHints) {
1990
+ reqOptions.disable_client_hints = true;
1991
+ }
1992
+ if (disableHighEntropyClientHints) {
1993
+ reqOptions.disable_high_entropy_client_hints = true;
1994
+ }
1816
1995
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1817
1996
 
1818
1997
  // Register async request with callback manager
1819
1998
  const manager = getAsyncManager();
1820
- const { callbackId, promise } = manager.registerRequest(this._lib);
1999
+ const { callbackId, promise } = manager.registerRequest(this._lib, signal);
1821
2000
 
1822
2001
  // Start async request
1823
2002
  this._lib.httpcloak_get_async(this._handle, url, optionsJson, callbackId);
@@ -1833,16 +2012,23 @@ class Session {
1833
2012
  * @returns {Promise<Response>} Response object
1834
2013
  */
1835
2014
  post(url, options = {}) {
1836
- let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
2015
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false, signal = null } = options;
1837
2016
 
1838
2017
  url = addParamsToUrl(url, params);
1839
2018
  let mergedHeaders = this._mergeHeaders(headers);
1840
2019
 
2020
+ // bodyEncoding tells clib whether `body` is plain text or base64-encoded
2021
+ // binary. Default "" = text. Anything that carries arbitrary bytes
2022
+ // (Buffer, multipart) flows through as base64 so NUL bytes don't truncate
2023
+ // the C string at the cgo boundary.
2024
+ let bodyEncoding = "";
2025
+
1841
2026
  // Handle multipart file upload
1842
2027
  if (files !== null) {
1843
2028
  const formData = (data !== null && typeof data === "object") ? data : null;
1844
2029
  const multipart = encodeMultipart(formData, files);
1845
- body = multipart.body.toString("latin1");
2030
+ body = multipart.body.toString("base64");
2031
+ bodyEncoding = "base64";
1846
2032
  mergedHeaders = mergedHeaders || {};
1847
2033
  mergedHeaders["Content-Type"] = multipart.contentType;
1848
2034
  }
@@ -1862,9 +2048,11 @@ class Session {
1862
2048
  mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1863
2049
  }
1864
2050
  }
1865
- // Handle Buffer body
2051
+ // Handle Buffer body (binary payload). base64-encode so NUL bytes and
2052
+ // non-UTF-8 sequences survive the cgo C-string round-trip intact.
1866
2053
  else if (Buffer.isBuffer(body)) {
1867
- body = body.toString("utf8");
2054
+ body = body.toString("base64");
2055
+ bodyEncoding = "base64";
1868
2056
  }
1869
2057
 
1870
2058
  // Use request auth if provided, otherwise fall back to session auth
@@ -1884,11 +2072,26 @@ class Session {
1884
2072
  // Public API: seconds. clib post_async path enforces in seconds.
1885
2073
  reqOptions.timeout = timeout;
1886
2074
  }
2075
+ if (allowRedirects !== null && allowRedirects !== undefined) {
2076
+ reqOptions.follow_redirects = !!allowRedirects;
2077
+ }
2078
+ if (disableConditionalCache) {
2079
+ reqOptions.disable_conditional_cache = true;
2080
+ }
2081
+ if (disableClientHints) {
2082
+ reqOptions.disable_client_hints = true;
2083
+ }
2084
+ if (disableHighEntropyClientHints) {
2085
+ reqOptions.disable_high_entropy_client_hints = true;
2086
+ }
2087
+ if (bodyEncoding) {
2088
+ reqOptions.body_encoding = bodyEncoding;
2089
+ }
1887
2090
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1888
2091
 
1889
2092
  // Register async request with callback manager
1890
2093
  const manager = getAsyncManager();
1891
- const { callbackId, promise } = manager.registerRequest(this._lib);
2094
+ const { callbackId, promise } = manager.registerRequest(this._lib, signal);
1892
2095
 
1893
2096
  // Start async request
1894
2097
  this._lib.httpcloak_post_async(this._handle, url, body, optionsJson, callbackId);
@@ -1905,16 +2108,22 @@ class Session {
1905
2108
  * @returns {Promise<Response>} Response object
1906
2109
  */
1907
2110
  request(method, url, options = {}) {
1908
- let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
2111
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false, signal = null } = options;
1909
2112
 
1910
2113
  url = addParamsToUrl(url, params);
1911
2114
  let mergedHeaders = this._mergeHeaders(headers);
1912
2115
 
2116
+ // bodyEncoding tells clib whether `body` is text or base64 binary.
2117
+ // Binary payloads (Buffer / multipart) flow through as base64 so they
2118
+ // survive JSON serialization + the cgo C-string boundary intact.
2119
+ let bodyEncoding = "";
2120
+
1913
2121
  // Handle multipart file upload
1914
2122
  if (files !== null) {
1915
2123
  const formData = (data !== null && typeof data === "object") ? data : null;
1916
2124
  const multipart = encodeMultipart(formData, files);
1917
- body = multipart.body.toString("latin1");
2125
+ body = multipart.body.toString("base64");
2126
+ bodyEncoding = "base64";
1918
2127
  mergedHeaders = mergedHeaders || {};
1919
2128
  mergedHeaders["Content-Type"] = multipart.contentType;
1920
2129
  }
@@ -1934,9 +2143,11 @@ class Session {
1934
2143
  mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1935
2144
  }
1936
2145
  }
1937
- // Handle Buffer body
2146
+ // Handle Buffer body (binary payload). base64-encode so non-UTF-8 bytes
2147
+ // and NUL bytes survive JSON.stringify + the C boundary.
1938
2148
  else if (Buffer.isBuffer(body)) {
1939
- body = body.toString("utf8");
2149
+ body = body.toString("base64");
2150
+ bodyEncoding = "base64";
1940
2151
  }
1941
2152
 
1942
2153
  // Use request auth if provided, otherwise fall back to session auth
@@ -1950,12 +2161,17 @@ class Session {
1950
2161
  };
1951
2162
  if (mergedHeaders) requestConfig.headers = mergedHeaders;
1952
2163
  if (body) requestConfig.body = body;
2164
+ if (bodyEncoding) requestConfig.body_encoding = bodyEncoding;
1953
2165
  if (timeout) requestConfig.timeout = timeout;
1954
2166
  if (fetchMode) requestConfig.fetch_mode = fetchMode;
2167
+ if (allowRedirects !== null && allowRedirects !== undefined) requestConfig.follow_redirects = !!allowRedirects;
2168
+ if (disableConditionalCache) requestConfig.disable_conditional_cache = true;
2169
+ if (disableClientHints) requestConfig.disable_client_hints = true;
2170
+ if (disableHighEntropyClientHints) requestConfig.disable_high_entropy_client_hints = true;
1955
2171
 
1956
2172
  // Register async request with callback manager
1957
2173
  const manager = getAsyncManager();
1958
- const { callbackId, promise } = manager.registerRequest(this._lib);
2174
+ const { callbackId, promise } = manager.registerRequest(this._lib, signal);
1959
2175
 
1960
2176
  // Start async request
1961
2177
  this._lib.httpcloak_request_async(this._handle, JSON.stringify(requestConfig), callbackId);
@@ -2103,6 +2319,160 @@ class Session {
2103
2319
  return this.getCookies();
2104
2320
  }
2105
2321
 
2322
+ // ===========================================================================
2323
+ // Conditional Cache and Redirect Runtime Control
2324
+ // ===========================================================================
2325
+
2326
+ /**
2327
+ * Drop the session's per-URL conditional-cache map (ETag / Last-Modified).
2328
+ * The next request to each URL goes out without If-None-Match /
2329
+ * If-Modified-Since headers. Cookies and TLS tickets are not touched.
2330
+ */
2331
+ clearCache() {
2332
+ this._lib.httpcloak_session_clear_cache(this._handle);
2333
+ }
2334
+
2335
+ /**
2336
+ * Return a snapshot of session counters, timestamps and transport-level
2337
+ * metrics. Useful for long-running scrapers that want per-session metrics
2338
+ * scraped into Prometheus / Datadog / etc.
2339
+ *
2340
+ * Keys: id, preset, created_at (Unix ns), last_used (Unix ns),
2341
+ * request_count, active, cookie_count, cache_entry_count, age_ns,
2342
+ * idle_time_ns, transport_stats (per-protocol object).
2343
+ *
2344
+ * @returns {Object} Stats snapshot, or empty object if the session is closed.
2345
+ */
2346
+ stats() {
2347
+ const ptr = this._lib.httpcloak_session_stats(this._handle);
2348
+ const raw = resultToString(ptr);
2349
+ if (!raw) return {};
2350
+ try { return JSON.parse(raw); } catch { return {}; }
2351
+ }
2352
+
2353
+ /**
2354
+ * Return the time since the session last serviced a request, in seconds.
2355
+ * Returns -1 if the session handle is invalid.
2356
+ *
2357
+ * @returns {number} Idle time in seconds (may be fractional).
2358
+ */
2359
+ idleTime() {
2360
+ const ns = Number(this._lib.httpcloak_session_idle_time(this._handle));
2361
+ if (ns < 0) return -1;
2362
+ return ns / 1_000_000_000;
2363
+ }
2364
+
2365
+ /**
2366
+ * Return true if the session is still usable (close() has not been called
2367
+ * and the handle is valid).
2368
+ *
2369
+ * @returns {boolean}
2370
+ */
2371
+ isActive() {
2372
+ return this._lib.httpcloak_session_is_active(this._handle) === 1;
2373
+ }
2374
+
2375
+ /**
2376
+ * Reset the idle timer to now without issuing a request. Useful in
2377
+ * long-running pools where an external heartbeat shouldn't let a session
2378
+ * look idle to a reaper.
2379
+ */
2380
+ touch() {
2381
+ this._lib.httpcloak_session_touch(this._handle);
2382
+ }
2383
+
2384
+ /**
2385
+ * Toggle the session's ETag / If-Modified-Since handling at runtime.
2386
+ * When disabled, the session stops injecting cache validators on outgoing
2387
+ * requests and stops storing them from responses; the existing cache map
2388
+ * is preserved (re-enabling resumes using it). Pair with clearCache() to
2389
+ * also wipe previously-stored validators.
2390
+ * @param {boolean} enabled
2391
+ */
2392
+ setConditionalCache(enabled) {
2393
+ this._lib.httpcloak_session_set_conditional_cache(this._handle, enabled ? 1 : 0);
2394
+ }
2395
+
2396
+ /**
2397
+ * Return the session's current conditional-cache state.
2398
+ * @returns {boolean}
2399
+ */
2400
+ getConditionalCache() {
2401
+ return this._lib.httpcloak_session_get_conditional_cache(this._handle) !== 0;
2402
+ }
2403
+
2404
+ /**
2405
+ * Toggle the session's client-hints handling at runtime (full strip).
2406
+ * When disabled, the session drops ALL sec-ch-ua client hints, including
2407
+ * the always-on trio (sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform).
2408
+ * The change takes effect on the next request and persists until set again.
2409
+ * @param {boolean} enabled
2410
+ */
2411
+ setClientHints(enabled) {
2412
+ this._lib.httpcloak_session_set_client_hints(this._handle, enabled ? 1 : 0);
2413
+ }
2414
+
2415
+ /**
2416
+ * Return the session's current client-hints state.
2417
+ * @returns {boolean}
2418
+ */
2419
+ getClientHints() {
2420
+ return this._lib.httpcloak_session_get_client_hints(this._handle) !== 0;
2421
+ }
2422
+
2423
+ /**
2424
+ * Toggle the session's high-entropy client-hints handling at runtime.
2425
+ * When disabled, the session keeps the always-on trio but drops only the
2426
+ * high-entropy Accept-CH hints (e.g. sec-ch-ua-platform-version, -arch,
2427
+ * -model, -bitness, -full-version-list). Takes effect on the next request.
2428
+ * @param {boolean} enabled
2429
+ */
2430
+ setHighEntropyClientHints(enabled) {
2431
+ this._lib.httpcloak_session_set_high_entropy_client_hints(this._handle, enabled ? 1 : 0);
2432
+ }
2433
+
2434
+ /**
2435
+ * Return the session's current high-entropy client-hints state.
2436
+ * @returns {boolean}
2437
+ */
2438
+ getHighEntropyClientHints() {
2439
+ return this._lib.httpcloak_session_get_high_entropy_client_hints(this._handle) !== 0;
2440
+ }
2441
+
2442
+ /**
2443
+ * Toggle the session's redirect-following policy at runtime. The change
2444
+ * takes effect on the next request and persists until set again.
2445
+ * @param {boolean} enabled
2446
+ */
2447
+ setFollowRedirects(enabled) {
2448
+ this._lib.httpcloak_session_set_follow_redirects(this._handle, enabled ? 1 : 0);
2449
+ }
2450
+
2451
+ /**
2452
+ * Return the session's current redirect-following policy.
2453
+ * @returns {boolean}
2454
+ */
2455
+ getFollowRedirects() {
2456
+ return this._lib.httpcloak_session_get_follow_redirects(this._handle) !== 0;
2457
+ }
2458
+
2459
+ /**
2460
+ * Update the session's redirect cap at runtime. Values of zero or below
2461
+ * are ignored, leaving the prior cap (or the default of 10) in place.
2462
+ * @param {number} max
2463
+ */
2464
+ setMaxRedirects(max) {
2465
+ this._lib.httpcloak_session_set_max_redirects(this._handle, max);
2466
+ }
2467
+
2468
+ /**
2469
+ * Return the session's current redirect cap.
2470
+ * @returns {number}
2471
+ */
2472
+ getMaxRedirects() {
2473
+ return this._lib.httpcloak_session_get_max_redirects(this._handle);
2474
+ }
2475
+
2106
2476
  // ===========================================================================
2107
2477
  // Proxy Management
2108
2478
  // ===========================================================================
@@ -2399,7 +2769,7 @@ class Session {
2399
2769
  * stream.close();
2400
2770
  */
2401
2771
  getStream(url, options = {}) {
2402
- const { params, headers, cookies, timeout } = options;
2772
+ const { params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false } = options;
2403
2773
 
2404
2774
  // Add params to URL
2405
2775
  if (params) {
@@ -2426,6 +2796,18 @@ class Session {
2426
2796
  if (timeout) {
2427
2797
  reqOptions.timeout = timeout;
2428
2798
  }
2799
+ if (allowRedirects !== null && allowRedirects !== undefined) {
2800
+ reqOptions.follow_redirects = !!allowRedirects;
2801
+ }
2802
+ if (disableConditionalCache) {
2803
+ reqOptions.disable_conditional_cache = true;
2804
+ }
2805
+ if (disableClientHints) {
2806
+ reqOptions.disable_client_hints = true;
2807
+ }
2808
+ if (disableHighEntropyClientHints) {
2809
+ reqOptions.disable_high_entropy_client_hints = true;
2810
+ }
2429
2811
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
2430
2812
 
2431
2813
  // Start stream
@@ -2465,7 +2847,7 @@ class Session {
2465
2847
  * @returns {StreamResponse} - Streaming response for chunked reading
2466
2848
  */
2467
2849
  postStream(url, options = {}) {
2468
- const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout } = options;
2850
+ const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false } = options;
2469
2851
 
2470
2852
  // Add params to URL
2471
2853
  if (params) {
@@ -2505,6 +2887,18 @@ class Session {
2505
2887
  if (timeout) {
2506
2888
  reqOptions.timeout = timeout;
2507
2889
  }
2890
+ if (allowRedirects !== null && allowRedirects !== undefined) {
2891
+ reqOptions.follow_redirects = !!allowRedirects;
2892
+ }
2893
+ if (disableConditionalCache) {
2894
+ reqOptions.disable_conditional_cache = true;
2895
+ }
2896
+ if (disableClientHints) {
2897
+ reqOptions.disable_client_hints = true;
2898
+ }
2899
+ if (disableHighEntropyClientHints) {
2900
+ reqOptions.disable_high_entropy_client_hints = true;
2901
+ }
2508
2902
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
2509
2903
 
2510
2904
  // Start stream
@@ -2543,7 +2937,7 @@ class Session {
2543
2937
  * @returns {StreamResponse} - Streaming response for chunked reading
2544
2938
  */
2545
2939
  requestStream(method, url, options = {}) {
2546
- const { body, params, headers, cookies, timeout } = options;
2940
+ const { body, params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false } = options;
2547
2941
 
2548
2942
  // Add params to URL
2549
2943
  if (params) {
@@ -2576,6 +2970,18 @@ class Session {
2576
2970
  if (timeout) {
2577
2971
  requestConfig.timeout = timeout;
2578
2972
  }
2973
+ if (allowRedirects !== null && allowRedirects !== undefined) {
2974
+ requestConfig.follow_redirects = !!allowRedirects;
2975
+ }
2976
+ if (disableConditionalCache) {
2977
+ requestConfig.disable_conditional_cache = true;
2978
+ }
2979
+ if (disableClientHints) {
2980
+ requestConfig.disable_client_hints = true;
2981
+ }
2982
+ if (disableHighEntropyClientHints) {
2983
+ requestConfig.disable_high_entropy_client_hints = true;
2984
+ }
2579
2985
 
2580
2986
  // Start stream
2581
2987
  const streamHandle = this._lib.httpcloak_stream_request(this._handle, JSON.stringify(requestConfig));
@@ -2894,6 +3300,84 @@ class Session {
2894
3300
  patchFast(url, options = {}) {
2895
3301
  return this.requestFast("PATCH", url, options);
2896
3302
  }
3303
+
3304
+ /**
3305
+ * Stream an arbitrary-sized body to the wire without buffering in memory.
3306
+ *
3307
+ * `chunks` is anything iterable that yields Buffer-shaped objects: an
3308
+ * AsyncIterable&lt;Buffer&gt; (e.g. an `fs.createReadStream(path)`), an
3309
+ * Iterable&lt;Buffer&gt; (e.g. an array), or a sync generator that yields
3310
+ * Buffers. String chunks are auto-encoded as UTF-8.
3311
+ *
3312
+ * The Go side opens an `io.Pipe()` for the body when uploadStart returns;
3313
+ * each chunk is written straight through with no base64 / no JSON wrapping.
3314
+ * On error the partial upload is cancelled and the underlying connection
3315
+ * closed; on success uploadFinish reads the full response and parses it
3316
+ * into a regular `Response` (same shape as a normal post()).
3317
+ *
3318
+ * @param {string} method HTTP method (POST / PUT / PATCH typically)
3319
+ * @param {string} url
3320
+ * @param {AsyncIterable&lt;Buffer&gt;|Iterable&lt;Buffer&gt;} chunks Body chunks
3321
+ * @param {Object} [options]
3322
+ * @param {Object} [options.headers]
3323
+ * @param {string} [options.contentType="application/octet-stream"]
3324
+ * @param {number} [options.timeout] Per-request timeout in ms
3325
+ * @returns {Promise<Response>}
3326
+ */
3327
+ async uploadStream(method, url, chunks, options = {}) {
3328
+ const { headers = null, contentType = "application/octet-stream", timeout = null } = options;
3329
+ const mergedHeaders = this._mergeHeaders(headers) || {};
3330
+ const optsJson = JSON.stringify({
3331
+ method: method.toUpperCase(),
3332
+ headers: mergedHeaders,
3333
+ content_type: contentType,
3334
+ ...(timeout ? { timeout } : {}),
3335
+ });
3336
+
3337
+ const uploadHandle = this._lib.httpcloak_upload_start(this._handle, url, optsJson);
3338
+ if (uploadHandle <= 0) {
3339
+ throw new HTTPCloakError("Failed to start streaming upload");
3340
+ }
3341
+
3342
+ const startTime = Date.now();
3343
+ try {
3344
+ for await (const chunk of chunks) {
3345
+ const buf = Buffer.isBuffer(chunk)
3346
+ ? chunk
3347
+ : typeof chunk === "string"
3348
+ ? Buffer.from(chunk, "utf8")
3349
+ : Buffer.from(chunk);
3350
+ if (buf.length === 0) continue;
3351
+ const written = this._lib.httpcloak_upload_write_raw(uploadHandle, buf, buf.length);
3352
+ if (written < 0) {
3353
+ throw new HTTPCloakError("Failed to write upload chunk");
3354
+ }
3355
+ }
3356
+
3357
+ const resultPtr = this._lib.httpcloak_upload_finish(uploadHandle);
3358
+ const elapsed = Date.now() - startTime;
3359
+ const raw = resultToString(resultPtr);
3360
+ if (!raw) {
3361
+ throw new HTTPCloakError("Empty response from streaming upload");
3362
+ }
3363
+ const data = JSON.parse(raw);
3364
+ if (data.error) {
3365
+ throw new HTTPCloakError(data.error);
3366
+ }
3367
+ return new Response(data, elapsed);
3368
+ } catch (err) {
3369
+ try { this._lib.httpcloak_upload_cancel(uploadHandle); } catch { /* best-effort */ }
3370
+ throw err;
3371
+ }
3372
+ }
3373
+
3374
+ /**
3375
+ * Convenience wrapper: streaming POST. Same semantics as
3376
+ * {@link uploadStream}('POST', ...).
3377
+ */
3378
+ postUpload(url, chunks, options = {}) {
3379
+ return this.uploadStream("POST", url, chunks, options);
3380
+ }
2897
3381
  }
2898
3382
 
2899
3383
  // =============================================================================
@@ -3227,6 +3711,41 @@ class LocalProxy {
3227
3711
  return result === 1;
3228
3712
  }
3229
3713
 
3714
+ /**
3715
+ * Return the IDs of every session currently registered on this proxy.
3716
+ *
3717
+ * These are the same IDs the X-HTTPCloak-Session header accepts for
3718
+ * per-request session routing. Useful for sanity checks, GC of stale
3719
+ * registrations in long-running processes, and operational dashboards.
3720
+ *
3721
+ * @returns {string[]} List of registered session IDs (empty if none).
3722
+ */
3723
+ listSessions() {
3724
+ const resultPtr = this._lib.httpcloak_local_proxy_list_sessions(this._handle);
3725
+ const raw = resultToString(resultPtr);
3726
+ if (!raw) return [];
3727
+ try {
3728
+ const parsed = JSON.parse(raw);
3729
+ return Array.isArray(parsed) ? parsed.map(String) : [];
3730
+ } catch {
3731
+ return [];
3732
+ }
3733
+ }
3734
+
3735
+ /**
3736
+ * Return true if a session with the given ID is currently registered.
3737
+ *
3738
+ * Cheaper than listSessions() + .includes() when callers only need an
3739
+ * existence check.
3740
+ *
3741
+ * @param {string} sessionId
3742
+ * @returns {boolean}
3743
+ */
3744
+ hasSession(sessionId) {
3745
+ if (!sessionId) return false;
3746
+ return this._lib.httpcloak_local_proxy_has_session(this._handle, sessionId) === 1;
3747
+ }
3748
+
3230
3749
  /**
3231
3750
  * Stop the local proxy server.
3232
3751
  */