httpcloak 1.5.9 → 1.6.0-beta.4

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/README.md CHANGED
@@ -291,10 +291,10 @@ Connect to one server while requesting a different domain:
291
291
  ```javascript
292
292
  const { Session } = require("httpcloak");
293
293
 
294
- // Connect to claude.ai's IP but request www.cloudflare.com
294
+ // Connect to example.com's IP but request www.cloudflare.com
295
295
  const session = new Session({
296
296
  preset: "chrome-143",
297
- connectTo: { "www.cloudflare.com": "claude.ai" },
297
+ connectTo: { "www.cloudflare.com": "example.com" },
298
298
  });
299
299
 
300
300
  const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
@@ -371,8 +371,8 @@ const session = new Session({
371
371
  const { availablePresets } = require("httpcloak");
372
372
 
373
373
  console.log(availablePresets());
374
- // ['chrome-143', 'chrome-143-windows', 'chrome-143-linux', 'chrome-143-macos',
375
- // 'chrome-131', 'firefox-133', 'safari-18', ...]
374
+ // ['chrome-144', 'chrome-143', 'chrome-141', 'chrome-133',
375
+ // 'firefox-133', 'safari-18', 'ios-chrome-144', ...]
376
376
  ```
377
377
 
378
378
  ## Response Object
package/lib/index.d.ts CHANGED
@@ -11,6 +11,20 @@ export class Cookie {
11
11
  name: string;
12
12
  /** Cookie value */
13
13
  value: string;
14
+ /** Cookie domain */
15
+ domain: string;
16
+ /** Cookie path */
17
+ path: string;
18
+ /** Expiration date (RFC1123 format) */
19
+ expires: string;
20
+ /** Max age in seconds (0 means not set) */
21
+ maxAge: number;
22
+ /** Secure flag */
23
+ secure: boolean;
24
+ /** HttpOnly flag */
25
+ httpOnly: boolean;
26
+ /** SameSite attribute (Strict, Lax, None) */
27
+ sameSite: string;
14
28
  }
15
29
 
16
30
  export class RedirectInfo {
@@ -181,7 +195,7 @@ export class StreamResponse {
181
195
  }
182
196
 
183
197
  export interface SessionOptions {
184
- /** Browser preset to use (default: "chrome-143") */
198
+ /** Browser preset to use (default: "chrome-144") */
185
199
  preset?: string;
186
200
  /** Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port") */
187
201
  proxy?: string;
@@ -219,6 +233,10 @@ export interface SessionOptions {
219
233
  tlsOnly?: boolean;
220
234
  /** QUIC idle timeout in seconds (default: 30). Set higher for long-lived HTTP/3 connections. */
221
235
  quicIdleTimeout?: number;
236
+ /** Local IP address to bind outgoing connections (for IPv6 rotation with IP_FREEBIND on Linux) */
237
+ localAddress?: string;
238
+ /** Path to write TLS key log for Wireshark decryption (overrides SSLKEYLOGFILE env var) */
239
+ keyLogFile?: string;
222
240
  }
223
241
 
224
242
  export interface RequestOptions {
@@ -254,6 +272,12 @@ export class Session {
254
272
  /** Close the session and release resources */
255
273
  close(): void;
256
274
 
275
+ /** Refresh the session by closing all connections while keeping TLS session tickets.
276
+ * This simulates a browser page refresh - connections are severed but 0-RTT
277
+ * early data can be used on reconnection due to preserved session tickets.
278
+ */
279
+ refresh(): void;
280
+
257
281
  // Synchronous methods
258
282
  /** Perform a synchronous GET request */
259
283
  getSync(url: string, options?: RequestOptions): Response;
@@ -572,7 +596,7 @@ export class Session {
572
596
  export interface LocalProxyOptions {
573
597
  /** Port to listen on (default: 0 for auto-assign) */
574
598
  port?: number;
575
- /** Browser preset to use (default: "chrome-143") */
599
+ /** Browser preset to use (default: "chrome-144") */
576
600
  preset?: string;
577
601
  /** Request timeout in seconds (default: 30) */
578
602
  timeout?: number;
@@ -599,6 +623,38 @@ export interface LocalProxyStats {
599
623
  bytesReceived: number;
600
624
  }
601
625
 
626
+ /**
627
+ * Local HTTP proxy server that forwards requests through httpcloak with TLS fingerprinting.
628
+ * Use this to transparently apply fingerprinting to any HTTP client (e.g., Undici, fetch).
629
+ *
630
+ * Supports per-request proxy rotation via X-Upstream-Proxy header.
631
+ * Supports per-request session routing via X-HTTPCloak-Session header.
632
+ *
633
+ * IMPORTANT: For distributed session caching to work with X-HTTPCloak-Session header,
634
+ * you MUST register the session with the proxy using registerSession() first.
635
+ * Without registration, cache callbacks will not be triggered for that session.
636
+ *
637
+ * @example
638
+ * // Basic usage
639
+ * const proxy = new LocalProxy({ preset: "chrome-144", tlsOnly: true });
640
+ * console.log(`Proxy running on ${proxy.proxyUrl}`);
641
+ * // Use with any HTTP client pointing to the proxy
642
+ * proxy.close();
643
+ *
644
+ * @example
645
+ * // With distributed session cache
646
+ * const proxy = new LocalProxy({ port: 8888 });
647
+ * const session = new Session({ preset: 'chrome-144' });
648
+ *
649
+ * // Configure distributed cache
650
+ * httpcloak.configureSessionCache({
651
+ * get: async (key) => await redis.get(key),
652
+ * put: async (key, value, ttl) => { await redis.setex(key, ttl, value); return 0; },
653
+ * });
654
+ *
655
+ * // REQUIRED: Register session for cache callbacks to work
656
+ * proxy.registerSession('session-1', session);
657
+ */
602
658
  export class LocalProxy {
603
659
  /**
604
660
  * Create a new LocalProxy instance.
@@ -626,6 +682,9 @@ export class LocalProxy {
626
682
  * Register a session with an ID for use with X-HTTPCloak-Session header.
627
683
  * This allows per-request session routing through the proxy.
628
684
  *
685
+ * IMPORTANT: This is REQUIRED for distributed session caching to work.
686
+ * Without registration, cache callbacks will not be triggered for the session.
687
+ *
629
688
  * When a request is made through the proxy with the `X-HTTPCloak-Session: <sessionId>` header,
630
689
  * the proxy will use the registered session for that request, applying its TLS fingerprint
631
690
  * and cookies.
@@ -637,12 +696,13 @@ export class LocalProxy {
637
696
  * @example
638
697
  * ```typescript
639
698
  * const proxy = new LocalProxy({ port: 8888 });
640
- * const session = new Session({ preset: 'chrome-143' });
699
+ * const session = new Session({ preset: 'chrome-144' });
641
700
  *
642
- * // Register session with ID
701
+ * // Register session with ID (required for cache callbacks)
643
702
  * proxy.registerSession('user-1', session);
644
703
  *
645
704
  * // Now requests with X-HTTPCloak-Session: user-1 header will use this session
705
+ * // and trigger cache callbacks
646
706
  * ```
647
707
  */
648
708
  registerSession(sessionId: string, session: Session): void;
@@ -744,10 +804,13 @@ export const Preset: {
744
804
  CHROME_131_LINUX: string;
745
805
  CHROME_131_MACOS: string;
746
806
  IOS_CHROME_143: string;
807
+ IOS_CHROME_144: string;
747
808
  ANDROID_CHROME_143: string;
809
+ ANDROID_CHROME_144: string;
748
810
  FIREFOX_133: string;
749
811
  SAFARI_18: string;
750
812
  IOS_SAFARI_17: string;
813
+ IOS_SAFARI_18: string;
751
814
  all(): string[];
752
815
  };
753
816
 
@@ -759,42 +822,48 @@ export interface SessionCacheOptions {
759
822
  /**
760
823
  * Function to get session data from cache.
761
824
  * Returns JSON string with session data, or null if not found.
762
- * Note: Must be synchronous - async callbacks are not supported.
825
+ * Supports both sync and async callbacks.
763
826
  */
764
- get?: (key: string) => string | null;
827
+ get?: (key: string) => string | null | Promise<string | null>;
765
828
 
766
829
  /**
767
830
  * Function to store session data in cache.
768
831
  * Returns 0 on success, non-zero on error.
769
- * Note: Must be synchronous - async callbacks are not supported.
832
+ * Supports both sync and async callbacks.
770
833
  */
771
- put?: (key: string, value: string, ttlSeconds: number) => number;
834
+ put?: (key: string, value: string, ttlSeconds: number) => number | Promise<number>;
772
835
 
773
836
  /**
774
837
  * Function to delete session data from cache.
775
838
  * Returns 0 on success, non-zero on error.
776
- * Note: Must be synchronous - async callbacks are not supported.
839
+ * Supports both sync and async callbacks.
777
840
  */
778
- delete?: (key: string) => number;
841
+ delete?: (key: string) => number | Promise<number>;
779
842
 
780
843
  /**
781
844
  * Function to get ECH config from cache.
782
845
  * Returns base64-encoded config, or null if not found.
783
- * Note: Must be synchronous - async callbacks are not supported.
846
+ * Supports both sync and async callbacks.
784
847
  */
785
- getEch?: (key: string) => string | null;
848
+ getEch?: (key: string) => string | null | Promise<string | null>;
786
849
 
787
850
  /**
788
851
  * Function to store ECH config in cache.
789
852
  * Returns 0 on success, non-zero on error.
790
- * Note: Must be synchronous - async callbacks are not supported.
853
+ * Supports both sync and async callbacks.
791
854
  */
792
- putEch?: (key: string, value: string, ttlSeconds: number) => number;
855
+ putEch?: (key: string, value: string, ttlSeconds: number) => number | Promise<number>;
793
856
 
794
857
  /**
795
858
  * Error callback for cache operations.
796
859
  */
797
860
  onError?: (operation: string, key: string, error: string) => void;
861
+
862
+ /**
863
+ * Force async mode. If not specified, async mode is auto-detected
864
+ * based on whether any callback is an async function.
865
+ */
866
+ async?: boolean;
798
867
  }
799
868
 
800
869
  /**
@@ -803,13 +872,42 @@ export interface SessionCacheOptions {
803
872
  * Enables TLS session resumption across distributed httpcloak instances
804
873
  * by storing session tickets in an external cache like Redis or Memcached.
805
874
  *
875
+ * Supports both synchronous callbacks (for in-memory Map) and asynchronous
876
+ * callbacks (for Redis, database, etc.). Async mode is auto-detected when
877
+ * any callback is an async function.
878
+ *
806
879
  * Cache key formats:
807
880
  * - TLS sessions: httpcloak:sessions:{preset}:{protocol}:{host}:{port}
808
881
  * - ECH configs: httpcloak:ech:{preset}:{host}:{port}
882
+ *
883
+ * @example
884
+ * // Sync example with Map
885
+ * const cache = new Map();
886
+ * const backend = new SessionCacheBackend({
887
+ * get: (key) => cache.get(key) || null,
888
+ * put: (key, value, ttl) => { cache.set(key, value); return 0; },
889
+ * delete: (key) => { cache.delete(key); return 0; },
890
+ * });
891
+ * backend.register();
892
+ *
893
+ * @example
894
+ * // Async example with Redis
895
+ * const redis = new Redis();
896
+ * const backend = new SessionCacheBackend({
897
+ * get: async (key) => await redis.get(key),
898
+ * put: async (key, value, ttl) => { await redis.setex(key, ttl, value); return 0; },
899
+ * delete: async (key) => { await redis.del(key); return 0; },
900
+ * });
901
+ * backend.register();
809
902
  */
810
903
  export class SessionCacheBackend {
811
904
  constructor(options?: SessionCacheOptions);
812
905
 
906
+ /**
907
+ * Check if this backend is running in async mode.
908
+ */
909
+ readonly isAsync: boolean;
910
+
813
911
  /**
814
912
  * Register this cache backend globally.
815
913
  * After registration, all new Session and LocalProxy instances will use
@@ -827,8 +925,19 @@ export class SessionCacheBackend {
827
925
  /**
828
926
  * Configure a distributed session cache backend.
829
927
  *
928
+ * Supports both synchronous and asynchronous callbacks (auto-detected).
929
+ *
830
930
  * @param options Cache configuration
831
931
  * @returns The registered SessionCacheBackend instance
932
+ *
933
+ * @example
934
+ * // Using Redis with async callbacks
935
+ * const redis = new Redis();
936
+ * httpcloak.configureSessionCache({
937
+ * get: async (key) => await redis.get(key),
938
+ * put: async (key, value, ttl) => { await redis.setex(key, ttl, value); return 0; },
939
+ * delete: async (key) => { await redis.del(key); return 0; },
940
+ * });
832
941
  */
833
942
  export function configureSessionCache(options: SessionCacheOptions): SessionCacheBackend;
834
943
 
package/lib/index.js CHANGED
@@ -43,15 +43,11 @@ const Preset = {
43
43
  // Chrome 133
44
44
  CHROME_133: "chrome-133",
45
45
 
46
- // Chrome 131
47
- CHROME_131: "chrome-131",
48
- CHROME_131_WINDOWS: "chrome-131-windows",
49
- CHROME_131_LINUX: "chrome-131-linux",
50
- CHROME_131_MACOS: "chrome-131-macos",
51
-
52
46
  // Mobile Chrome
53
47
  IOS_CHROME_143: "ios-chrome-143",
48
+ IOS_CHROME_144: "ios-chrome-144",
54
49
  ANDROID_CHROME_143: "android-chrome-143",
50
+ ANDROID_CHROME_144: "android-chrome-144",
55
51
 
56
52
  // Firefox
57
53
  FIREFOX_133: "firefox-133",
@@ -59,6 +55,7 @@ const Preset = {
59
55
  // Safari (desktop and mobile)
60
56
  SAFARI_18: "safari-18",
61
57
  IOS_SAFARI_17: "ios-safari-17",
58
+ IOS_SAFARI_18: "ios-safari-18",
62
59
 
63
60
  /**
64
61
  * Get all available preset names
@@ -66,11 +63,12 @@ const Preset = {
66
63
  */
67
64
  all() {
68
65
  return [
66
+ this.CHROME_144, this.CHROME_144_WINDOWS, this.CHROME_144_LINUX, this.CHROME_144_MACOS,
69
67
  this.CHROME_143, this.CHROME_143_WINDOWS, this.CHROME_143_LINUX, this.CHROME_143_MACOS,
70
- this.CHROME_141, this.CHROME_133, this.CHROME_131,
71
- this.IOS_CHROME_143, this.ANDROID_CHROME_143,
68
+ this.CHROME_141, this.CHROME_133,
69
+ this.IOS_CHROME_143, this.IOS_CHROME_144, this.ANDROID_CHROME_143, this.ANDROID_CHROME_144,
72
70
  this.FIREFOX_133,
73
- this.SAFARI_18, this.IOS_SAFARI_17,
71
+ this.SAFARI_18, this.IOS_SAFARI_17, this.IOS_SAFARI_18,
74
72
  ];
75
73
  },
76
74
  };
@@ -105,16 +103,51 @@ const HTTP_STATUS_PHRASES = {
105
103
  */
106
104
  class Cookie {
107
105
  /**
108
- * @param {string} name - Cookie name
109
- * @param {string} value - Cookie value
110
- */
111
- constructor(name, value) {
112
- this.name = name;
113
- this.value = value;
106
+ * @param {Object} data - Cookie data from response
107
+ * @param {string} data.name - Cookie name
108
+ * @param {string} data.value - Cookie value
109
+ * @param {string} [data.domain] - Cookie domain
110
+ * @param {string} [data.path] - Cookie path
111
+ * @param {string} [data.expires] - Expiration date (RFC1123 format)
112
+ * @param {number} [data.max_age] - Max age in seconds
113
+ * @param {boolean} [data.secure] - Secure flag
114
+ * @param {boolean} [data.http_only] - HttpOnly flag
115
+ * @param {string} [data.same_site] - SameSite attribute (Strict, Lax, None)
116
+ */
117
+ constructor(data) {
118
+ if (typeof data === 'string') {
119
+ // Legacy: constructor(name, value)
120
+ this.name = data;
121
+ this.value = arguments[1] || "";
122
+ this.domain = "";
123
+ this.path = "";
124
+ this.expires = "";
125
+ this.maxAge = 0;
126
+ this.secure = false;
127
+ this.httpOnly = false;
128
+ this.sameSite = "";
129
+ } else {
130
+ this.name = data.name || "";
131
+ this.value = data.value || "";
132
+ this.domain = data.domain || "";
133
+ this.path = data.path || "";
134
+ this.expires = data.expires || "";
135
+ this.maxAge = data.max_age || 0;
136
+ this.secure = data.secure || false;
137
+ this.httpOnly = data.http_only || false;
138
+ this.sameSite = data.same_site || "";
139
+ }
114
140
  }
115
141
 
116
142
  toString() {
117
- return `Cookie(name=${this.name}, value=${this.value})`;
143
+ let str = `Cookie(name=${this.name}, value=${this.value}`;
144
+ if (this.domain) str += `, domain=${this.domain}`;
145
+ if (this.path) str += `, path=${this.path}`;
146
+ if (this.secure) str += `, secure`;
147
+ if (this.httpOnly) str += `, httpOnly`;
148
+ if (this.sameSite) str += `, sameSite=${this.sameSite}`;
149
+ str += `)`;
150
+ return str;
118
151
  }
119
152
  }
120
153
 
@@ -156,7 +189,7 @@ class Response {
156
189
  this.elapsed = elapsed; // milliseconds
157
190
 
158
191
  // Parse cookies from response
159
- this._cookies = (data.cookies || []).map(c => new Cookie(c.name || "", c.value || ""));
192
+ this._cookies = (data.cookies || []).map(c => new Cookie(c));
160
193
 
161
194
  // Parse redirect history
162
195
  this._history = (data.history || []).map(h => new RedirectInfo(
@@ -343,7 +376,7 @@ class FastResponse {
343
376
  this.elapsed = elapsed;
344
377
 
345
378
  // Parse cookies from response
346
- this._cookies = (metadata.cookies || []).map(c => new Cookie(c.name || "", c.value || ""));
379
+ this._cookies = (metadata.cookies || []).map(c => new Cookie(c));
347
380
 
348
381
  // Parse redirect history
349
382
  this._history = (metadata.history || []).map(h => new RedirectInfo(
@@ -465,7 +498,7 @@ class StreamResponse {
465
498
  this.finalUrl = metadata.final_url || "";
466
499
  this.protocol = metadata.protocol || "";
467
500
  this.contentLength = metadata.content_length || -1;
468
- this._cookies = (metadata.cookies || []).map(c => new Cookie(c.name || "", c.value || ""));
501
+ this._cookies = (metadata.cookies || []).map(c => new Cookie(c));
469
502
  this._closed = false;
470
503
  }
471
504
 
@@ -693,7 +726,7 @@ function getLibPath() {
693
726
  // Define callback proto globally for koffi (must be before getLib)
694
727
  const AsyncCallbackProto = koffi.proto("void AsyncCallback(int64 callbackId, str responseJson, str error)");
695
728
 
696
- // Session cache callback prototypes
729
+ // Session cache callback prototypes (SYNC mode - for sync callbacks like Map)
697
730
  const SessionCacheGetProto = koffi.proto("str SessionCacheGet(str key)");
698
731
  const SessionCachePutProto = koffi.proto("int SessionCachePut(str key, str valueJson, int64 ttlSeconds)");
699
732
  const SessionCacheDeleteProto = koffi.proto("int SessionCacheDelete(str key)");
@@ -701,6 +734,13 @@ const SessionCacheErrorProto = koffi.proto("void SessionCacheError(str operation
701
734
  const EchCacheGetProto = koffi.proto("str EchCacheGet(str key)");
702
735
  const EchCachePutProto = koffi.proto("int EchCachePut(str key, str valueBase64, int64 ttlSeconds)");
703
736
 
737
+ // Session cache callback prototypes (ASYNC mode - for async callbacks like Redis)
738
+ const AsyncCacheGetProto = koffi.proto("void AsyncCacheGet(int64 requestId, str key)");
739
+ const AsyncCachePutProto = koffi.proto("void AsyncCachePut(int64 requestId, str key, str valueJson, int64 ttlSeconds)");
740
+ const AsyncCacheDeleteProto = koffi.proto("void AsyncCacheDelete(int64 requestId, str key)");
741
+ const AsyncEchGetProto = koffi.proto("void AsyncEchGet(int64 requestId, str key)");
742
+ const AsyncEchPutProto = koffi.proto("void AsyncEchPut(int64 requestId, str key, str valueBase64, int64 ttlSeconds)");
743
+
704
744
  // Load the native library
705
745
  let lib = null;
706
746
  let nativeLibHandle = null;
@@ -715,6 +755,8 @@ function getLib() {
715
755
  lib = {
716
756
  httpcloak_session_new: nativeLibHandle.func("httpcloak_session_new", "int64", ["str"]),
717
757
  httpcloak_session_free: nativeLibHandle.func("httpcloak_session_free", "void", ["int64"]),
758
+ httpcloak_session_refresh: nativeLibHandle.func("httpcloak_session_refresh", "void", ["int64"]),
759
+ httpcloak_session_refresh_protocol: nativeLibHandle.func("httpcloak_session_refresh_protocol", "str", ["int64", "str"]),
718
760
  httpcloak_get: nativeLibHandle.func("httpcloak_get", "str", ["int64", "str", "str"]),
719
761
  httpcloak_post: nativeLibHandle.func("httpcloak_post", "str", ["int64", "str", "str", "str"]),
720
762
  httpcloak_request: nativeLibHandle.func("httpcloak_request", "str", ["int64", "str"]),
@@ -781,6 +823,18 @@ function getLib() {
781
823
  koffi.pointer(SessionCacheErrorProto),
782
824
  ]),
783
825
  httpcloak_clear_session_cache_callbacks: nativeLibHandle.func("httpcloak_clear_session_cache_callbacks", "void", []),
826
+ // Async session cache callbacks (for async backends like Redis)
827
+ httpcloak_set_async_session_cache_callbacks: nativeLibHandle.func("httpcloak_set_async_session_cache_callbacks", "void", [
828
+ koffi.pointer(AsyncCacheGetProto),
829
+ koffi.pointer(AsyncCachePutProto),
830
+ koffi.pointer(AsyncCacheDeleteProto),
831
+ koffi.pointer(AsyncEchGetProto),
832
+ koffi.pointer(AsyncEchPutProto),
833
+ koffi.pointer(SessionCacheErrorProto),
834
+ ]),
835
+ // Async cache result functions (called by JS to provide results to Go)
836
+ httpcloak_async_cache_get_result: nativeLibHandle.func("httpcloak_async_cache_get_result", "void", ["int64", "str"]),
837
+ httpcloak_async_cache_op_result: nativeLibHandle.func("httpcloak_async_cache_op_result", "void", ["int64", "int"]),
784
838
  };
785
839
  }
786
840
  return lib;
@@ -1078,7 +1132,9 @@ function version() {
1078
1132
  }
1079
1133
 
1080
1134
  /**
1081
- * Get list of available browser presets
1135
+ * Get available browser presets with their supported protocols.
1136
+ * Returns an object mapping preset names to their info:
1137
+ * { "chrome-144": { protocols: ["h1", "h2", "h3"] }, ... }
1082
1138
  */
1083
1139
  function availablePresets() {
1084
1140
  const nativeLib = getLib();
@@ -1087,7 +1143,7 @@ function availablePresets() {
1087
1143
  if (result) {
1088
1144
  return JSON.parse(result);
1089
1145
  }
1090
- return [];
1146
+ return {};
1091
1147
  }
1092
1148
 
1093
1149
  /**
@@ -1184,6 +1240,10 @@ class Session {
1184
1240
  echConfigDomain = null,
1185
1241
  tlsOnly = false,
1186
1242
  quicIdleTimeout = 0,
1243
+ localAddress = null,
1244
+ keyLogFile = null,
1245
+ disableSpeculativeTls = false,
1246
+ switchProtocol = null,
1187
1247
  } = options;
1188
1248
 
1189
1249
  this._lib = getLib();
@@ -1238,6 +1298,18 @@ class Session {
1238
1298
  if (quicIdleTimeout > 0) {
1239
1299
  config.quic_idle_timeout = quicIdleTimeout;
1240
1300
  }
1301
+ if (localAddress) {
1302
+ config.local_address = localAddress;
1303
+ }
1304
+ if (keyLogFile) {
1305
+ config.key_log_file = keyLogFile;
1306
+ }
1307
+ if (disableSpeculativeTls) {
1308
+ config.disable_speculative_tls = true;
1309
+ }
1310
+ if (switchProtocol) {
1311
+ config.switch_protocol = switchProtocol;
1312
+ }
1241
1313
 
1242
1314
  this._handle = this._lib.httpcloak_session_new(JSON.stringify(config));
1243
1315
 
@@ -1256,6 +1328,29 @@ class Session {
1256
1328
  }
1257
1329
  }
1258
1330
 
1331
+ /**
1332
+ * Refresh the session by closing all connections while keeping TLS session tickets.
1333
+ * This simulates a browser page refresh - connections are severed but 0-RTT
1334
+ * early data can be used on reconnection due to preserved session tickets.
1335
+ * @param {string} [switchProtocol] - Optional protocol to switch to ("h1", "h2", "h3").
1336
+ * Overrides any switchProtocol set at construction time. Persists for future refresh() calls.
1337
+ */
1338
+ refresh(switchProtocol) {
1339
+ if (this._handle) {
1340
+ if (switchProtocol) {
1341
+ const result = this._lib.httpcloak_session_refresh_protocol(this._handle, switchProtocol);
1342
+ if (result) {
1343
+ const data = JSON.parse(result);
1344
+ if (data.error) {
1345
+ throw new HTTPCloakError(data.error);
1346
+ }
1347
+ }
1348
+ } else {
1349
+ this._lib.httpcloak_session_refresh(this._handle);
1350
+ }
1351
+ }
1352
+ }
1353
+
1259
1354
  /**
1260
1355
  * Merge session headers with request headers
1261
1356
  */
@@ -2700,6 +2795,11 @@ function request(method, url, options = {}) {
2700
2795
  * Use this to transparently apply fingerprinting to any HTTP client (e.g., Undici, fetch).
2701
2796
  *
2702
2797
  * Supports per-request proxy rotation via X-Upstream-Proxy header.
2798
+ * Supports per-request session routing via X-HTTPCloak-Session header.
2799
+ *
2800
+ * IMPORTANT: For distributed session caching to work with X-HTTPCloak-Session header,
2801
+ * you MUST register the session with the proxy using registerSession() first.
2802
+ * Without registration, cache callbacks will not be triggered for that session.
2703
2803
  *
2704
2804
  * @example
2705
2805
  * const proxy = new LocalProxy({ preset: "chrome-143", tlsOnly: true });
@@ -2709,6 +2809,22 @@ function request(method, url, options = {}) {
2709
2809
  * // Pass X-Upstream-Proxy header to rotate proxies per-request
2710
2810
  *
2711
2811
  * proxy.close();
2812
+ *
2813
+ * @example
2814
+ * // Using with distributed session cache
2815
+ * const proxy = new LocalProxy({ port: 8888 });
2816
+ * const session = new Session({ preset: 'chrome-143' });
2817
+ *
2818
+ * // Configure distributed cache (e.g., Redis)
2819
+ * httpcloak.configureSessionCache({
2820
+ * get: async (key) => await redis.get(key),
2821
+ * put: async (key, value, ttl) => { await redis.setex(key, ttl, value); return 0; },
2822
+ * });
2823
+ *
2824
+ * // REQUIRED: Register session with proxy for cache callbacks to work
2825
+ * proxy.registerSession('session-1', session);
2826
+ *
2827
+ * // Now requests with X-HTTPCloak-Session: session-1 will trigger cache callbacks
2712
2828
  */
2713
2829
  class LocalProxy {
2714
2830
  /**
@@ -2898,13 +3014,17 @@ class SessionCacheBackend {
2898
3014
  /**
2899
3015
  * Create a session cache backend.
2900
3016
  *
3017
+ * Supports both synchronous and asynchronous callbacks. Async mode is automatically
3018
+ * detected when any callback is an async function or returns a Promise.
3019
+ *
2901
3020
  * @param {Object} options Cache configuration
2902
- * @param {Function} [options.get] Function to get session data: (key: string) => string|null|Promise<string|null>
2903
- * @param {Function} [options.put] Function to store session: (key: string, value: string, ttlSeconds: number) => number|Promise<number>
2904
- * @param {Function} [options.delete] Function to delete session: (key: string) => number|Promise<number>
2905
- * @param {Function} [options.getEch] Function to get ECH config: (key: string) => string|null|Promise<string|null>
2906
- * @param {Function} [options.putEch] Function to store ECH: (key: string, value: string, ttlSeconds: number) => number|Promise<number>
3021
+ * @param {Function} [options.get] Get session data: (key: string) => string|null|Promise<string|null>
3022
+ * @param {Function} [options.put] Store session: (key: string, value: string, ttlSeconds: number) => number|Promise<number>
3023
+ * @param {Function} [options.delete] Delete session: (key: string) => number|Promise<number>
3024
+ * @param {Function} [options.getEch] Get ECH config: (key: string) => string|null|Promise<string|null>
3025
+ * @param {Function} [options.putEch] Store ECH: (key: string, value: string, ttlSeconds: number) => number|Promise<number>
2907
3026
  * @param {Function} [options.onError] Error callback: (operation: string, key: string, error: string) => void
3027
+ * @param {boolean} [options.async] Force async mode (auto-detected if not specified)
2908
3028
  */
2909
3029
  constructor(options = {}) {
2910
3030
  this._get = options.get || null;
@@ -2914,6 +3034,23 @@ class SessionCacheBackend {
2914
3034
  this._putEch = options.putEch || null;
2915
3035
  this._onError = options.onError || null;
2916
3036
  this._registered = false;
3037
+
3038
+ // Auto-detect async mode based on function types
3039
+ // AsyncFunction.constructor.name === 'AsyncFunction'
3040
+ // Use !! to ensure boolean result (null && x returns null, not false)
3041
+ const isAsyncFn = (fn) => !!(fn && (fn.constructor.name === 'AsyncFunction' || fn[Symbol.toStringTag] === 'AsyncFunction'));
3042
+ this._asyncMode = options.async !== undefined ? options.async : (
3043
+ isAsyncFn(this._get) || isAsyncFn(this._put) || isAsyncFn(this._delete) ||
3044
+ isAsyncFn(this._getEch) || isAsyncFn(this._putEch)
3045
+ );
3046
+ }
3047
+
3048
+ /**
3049
+ * Check if this backend is running in async mode.
3050
+ * @returns {boolean}
3051
+ */
3052
+ get isAsync() {
3053
+ return this._asyncMode;
2917
3054
  }
2918
3055
 
2919
3056
  /**
@@ -2926,17 +3063,42 @@ class SessionCacheBackend {
2926
3063
  const lib = getLib();
2927
3064
 
2928
3065
  // Create callback pointers (keep references to prevent GC)
2929
- // Use no-op callbacks for missing functions to avoid null pointer issues
2930
3066
  _sessionCacheCallbackPtrs = {};
2931
3067
 
2932
- // Create get callback (or no-op)
3068
+ // Create error callback (shared between sync and async modes)
3069
+ const errorFn = this._onError;
3070
+ _sessionCacheCallbackPtrs.error = koffi.register((operation, key, error) => {
3071
+ if (!errorFn) return;
3072
+ try {
3073
+ errorFn(operation, key, error);
3074
+ } catch (e) {
3075
+ // Ignore errors in error callback
3076
+ }
3077
+ }, koffi.pointer(SessionCacheErrorProto));
3078
+
3079
+ if (this._asyncMode) {
3080
+ this._registerAsync(lib);
3081
+ } else {
3082
+ this._registerSync(lib);
3083
+ }
3084
+
3085
+ _sessionCacheBackend = this;
3086
+ this._registered = true;
3087
+ }
3088
+
3089
+ /**
3090
+ * Register synchronous callbacks (for in-memory Map, etc.)
3091
+ * @private
3092
+ */
3093
+ _registerSync(lib) {
2933
3094
  const getFn = this._get;
2934
3095
  _sessionCacheCallbackPtrs.get = koffi.register((key) => {
2935
3096
  if (!getFn) return null;
2936
3097
  try {
2937
3098
  const result = getFn(key);
3099
+ // If sync mode but user passed async function, warn and return null
2938
3100
  if (result && typeof result.then === 'function') {
2939
- console.warn('SessionCacheBackend: Async callbacks not fully supported, returning null');
3101
+ console.warn('SessionCacheBackend: Detected async callback in sync mode. Use async: true option.');
2940
3102
  return null;
2941
3103
  }
2942
3104
  return result || null;
@@ -2945,13 +3107,13 @@ class SessionCacheBackend {
2945
3107
  }
2946
3108
  }, koffi.pointer(SessionCacheGetProto));
2947
3109
 
2948
- // Create put callback (or no-op)
2949
3110
  const putFn = this._put;
2950
3111
  _sessionCacheCallbackPtrs.put = koffi.register((key, value, ttlSeconds) => {
2951
3112
  if (!putFn) return 0;
2952
3113
  try {
2953
3114
  const result = putFn(key, value, Number(ttlSeconds));
2954
3115
  if (result && typeof result.then === 'function') {
3116
+ console.warn('SessionCacheBackend: Detected async callback in sync mode. Use async: true option.');
2955
3117
  return 0;
2956
3118
  }
2957
3119
  return result || 0;
@@ -2960,13 +3122,13 @@ class SessionCacheBackend {
2960
3122
  }
2961
3123
  }, koffi.pointer(SessionCachePutProto));
2962
3124
 
2963
- // Create delete callback (or no-op)
2964
3125
  const deleteFn = this._delete;
2965
3126
  _sessionCacheCallbackPtrs.delete = koffi.register((key) => {
2966
3127
  if (!deleteFn) return 0;
2967
3128
  try {
2968
3129
  const result = deleteFn(key);
2969
3130
  if (result && typeof result.then === 'function') {
3131
+ console.warn('SessionCacheBackend: Detected async callback in sync mode. Use async: true option.');
2970
3132
  return 0;
2971
3133
  }
2972
3134
  return result || 0;
@@ -2975,13 +3137,13 @@ class SessionCacheBackend {
2975
3137
  }
2976
3138
  }, koffi.pointer(SessionCacheDeleteProto));
2977
3139
 
2978
- // Create ECH get callback (or no-op)
2979
3140
  const getEchFn = this._getEch;
2980
3141
  _sessionCacheCallbackPtrs.getEch = koffi.register((key) => {
2981
3142
  if (!getEchFn) return null;
2982
3143
  try {
2983
3144
  const result = getEchFn(key);
2984
3145
  if (result && typeof result.then === 'function') {
3146
+ console.warn('SessionCacheBackend: Detected async callback in sync mode. Use async: true option.');
2985
3147
  return null;
2986
3148
  }
2987
3149
  return result || null;
@@ -2990,13 +3152,13 @@ class SessionCacheBackend {
2990
3152
  }
2991
3153
  }, koffi.pointer(EchCacheGetProto));
2992
3154
 
2993
- // Create ECH put callback (or no-op)
2994
3155
  const putEchFn = this._putEch;
2995
3156
  _sessionCacheCallbackPtrs.putEch = koffi.register((key, value, ttlSeconds) => {
2996
3157
  if (!putEchFn) return 0;
2997
3158
  try {
2998
3159
  const result = putEchFn(key, value, Number(ttlSeconds));
2999
3160
  if (result && typeof result.then === 'function') {
3161
+ console.warn('SessionCacheBackend: Detected async callback in sync mode. Use async: true option.');
3000
3162
  return 0;
3001
3163
  }
3002
3164
  return result || 0;
@@ -3005,18 +3167,6 @@ class SessionCacheBackend {
3005
3167
  }
3006
3168
  }, koffi.pointer(EchCachePutProto));
3007
3169
 
3008
- // Create error callback (or no-op)
3009
- const errorFn = this._onError;
3010
- _sessionCacheCallbackPtrs.error = koffi.register((operation, key, error) => {
3011
- if (!errorFn) return;
3012
- try {
3013
- errorFn(operation, key, error);
3014
- } catch (e) {
3015
- // Ignore errors in error callback
3016
- }
3017
- }, koffi.pointer(SessionCacheErrorProto));
3018
-
3019
- // Register with library
3020
3170
  lib.httpcloak_set_session_cache_callbacks(
3021
3171
  _sessionCacheCallbackPtrs.get,
3022
3172
  _sessionCacheCallbackPtrs.put,
@@ -3025,9 +3175,104 @@ class SessionCacheBackend {
3025
3175
  _sessionCacheCallbackPtrs.putEch,
3026
3176
  _sessionCacheCallbackPtrs.error
3027
3177
  );
3178
+ }
3028
3179
 
3029
- _sessionCacheBackend = this;
3030
- this._registered = true;
3180
+ /**
3181
+ * Register asynchronous callbacks (for Redis, database, etc.)
3182
+ * Go will call our callback with a request ID, we process async,
3183
+ * then call back into Go with the result.
3184
+ * @private
3185
+ */
3186
+ _registerAsync(lib) {
3187
+ const getFn = this._get;
3188
+ _sessionCacheCallbackPtrs.get = koffi.register((requestId, key) => {
3189
+ if (!getFn) {
3190
+ lib.httpcloak_async_cache_get_result(requestId, null);
3191
+ return;
3192
+ }
3193
+ // Process async and call back with result
3194
+ Promise.resolve()
3195
+ .then(() => getFn(key))
3196
+ .then((result) => {
3197
+ lib.httpcloak_async_cache_get_result(requestId, result || null);
3198
+ })
3199
+ .catch(() => {
3200
+ lib.httpcloak_async_cache_get_result(requestId, null);
3201
+ });
3202
+ }, koffi.pointer(AsyncCacheGetProto));
3203
+
3204
+ const putFn = this._put;
3205
+ _sessionCacheCallbackPtrs.put = koffi.register((requestId, key, value, ttlSeconds) => {
3206
+ if (!putFn) {
3207
+ lib.httpcloak_async_cache_op_result(requestId, 0);
3208
+ return;
3209
+ }
3210
+ Promise.resolve()
3211
+ .then(() => putFn(key, value, Number(ttlSeconds)))
3212
+ .then((result) => {
3213
+ lib.httpcloak_async_cache_op_result(requestId, result || 0);
3214
+ })
3215
+ .catch(() => {
3216
+ lib.httpcloak_async_cache_op_result(requestId, -1);
3217
+ });
3218
+ }, koffi.pointer(AsyncCachePutProto));
3219
+
3220
+ const deleteFn = this._delete;
3221
+ _sessionCacheCallbackPtrs.delete = koffi.register((requestId, key) => {
3222
+ if (!deleteFn) {
3223
+ lib.httpcloak_async_cache_op_result(requestId, 0);
3224
+ return;
3225
+ }
3226
+ Promise.resolve()
3227
+ .then(() => deleteFn(key))
3228
+ .then((result) => {
3229
+ lib.httpcloak_async_cache_op_result(requestId, result || 0);
3230
+ })
3231
+ .catch(() => {
3232
+ lib.httpcloak_async_cache_op_result(requestId, -1);
3233
+ });
3234
+ }, koffi.pointer(AsyncCacheDeleteProto));
3235
+
3236
+ const getEchFn = this._getEch;
3237
+ _sessionCacheCallbackPtrs.getEch = koffi.register((requestId, key) => {
3238
+ if (!getEchFn) {
3239
+ lib.httpcloak_async_cache_get_result(requestId, null);
3240
+ return;
3241
+ }
3242
+ Promise.resolve()
3243
+ .then(() => getEchFn(key))
3244
+ .then((result) => {
3245
+ lib.httpcloak_async_cache_get_result(requestId, result || null);
3246
+ })
3247
+ .catch(() => {
3248
+ lib.httpcloak_async_cache_get_result(requestId, null);
3249
+ });
3250
+ }, koffi.pointer(AsyncEchGetProto));
3251
+
3252
+ const putEchFn = this._putEch;
3253
+ _sessionCacheCallbackPtrs.putEch = koffi.register((requestId, key, value, ttlSeconds) => {
3254
+ if (!putEchFn) {
3255
+ lib.httpcloak_async_cache_op_result(requestId, 0);
3256
+ return;
3257
+ }
3258
+ Promise.resolve()
3259
+ .then(() => putEchFn(key, value, Number(ttlSeconds)))
3260
+ .then((result) => {
3261
+ lib.httpcloak_async_cache_op_result(requestId, result || 0);
3262
+ })
3263
+ .catch(() => {
3264
+ lib.httpcloak_async_cache_op_result(requestId, -1);
3265
+ });
3266
+ }, koffi.pointer(AsyncEchPutProto));
3267
+
3268
+ lib.httpcloak_set_async_session_cache_callbacks(
3269
+ _sessionCacheCallbackPtrs.get,
3270
+ _sessionCacheCallbackPtrs.put,
3271
+ _sessionCacheCallbackPtrs.delete,
3272
+ _sessionCacheCallbackPtrs.getEch,
3273
+ _sessionCacheCallbackPtrs.putEch,
3274
+ _sessionCacheCallbackPtrs.error
3275
+ );
3031
3276
  }
3032
3277
 
3033
3278
  /**
@@ -3053,11 +3298,25 @@ class SessionCacheBackend {
3053
3298
  * Configure a distributed session cache backend.
3054
3299
  *
3055
3300
  * This is a convenience function that creates and registers a SessionCacheBackend.
3301
+ * Supports both synchronous and asynchronous callbacks (auto-detected).
3056
3302
  *
3057
3303
  * @param {Object} options Cache configuration (same as SessionCacheBackend constructor)
3058
3304
  * @returns {SessionCacheBackend} The registered backend
3059
3305
  *
3060
- * Example:
3306
+ * Example using in-memory Map (sync):
3307
+ * ```javascript
3308
+ * const httpcloak = require('httpcloak');
3309
+ *
3310
+ * const cache = new Map();
3311
+ *
3312
+ * httpcloak.configureSessionCache({
3313
+ * get: (key) => cache.get(key) || null,
3314
+ * put: (key, value, ttl) => { cache.set(key, value); return 0; },
3315
+ * delete: (key) => { cache.delete(key); return 0; },
3316
+ * });
3317
+ * ```
3318
+ *
3319
+ * Example using Redis (async):
3061
3320
  * ```javascript
3062
3321
  * const Redis = require('ioredis');
3063
3322
  * const httpcloak = require('httpcloak');
@@ -3065,12 +3324,12 @@ class SessionCacheBackend {
3065
3324
  * const redis = new Redis();
3066
3325
  *
3067
3326
  * httpcloak.configureSessionCache({
3068
- * get: (key) => redis.get(key),
3069
- * put: (key, value, ttl) => { redis.setex(key, ttl, value); return 0; },
3070
- * delete: (key) => { redis.del(key); return 0; },
3327
+ * get: async (key) => await redis.get(key),
3328
+ * put: async (key, value, ttl) => { await redis.setex(key, ttl, value); return 0; },
3329
+ * delete: async (key) => { await redis.del(key); return 0; },
3071
3330
  * });
3072
3331
  *
3073
- * // Now all sessions will use Redis for TLS session storage
3332
+ * // Async callbacks are automatically detected and handled properly
3074
3333
  * const session = new httpcloak.Session();
3075
3334
  * await session.get('https://example.com');
3076
3335
  * ```
package/lib/index.mjs CHANGED
@@ -12,11 +12,26 @@ const require = createRequire(import.meta.url);
12
12
  const cjs = require("./index.js");
13
13
 
14
14
  // Re-export all named exports
15
+ // Classes
15
16
  export const Session = cjs.Session;
17
+ export const LocalProxy = cjs.LocalProxy;
16
18
  export const Response = cjs.Response;
19
+ export const FastResponse = cjs.FastResponse;
20
+ export const StreamResponse = cjs.StreamResponse;
21
+ export const Cookie = cjs.Cookie;
22
+ export const RedirectInfo = cjs.RedirectInfo;
17
23
  export const HTTPCloakError = cjs.HTTPCloakError;
24
+ export const SessionCacheBackend = cjs.SessionCacheBackend;
25
+
26
+ // Presets
18
27
  export const Preset = cjs.Preset;
28
+
29
+ // Configuration
19
30
  export const configure = cjs.configure;
31
+ export const configureSessionCache = cjs.configureSessionCache;
32
+ export const clearSessionCache = cjs.clearSessionCache;
33
+
34
+ // Module-level functions
20
35
  export const get = cjs.get;
21
36
  export const post = cjs.post;
22
37
  export const put = cjs.put;
@@ -24,9 +39,15 @@ export const patch = cjs.patch;
24
39
  export const head = cjs.head;
25
40
  export const options = cjs.options;
26
41
  export const request = cjs.request;
42
+
43
+ // Utility
27
44
  export const version = cjs.version;
28
45
  export const availablePresets = cjs.availablePresets;
29
46
 
47
+ // DNS configuration
48
+ export const setEchDnsServers = cjs.setEchDnsServers;
49
+ export const getEchDnsServers = cjs.getEchDnsServers;
50
+
30
51
  // 'delete' is a reserved word in ESM, so we export it specially
31
52
  const del = cjs.delete;
32
53
  export { del as delete };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/darwin-arm64",
3
- "version": "1.5.9",
3
+ "version": "1.6.0-beta.4",
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.5.9",
3
+ "version": "1.6.0-beta.4",
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.5.9",
3
+ "version": "1.6.0-beta.4",
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.5.9",
3
+ "version": "1.6.0-beta.4",
4
4
  "description": "HTTPCloak native binary for linux x64",
5
5
  "os": [
6
6
  "linux"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/win32-arm64",
3
- "version": "1.5.9",
3
+ "version": "1.6.0-beta.4",
4
4
  "description": "HTTPCloak native binary for win32 arm64",
5
5
  "os": [
6
6
  "win32"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/win32-x64",
3
- "version": "1.5.9",
3
+ "version": "1.6.0-beta.4",
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.5.9",
3
+ "version": "1.6.0-beta.4",
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,11 +49,15 @@
49
49
  "koffi": "^2.9.0"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@httpcloak/linux-x64": "1.5.9",
53
- "@httpcloak/linux-arm64": "1.5.9",
54
- "@httpcloak/darwin-x64": "1.5.9",
55
- "@httpcloak/darwin-arm64": "1.5.9",
56
- "@httpcloak/win32-x64": "1.5.9",
57
- "@httpcloak/win32-arm64": "1.5.9"
52
+ "@httpcloak/darwin-arm64": "1.6.0-beta.4",
53
+ "@httpcloak/darwin-x64": "1.6.0-beta.4",
54
+ "@httpcloak/linux-arm64": "1.6.0-beta.4",
55
+ "@httpcloak/linux-x64": "1.6.0-beta.4",
56
+ "@httpcloak/win32-arm64": "1.6.0-beta.4",
57
+ "@httpcloak/win32-x64": "1.6.0-beta.4"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^25.1.0",
61
+ "typescript": "^5.9.3"
58
62
  }
59
63
  }