httpcloak 1.6.6 → 1.6.7

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
@@ -213,9 +213,9 @@ export interface SessionOptions {
213
213
  allowRedirects?: boolean;
214
214
  /** Maximum number of redirects to follow (default: 10) */
215
215
  maxRedirects?: number;
216
- /** Number of retries on failure (default: 3, set to 0 to disable) */
216
+ /** Number of retries on failure (default: 0, opt in by setting a positive integer) */
217
217
  retry?: number;
218
- /** Status codes to retry on (default: [429, 500, 502, 503, 504]) */
218
+ /** Status codes to retry on (default: empty; set explicitly to opt in, e.g. [429, 500, 502, 503, 504]) */
219
219
  retryOnStatus?: number[];
220
220
  /** Minimum wait time between retries in milliseconds (default: 500) */
221
221
  retryWaitMin?: number;
@@ -243,12 +243,28 @@ export interface SessionOptions {
243
243
  switchProtocol?: string;
244
244
  /** Disable internal cookie jar entirely — caller manages cookies via per-request headers (default: false) */
245
245
  withoutCookieJar?: boolean;
246
+ /** Disable ETag / If-Modified-Since handling for the lifetime of the session (default: false) */
247
+ withoutConditionalCache?: boolean;
248
+ /** Skip the ECH (Encrypted Client Hello) HTTPS RR lookup. Saves ~15-20ms on first connect (default: false) */
249
+ disableEch?: boolean;
250
+ /** Disable HTTP/3 racing while keeping H1/H2 auto-negotiation. Reachable indirectly via httpVersion: "h2" but the explicit flag is cleaner (default: false) */
251
+ disableHttp3?: boolean;
246
252
  /** Custom JA3 fingerprint string (e.g., "771,4865-4866-4867-...,0-23-65281-...,29-23-24,0") */
247
253
  ja3?: string;
248
254
  /** Custom Akamai HTTP/2 fingerprint string (e.g., "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p") */
249
255
  akamai?: string;
250
256
  /** Extra fingerprint options: { tls_alpn, tls_signature_algorithms, tls_cert_compression, tls_permute_extensions } */
251
257
  extraFp?: Record<string, any>;
258
+ /** TCP IP Time-To-Live in the SYN packet. 128 = Windows, 64 = Linux/macOS/iOS/Android. */
259
+ tcpTtl?: number;
260
+ /** TCP Maximum Segment Size option. 1460 for standard Ethernet. */
261
+ tcpMss?: number;
262
+ /** TCP Window Size in the SYN packet. 64240 = Windows 10/11, 65535 = Linux/macOS. */
263
+ tcpWindowSize?: number;
264
+ /** TCP Window Scale option exponent. 8 = Windows, 7 = Linux/Android, 6 = macOS/iOS. */
265
+ tcpWindowScale?: number;
266
+ /** IP Don't-Fragment flag. true on every modern client. */
267
+ tcpDf?: boolean;
252
268
  }
253
269
 
254
270
  export interface RequestOptions {
@@ -283,6 +299,64 @@ export interface RequestOptions {
283
299
  * Set this explicitly when the auto-sniff gets it wrong (e.g., POST to a CORS endpoint without a JSON Accept header).
284
300
  */
285
301
  fetchMode?: "cors" | "no-cors" | "navigate" | "websocket";
302
+
303
+ /**
304
+ * Per-request override for redirect following. true forces redirects on this
305
+ * call, false surfaces the 3xx back to the caller; null / undefined defers
306
+ * to the session-level setting (which itself defaults to follow).
307
+ */
308
+ allowRedirects?: boolean | null;
309
+
310
+ /**
311
+ * Per-request opt-out of the session's ETag / If-Modified-Since handling.
312
+ * When true, no cache validators are injected on the way out and the
313
+ * response's ETag / Last-Modified are not stored. Useful for a one-off
314
+ * fresh fetch without touching the session-wide setting.
315
+ */
316
+ disableConditionalCache?: boolean;
317
+
318
+ /**
319
+ * AbortSignal for cancelling an in-flight request. Honored by the async
320
+ * methods (get, post, put, patch, delete, head, options, request). When
321
+ * the signal aborts, the underlying Go-side request is cancelled (DNS /
322
+ * TCP / TLS / HTTP work is torn down) and the returned promise rejects
323
+ * with the signal's reason (or a generic AbortError if none was set).
324
+ *
325
+ * @example
326
+ * const controller = new AbortController();
327
+ * setTimeout(() => controller.abort(new Error("too slow")), 5000);
328
+ * await session.get("https://slow.example.com", { signal: controller.signal });
329
+ */
330
+ signal?: AbortSignal;
331
+ }
332
+
333
+ /**
334
+ * Snapshot returned by `Session.stats()`. Mirrors the wire shape emitted by
335
+ * the Go-side `session.Stats()` after snake_case marshalling.
336
+ */
337
+ export interface SessionStats {
338
+ /** Stable session ID assigned at construction. */
339
+ id: string;
340
+ /** Preset name the session was created with. */
341
+ preset: string;
342
+ /** Construction time, Unix nanoseconds. */
343
+ created_at: number;
344
+ /** Last-request time, Unix nanoseconds. */
345
+ last_used: number;
346
+ /** Total requests serviced by this session. */
347
+ request_count: number;
348
+ /** Whether the session is still usable (false after close). */
349
+ active: boolean;
350
+ /** Live cookie count in the jar. */
351
+ cookie_count: number;
352
+ /** Conditional-cache entry count (one per cached URL). */
353
+ cache_entry_count: number;
354
+ /** Age in nanoseconds (now - created_at). */
355
+ age_ns: number;
356
+ /** Idle time in nanoseconds (now - last_used). */
357
+ idle_time_ns: number;
358
+ /** Transport-level stats (per-protocol). Shape varies; treat as opaque. */
359
+ transport_stats?: Record<string, any>;
286
360
  }
287
361
 
288
362
  export class Session {
@@ -313,8 +387,11 @@ export class Session {
313
387
  /** Refresh the session by closing all connections while keeping TLS session tickets.
314
388
  * This simulates a browser page refresh - connections are severed but 0-RTT
315
389
  * early data can be used on reconnection due to preserved session tickets.
390
+ *
391
+ * @param switchProtocol - Optional protocol to switch to ("h1", "h2", "h3").
392
+ * Overrides any switchProtocol set at construction time. Persists for future refresh() calls.
316
393
  */
317
- refresh(): void;
394
+ refresh(switchProtocol?: "h1" | "h2" | "h3"): void;
318
395
 
319
396
  // Synchronous methods
320
397
  /** Perform a synchronous GET request */
@@ -359,17 +436,11 @@ export class Session {
359
436
  /** Get a specific cookie by name with full metadata */
360
437
  getCookieDetailed(name: string): Cookie | null;
361
438
 
362
- /**
363
- * Get all cookies as a flat name-value object.
364
- * @deprecated Will return Cookie[] with full metadata in a future release. Use getCookiesDetailed() for the new format now.
365
- */
366
- getCookies(): Record<string, string>;
439
+ /** Get all cookies with full metadata. Alias of getCookiesDetailed(); the older flat dict shape was removed in v1.6.5. */
440
+ getCookies(): Cookie[];
367
441
 
368
- /**
369
- * Get a specific cookie value by name.
370
- * @deprecated Will return Cookie|null in a future release. Use getCookieDetailed() for the new format now.
371
- */
372
- getCookie(name: string): string | null;
442
+ /** Get a specific cookie by name. Alias of getCookieDetailed(); the older value-only shape was removed in v1.6.5. */
443
+ getCookie(name: string): Cookie | null;
373
444
 
374
445
  /** Set a cookie in the session */
375
446
  setCookie(
@@ -392,11 +463,72 @@ export class Session {
392
463
  /** Clear all cookies from the session */
393
464
  clearCookies(): void;
394
465
 
466
+ /** Cookies in the session jar with full metadata. Same shape as getCookies(). */
467
+ readonly cookies: Cookie[];
468
+
469
+ // Conditional cache and redirect runtime control
470
+
471
+ /**
472
+ * Drop the session's per-URL conditional-cache map (ETag / Last-Modified).
473
+ * The next request to each URL goes out without If-None-Match /
474
+ * If-Modified-Since headers. Cookies and TLS tickets are not touched.
475
+ */
476
+ clearCache(): void;
477
+
478
+ /**
479
+ * Snapshot of session counters, timestamps and transport-level metrics.
480
+ * Mirrors the keys Go's session.Stats() returns, snake_case on the wire.
481
+ */
482
+ stats(): SessionStats;
483
+
484
+ /**
485
+ * Return the time since the session last serviced a request, in seconds.
486
+ * Returns -1 if the session handle is invalid.
487
+ */
488
+ idleTime(): number;
489
+
490
+ /**
491
+ * Return true if the session is still usable (close() has not been called
492
+ * and the handle is valid).
493
+ */
494
+ isActive(): boolean;
495
+
496
+ /**
497
+ * Reset the idle timer to now without issuing a request. Useful in
498
+ * long-running pools where an external heartbeat shouldn't let a session
499
+ * look idle to a reaper.
500
+ */
501
+ touch(): void;
502
+
503
+ /**
504
+ * Toggle the session's ETag / If-Modified-Since handling at runtime.
505
+ * When disabled, the session stops injecting cache validators on outgoing
506
+ * requests and stops storing them from responses; the existing cache map
507
+ * is preserved (re-enabling resumes using it). Pair with clearCache() to
508
+ * also wipe previously-stored validators.
509
+ */
510
+ setConditionalCache(enabled: boolean): void;
511
+
512
+ /** Read the session's current conditional-cache state. */
513
+ getConditionalCache(): boolean;
514
+
515
+ /**
516
+ * Toggle the session's redirect-following policy at runtime. The change
517
+ * takes effect on the next request and persists until set again.
518
+ */
519
+ setFollowRedirects(enabled: boolean): void;
520
+
521
+ /** Read the session's current redirect-following policy. */
522
+ getFollowRedirects(): boolean;
523
+
395
524
  /**
396
- * Get cookies as a property.
397
- * @deprecated Will return Cookie[] with full metadata in a future release.
525
+ * Update the session's redirect cap at runtime. Values of zero or below
526
+ * are ignored, leaving the prior cap (or the default of 10) in place.
398
527
  */
399
- readonly cookies: Record<string, string>;
528
+ setMaxRedirects(max: number): void;
529
+
530
+ /** Read the session's current redirect cap. */
531
+ getMaxRedirects(): number;
400
532
 
401
533
  // Proxy management
402
534
 
@@ -657,6 +789,37 @@ export class Session {
657
789
  * @returns FastResponse with Buffer body
658
790
  */
659
791
  patchFast(url: string, options?: RequestOptions): FastResponse;
792
+
793
+ /**
794
+ * Stream an arbitrary-sized body to the wire without buffering it in memory.
795
+ * `chunks` is any iterable / async-iterable yielding Buffer / Uint8Array /
796
+ * string chunks; the Go side opens an io.Pipe for the body and each chunk
797
+ * flows straight through with no base64 wrap and no JSON envelope.
798
+ *
799
+ * @param method Typically "POST", "PUT" or "PATCH".
800
+ * @param url
801
+ * @param chunks Iterable or async-iterable of body chunks.
802
+ * @param options.headers Headers (Content-Type defaults to application/octet-stream).
803
+ * @param options.contentType Optional explicit Content-Type override.
804
+ * @param options.timeout Per-request timeout in milliseconds.
805
+ * @returns Resolves to a regular `Response` once the upload completes.
806
+ */
807
+ uploadStream(
808
+ method: string,
809
+ url: string,
810
+ chunks: AsyncIterable<Buffer | Uint8Array | string> | Iterable<Buffer | Uint8Array | string>,
811
+ options?: { headers?: Record<string, string>; contentType?: string; timeout?: number }
812
+ ): Promise<Response>;
813
+
814
+ /**
815
+ * Convenience wrapper: streaming POST. Same semantics as
816
+ * `uploadStream("POST", url, chunks, options)`.
817
+ */
818
+ postUpload(
819
+ url: string,
820
+ chunks: AsyncIterable<Buffer | Uint8Array | string> | Iterable<Buffer | Uint8Array | string>,
821
+ options?: { headers?: Record<string, string>; contentType?: string; timeout?: number }
822
+ ): Promise<Response>;
660
823
  }
661
824
 
662
825
  export interface LocalProxyOptions {
@@ -677,16 +840,20 @@ export interface LocalProxyOptions {
677
840
  }
678
841
 
679
842
  export interface LocalProxyStats {
680
- /** Total number of requests processed */
681
- totalRequests: number;
682
- /** Number of active connections */
683
- activeConnections: number;
684
- /** Number of failed requests */
685
- failedRequests: number;
686
- /** Bytes sent */
687
- bytesSent: number;
688
- /** Bytes received */
689
- bytesReceived: number;
843
+ /** Whether the proxy is currently accepting connections */
844
+ running: boolean;
845
+ /** TCP port the proxy is bound to */
846
+ port: number;
847
+ /** Live count of in-flight connections */
848
+ active_conns: number;
849
+ /** Lifetime request counter since the proxy started */
850
+ total_requests: number;
851
+ /** Preset name the proxy was started with */
852
+ preset: string;
853
+ /** Max concurrent connections configured at start time */
854
+ max_connections: number;
855
+ /** Number of sessions currently registered via registerSession() */
856
+ registered_sessions: number;
690
857
  }
691
858
 
692
859
  /**
@@ -790,6 +957,22 @@ export class LocalProxy {
790
957
  */
791
958
  unregisterSession(sessionId: string): boolean;
792
959
 
960
+ /**
961
+ * Return the IDs of every session currently registered on this proxy.
962
+ * These are the same IDs the X-HTTPCloak-Session header accepts for
963
+ * per-request session routing.
964
+ *
965
+ * @returns List of registered session IDs (empty array if none).
966
+ */
967
+ listSessions(): string[];
968
+
969
+ /**
970
+ * Return true if a session with the given ID is currently registered.
971
+ * Cheaper than `listSessions().includes(id)` when callers only need an
972
+ * existence check (no JSON marshal across the FFI boundary).
973
+ */
974
+ hasSession(sessionId: string): boolean;
975
+
793
976
  /**
794
977
  * Stop and close the proxy.
795
978
  * After closing, the LocalProxy instance cannot be reused.
@@ -800,8 +983,30 @@ export class LocalProxy {
800
983
  /** Get the httpcloak library version */
801
984
  export function version(): string;
802
985
 
803
- /** Get list of available browser presets */
804
- export function availablePresets(): string[];
986
+ /**
987
+ * Get available browser presets keyed by preset name.
988
+ *
989
+ * Each entry carries the protocols the preset supports (some H1/H2 only, some H1/H2/H3).
990
+ * The shape is `{ [presetName: string]: { protocols: string[] } }`.
991
+ *
992
+ * @example
993
+ * const presets = availablePresets();
994
+ * Object.entries(presets).filter(([, info]) => info.protocols.includes("h3"));
995
+ */
996
+ export function availablePresets(): Record<string, { protocols: string[] }>;
997
+
998
+ /**
999
+ * Return a fully-resolved JSON dump of a preset's TLS / H2 / H3 / header configuration.
1000
+ *
1001
+ * Useful for inspecting what a preset name actually does at the wire level, or for
1002
+ * dumping a built-in preset, mutating it, and loading it back with `loadPresetFromJSON`
1003
+ * under a new name.
1004
+ *
1005
+ * @param name - Preset name (e.g. "chrome-148-windows", "firefox-148", "chrome-latest")
1006
+ * @returns JSON string. Parse with `JSON.parse` to get the structured form.
1007
+ * @throws {HTTPCloakError} If the preset name is not registered.
1008
+ */
1009
+ export function describePreset(name: string): string;
805
1010
 
806
1011
  /**
807
1012
  * Configure the DNS servers used for ECH (Encrypted Client Hello) config queries.
@@ -859,6 +1064,28 @@ export function request(method: string, url: string, options?: RequestOptions):
859
1064
 
860
1065
  /** Available browser presets */
861
1066
  export const Preset: {
1067
+ CHROME_LATEST: string;
1068
+ CHROME_LATEST_WINDOWS: string;
1069
+ CHROME_LATEST_LINUX: string;
1070
+ CHROME_LATEST_MACOS: string;
1071
+ CHROME_LATEST_IOS: string;
1072
+ CHROME_LATEST_ANDROID: string;
1073
+ CHROME_149: string;
1074
+ CHROME_149_WINDOWS: string;
1075
+ CHROME_149_LINUX: string;
1076
+ CHROME_149_MACOS: string;
1077
+ CHROME_148: string;
1078
+ CHROME_148_WINDOWS: string;
1079
+ CHROME_148_LINUX: string;
1080
+ CHROME_148_MACOS: string;
1081
+ CHROME_148_IOS: string;
1082
+ CHROME_148_ANDROID: string;
1083
+ CHROME_147: string;
1084
+ CHROME_147_WINDOWS: string;
1085
+ CHROME_147_LINUX: string;
1086
+ CHROME_147_MACOS: string;
1087
+ CHROME_147_IOS: string;
1088
+ CHROME_147_ANDROID: string;
862
1089
  CHROME_146: string;
863
1090
  CHROME_146_WINDOWS: string;
864
1091
  CHROME_146_LINUX: string;
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,18 @@ 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_follow_redirects: nativeLibHandle.func("httpcloak_session_set_follow_redirects", "void", ["int64", "int"]),
893
+ httpcloak_session_get_follow_redirects: nativeLibHandle.func("httpcloak_session_get_follow_redirects", "int", ["int64"]),
894
+ httpcloak_session_set_max_redirects: nativeLibHandle.func("httpcloak_session_set_max_redirects", "void", ["int64", "int"]),
895
+ httpcloak_session_get_max_redirects: nativeLibHandle.func("httpcloak_session_get_max_redirects", "int", ["int64"]),
896
+ httpcloak_session_set_identifier: nativeLibHandle.func("httpcloak_session_set_identifier", "void", ["int64", "str"]),
833
897
  httpcloak_free_string: httpcloakFreeString,
834
898
  httpcloak_version: nativeLibHandle.func("httpcloak_version", HeapStr, []),
835
899
  httpcloak_available_presets: nativeLibHandle.func("httpcloak_available_presets", HeapStr, []),
@@ -841,6 +905,7 @@ function getLib() {
841
905
  httpcloak_get_async: nativeLibHandle.func("httpcloak_get_async", "void", ["int64", "str", "str", "int64"]),
842
906
  httpcloak_post_async: nativeLibHandle.func("httpcloak_post_async", "void", ["int64", "str", "str", "str", "int64"]),
843
907
  httpcloak_request_async: nativeLibHandle.func("httpcloak_request_async", "void", ["int64", "str", "int64"]),
908
+ httpcloak_cancel_request: nativeLibHandle.func("httpcloak_cancel_request", "void", ["int64"]),
844
909
  // Streaming functions
845
910
  httpcloak_stream_get: nativeLibHandle.func("httpcloak_stream_get", "int64", ["int64", "str", "str"]),
846
911
  httpcloak_stream_post: nativeLibHandle.func("httpcloak_stream_post", "int64", ["int64", "str", "str", "str"]),
@@ -848,6 +913,15 @@ function getLib() {
848
913
  httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata", HeapStr, ["int64"]),
849
914
  httpcloak_stream_read: nativeLibHandle.func("httpcloak_stream_read", HeapStr, ["int64", "int64"]),
850
915
  httpcloak_stream_close: nativeLibHandle.func("httpcloak_stream_close", "void", ["int64"]),
916
+ // Chunked upload state machine (mirrors Python's stream_upload). Lets
917
+ // bindings ship arbitrarily-large bodies without buffering in memory:
918
+ // upload_start opens a pipe to the Go side, upload_write_raw streams
919
+ // chunks straight to the wire, upload_finish reads the response,
920
+ // upload_cancel tears down a partial upload on error.
921
+ httpcloak_upload_start: nativeLibHandle.func("httpcloak_upload_start", "int64", ["int64", "str", "str"]),
922
+ httpcloak_upload_write_raw: nativeLibHandle.func("httpcloak_upload_write_raw", "int", ["int64", "void*", "int"]),
923
+ httpcloak_upload_finish: nativeLibHandle.func("httpcloak_upload_finish", HeapStr, ["int64"]),
924
+ httpcloak_upload_cancel: nativeLibHandle.func("httpcloak_upload_cancel", "void", ["int64"]),
851
925
  // Raw response functions for fast-path (zero-copy)
852
926
  httpcloak_get_raw: nativeLibHandle.func("httpcloak_get_raw", "int64", ["int64", "str", "str"]),
853
927
  httpcloak_post_raw: nativeLibHandle.func("httpcloak_post_raw", "int64", ["int64", "str", "void*", "int", "str"]),
@@ -881,6 +955,8 @@ function getLib() {
881
955
  httpcloak_local_proxy_get_stats: nativeLibHandle.func("httpcloak_local_proxy_get_stats", HeapStr, ["int64"]),
882
956
  httpcloak_local_proxy_register_session: nativeLibHandle.func("httpcloak_local_proxy_register_session", HeapStr, ["int64", "str", "int64"]),
883
957
  httpcloak_local_proxy_unregister_session: nativeLibHandle.func("httpcloak_local_proxy_unregister_session", "int", ["int64", "str"]),
958
+ httpcloak_local_proxy_list_sessions: nativeLibHandle.func("httpcloak_local_proxy_list_sessions", HeapStr, ["int64"]),
959
+ httpcloak_local_proxy_has_session: nativeLibHandle.func("httpcloak_local_proxy_has_session", "int", ["int64", "str"]),
884
960
  // Session cache callbacks
885
961
  httpcloak_set_session_cache_callbacks: nativeLibHandle.func("httpcloak_set_session_cache_callbacks", "void", [
886
962
  koffi.pointer(SessionCacheGetProto),
@@ -1010,7 +1086,7 @@ class AsyncCallbackManager {
1010
1086
  * Register a new async request
1011
1087
  * @returns {{ callbackId: number, promise: Promise<Response> }}
1012
1088
  */
1013
- registerRequest(nativeLib) {
1089
+ registerRequest(nativeLib, signal) {
1014
1090
  this._ensureCallback();
1015
1091
 
1016
1092
  // Register callback with Go (each request gets unique ID)
@@ -1027,6 +1103,43 @@ class AsyncCallbackManager {
1027
1103
  this._pendingRequests.set(Number(callbackId), { resolve, reject, startTime });
1028
1104
  this._ref(); // Keep event loop alive
1029
1105
 
1106
+ // AbortSignal integration. If the caller supplies a signal that is
1107
+ // already aborted, cancel synchronously; otherwise install a listener
1108
+ // that fires once. Pre-aborted signals never deliver the "abort" event,
1109
+ // so the explicit early-path matters.
1110
+ //
1111
+ // Order matters in onAbort:
1112
+ // 1) cancel_request → unblocks the Go goroutine (ctx.Done fires)
1113
+ // 2) unregister_callback → removes the entry from Go's asyncCallbacks
1114
+ // map so the goroutine's final invokeCallback() finds !exists and
1115
+ // returns silently. Without this, Go would still relay an error
1116
+ // callback through koffi after the JS side has already settled
1117
+ // and torn down its callback context, which crashes node with
1118
+ // "Error::ThrowAsJavaScriptException napi_throw" during exit.
1119
+ if (signal && typeof signal === "object" && "aborted" in signal) {
1120
+ const onAbort = () => {
1121
+ const cid = Number(callbackId);
1122
+ try {
1123
+ nativeLib.httpcloak_cancel_request(cid);
1124
+ nativeLib.httpcloak_unregister_callback(cid);
1125
+ } catch {
1126
+ // Best-effort: the request may have already settled.
1127
+ }
1128
+ const pending = this._pendingRequests.get(cid);
1129
+ if (pending) {
1130
+ this._pendingRequests.delete(cid);
1131
+ this._unref();
1132
+ const reason = signal.reason ?? new Error("AbortError");
1133
+ pending.reject(reason);
1134
+ }
1135
+ };
1136
+ if (signal.aborted) {
1137
+ onAbort();
1138
+ } else if (typeof signal.addEventListener === "function") {
1139
+ signal.addEventListener("abort", onAbort, { once: true });
1140
+ }
1141
+ }
1142
+
1030
1143
  return { callbackId, promise };
1031
1144
  }
1032
1145
  }
@@ -1362,6 +1475,9 @@ class Session {
1362
1475
  enableSpeculativeTls = false,
1363
1476
  switchProtocol = null,
1364
1477
  withoutCookieJar = false,
1478
+ withoutConditionalCache = false,
1479
+ disableEch = false,
1480
+ disableHttp3 = false,
1365
1481
  ja3 = null,
1366
1482
  akamai = null,
1367
1483
  extraFp = null,
@@ -1439,6 +1555,15 @@ class Session {
1439
1555
  if (withoutCookieJar) {
1440
1556
  config.without_cookie_jar = true;
1441
1557
  }
1558
+ if (withoutConditionalCache) {
1559
+ config.without_conditional_cache = true;
1560
+ }
1561
+ if (disableEch) {
1562
+ config.disable_ech = true;
1563
+ }
1564
+ if (disableHttp3) {
1565
+ config.disable_http3 = true;
1566
+ }
1442
1567
  if (ja3) {
1443
1568
  config.ja3 = ja3;
1444
1569
  }
@@ -1602,7 +1727,7 @@ class Session {
1602
1727
  * @returns {Response} Response object
1603
1728
  */
1604
1729
  getSync(url, options = {}) {
1605
- const { headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
1730
+ const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false } = options;
1606
1731
 
1607
1732
  url = addParamsToUrl(url, params);
1608
1733
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1619,6 +1744,12 @@ class Session {
1619
1744
  if (fetchMode) {
1620
1745
  reqOptions.fetch_mode = fetchMode;
1621
1746
  }
1747
+ if (allowRedirects !== null && allowRedirects !== undefined) {
1748
+ reqOptions.follow_redirects = !!allowRedirects;
1749
+ }
1750
+ if (disableConditionalCache) {
1751
+ reqOptions.disable_conditional_cache = true;
1752
+ }
1622
1753
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1623
1754
 
1624
1755
  const startTime = Date.now();
@@ -1648,7 +1779,7 @@ class Session {
1648
1779
  * @returns {Response} Response object
1649
1780
  */
1650
1781
  postSync(url, options = {}) {
1651
- let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
1782
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false } = options;
1652
1783
 
1653
1784
  url = addParamsToUrl(url, params);
1654
1785
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1693,6 +1824,12 @@ class Session {
1693
1824
  if (fetchMode) {
1694
1825
  reqOptions.fetch_mode = fetchMode;
1695
1826
  }
1827
+ if (allowRedirects !== null && allowRedirects !== undefined) {
1828
+ reqOptions.follow_redirects = !!allowRedirects;
1829
+ }
1830
+ if (disableConditionalCache) {
1831
+ reqOptions.disable_conditional_cache = true;
1832
+ }
1696
1833
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1697
1834
 
1698
1835
  const bodyPtr = bodyBuffer || Buffer.alloc(0);
@@ -1717,7 +1854,7 @@ class Session {
1717
1854
  * @returns {Response} Response object
1718
1855
  */
1719
1856
  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;
1857
+ 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 } = options;
1721
1858
 
1722
1859
  url = addParamsToUrl(url, params);
1723
1860
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1760,6 +1897,8 @@ class Session {
1760
1897
  if (mergedHeaders) requestConfig.headers = mergedHeaders;
1761
1898
  if (timeout) requestConfig.timeout = timeout;
1762
1899
  if (fetchMode) requestConfig.fetch_mode = fetchMode;
1900
+ if (allowRedirects !== null && allowRedirects !== undefined) requestConfig.follow_redirects = !!allowRedirects;
1901
+ if (disableConditionalCache) requestConfig.disable_conditional_cache = true;
1763
1902
 
1764
1903
  const bodyPtr = bodyBuffer || Buffer.alloc(0);
1765
1904
  const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
@@ -1790,7 +1929,7 @@ class Session {
1790
1929
  * @returns {Promise<Response>} Response object
1791
1930
  */
1792
1931
  get(url, options = {}) {
1793
- const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
1932
+ const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null, allowRedirects = null, disableConditionalCache = false, signal = null } = options;
1794
1933
 
1795
1934
  url = addParamsToUrl(url, params);
1796
1935
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1813,11 +1952,17 @@ class Session {
1813
1952
  // httpcloak_request_async — that path also uses time.Second).
1814
1953
  reqOptions.timeout = timeout;
1815
1954
  }
1955
+ if (allowRedirects !== null && allowRedirects !== undefined) {
1956
+ reqOptions.follow_redirects = !!allowRedirects;
1957
+ }
1958
+ if (disableConditionalCache) {
1959
+ reqOptions.disable_conditional_cache = true;
1960
+ }
1816
1961
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1817
1962
 
1818
1963
  // Register async request with callback manager
1819
1964
  const manager = getAsyncManager();
1820
- const { callbackId, promise } = manager.registerRequest(this._lib);
1965
+ const { callbackId, promise } = manager.registerRequest(this._lib, signal);
1821
1966
 
1822
1967
  // Start async request
1823
1968
  this._lib.httpcloak_get_async(this._handle, url, optionsJson, callbackId);
@@ -1833,16 +1978,23 @@ class Session {
1833
1978
  * @returns {Promise<Response>} Response object
1834
1979
  */
1835
1980
  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;
1981
+ 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, signal = null } = options;
1837
1982
 
1838
1983
  url = addParamsToUrl(url, params);
1839
1984
  let mergedHeaders = this._mergeHeaders(headers);
1840
1985
 
1986
+ // bodyEncoding tells clib whether `body` is plain text or base64-encoded
1987
+ // binary. Default "" = text. Anything that carries arbitrary bytes
1988
+ // (Buffer, multipart) flows through as base64 so NUL bytes don't truncate
1989
+ // the C string at the cgo boundary.
1990
+ let bodyEncoding = "";
1991
+
1841
1992
  // Handle multipart file upload
1842
1993
  if (files !== null) {
1843
1994
  const formData = (data !== null && typeof data === "object") ? data : null;
1844
1995
  const multipart = encodeMultipart(formData, files);
1845
- body = multipart.body.toString("latin1");
1996
+ body = multipart.body.toString("base64");
1997
+ bodyEncoding = "base64";
1846
1998
  mergedHeaders = mergedHeaders || {};
1847
1999
  mergedHeaders["Content-Type"] = multipart.contentType;
1848
2000
  }
@@ -1862,9 +2014,11 @@ class Session {
1862
2014
  mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1863
2015
  }
1864
2016
  }
1865
- // Handle Buffer body
2017
+ // Handle Buffer body (binary payload). base64-encode so NUL bytes and
2018
+ // non-UTF-8 sequences survive the cgo C-string round-trip intact.
1866
2019
  else if (Buffer.isBuffer(body)) {
1867
- body = body.toString("utf8");
2020
+ body = body.toString("base64");
2021
+ bodyEncoding = "base64";
1868
2022
  }
1869
2023
 
1870
2024
  // Use request auth if provided, otherwise fall back to session auth
@@ -1884,11 +2038,20 @@ class Session {
1884
2038
  // Public API: seconds. clib post_async path enforces in seconds.
1885
2039
  reqOptions.timeout = timeout;
1886
2040
  }
2041
+ if (allowRedirects !== null && allowRedirects !== undefined) {
2042
+ reqOptions.follow_redirects = !!allowRedirects;
2043
+ }
2044
+ if (disableConditionalCache) {
2045
+ reqOptions.disable_conditional_cache = true;
2046
+ }
2047
+ if (bodyEncoding) {
2048
+ reqOptions.body_encoding = bodyEncoding;
2049
+ }
1887
2050
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1888
2051
 
1889
2052
  // Register async request with callback manager
1890
2053
  const manager = getAsyncManager();
1891
- const { callbackId, promise } = manager.registerRequest(this._lib);
2054
+ const { callbackId, promise } = manager.registerRequest(this._lib, signal);
1892
2055
 
1893
2056
  // Start async request
1894
2057
  this._lib.httpcloak_post_async(this._handle, url, body, optionsJson, callbackId);
@@ -1905,16 +2068,22 @@ class Session {
1905
2068
  * @returns {Promise<Response>} Response object
1906
2069
  */
1907
2070
  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;
2071
+ 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, signal = null } = options;
1909
2072
 
1910
2073
  url = addParamsToUrl(url, params);
1911
2074
  let mergedHeaders = this._mergeHeaders(headers);
1912
2075
 
2076
+ // bodyEncoding tells clib whether `body` is text or base64 binary.
2077
+ // Binary payloads (Buffer / multipart) flow through as base64 so they
2078
+ // survive JSON serialization + the cgo C-string boundary intact.
2079
+ let bodyEncoding = "";
2080
+
1913
2081
  // Handle multipart file upload
1914
2082
  if (files !== null) {
1915
2083
  const formData = (data !== null && typeof data === "object") ? data : null;
1916
2084
  const multipart = encodeMultipart(formData, files);
1917
- body = multipart.body.toString("latin1");
2085
+ body = multipart.body.toString("base64");
2086
+ bodyEncoding = "base64";
1918
2087
  mergedHeaders = mergedHeaders || {};
1919
2088
  mergedHeaders["Content-Type"] = multipart.contentType;
1920
2089
  }
@@ -1934,9 +2103,11 @@ class Session {
1934
2103
  mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1935
2104
  }
1936
2105
  }
1937
- // Handle Buffer body
2106
+ // Handle Buffer body (binary payload). base64-encode so non-UTF-8 bytes
2107
+ // and NUL bytes survive JSON.stringify + the C boundary.
1938
2108
  else if (Buffer.isBuffer(body)) {
1939
- body = body.toString("utf8");
2109
+ body = body.toString("base64");
2110
+ bodyEncoding = "base64";
1940
2111
  }
1941
2112
 
1942
2113
  // Use request auth if provided, otherwise fall back to session auth
@@ -1950,12 +2121,15 @@ class Session {
1950
2121
  };
1951
2122
  if (mergedHeaders) requestConfig.headers = mergedHeaders;
1952
2123
  if (body) requestConfig.body = body;
2124
+ if (bodyEncoding) requestConfig.body_encoding = bodyEncoding;
1953
2125
  if (timeout) requestConfig.timeout = timeout;
1954
2126
  if (fetchMode) requestConfig.fetch_mode = fetchMode;
2127
+ if (allowRedirects !== null && allowRedirects !== undefined) requestConfig.follow_redirects = !!allowRedirects;
2128
+ if (disableConditionalCache) requestConfig.disable_conditional_cache = true;
1955
2129
 
1956
2130
  // Register async request with callback manager
1957
2131
  const manager = getAsyncManager();
1958
- const { callbackId, promise } = manager.registerRequest(this._lib);
2132
+ const { callbackId, promise } = manager.registerRequest(this._lib, signal);
1959
2133
 
1960
2134
  // Start async request
1961
2135
  this._lib.httpcloak_request_async(this._handle, JSON.stringify(requestConfig), callbackId);
@@ -2103,6 +2277,122 @@ class Session {
2103
2277
  return this.getCookies();
2104
2278
  }
2105
2279
 
2280
+ // ===========================================================================
2281
+ // Conditional Cache and Redirect Runtime Control
2282
+ // ===========================================================================
2283
+
2284
+ /**
2285
+ * Drop the session's per-URL conditional-cache map (ETag / Last-Modified).
2286
+ * The next request to each URL goes out without If-None-Match /
2287
+ * If-Modified-Since headers. Cookies and TLS tickets are not touched.
2288
+ */
2289
+ clearCache() {
2290
+ this._lib.httpcloak_session_clear_cache(this._handle);
2291
+ }
2292
+
2293
+ /**
2294
+ * Return a snapshot of session counters, timestamps and transport-level
2295
+ * metrics. Useful for long-running scrapers that want per-session metrics
2296
+ * scraped into Prometheus / Datadog / etc.
2297
+ *
2298
+ * Keys: id, preset, created_at (Unix ns), last_used (Unix ns),
2299
+ * request_count, active, cookie_count, cache_entry_count, age_ns,
2300
+ * idle_time_ns, transport_stats (per-protocol object).
2301
+ *
2302
+ * @returns {Object} Stats snapshot, or empty object if the session is closed.
2303
+ */
2304
+ stats() {
2305
+ const ptr = this._lib.httpcloak_session_stats(this._handle);
2306
+ const raw = resultToString(ptr);
2307
+ if (!raw) return {};
2308
+ try { return JSON.parse(raw); } catch { return {}; }
2309
+ }
2310
+
2311
+ /**
2312
+ * Return the time since the session last serviced a request, in seconds.
2313
+ * Returns -1 if the session handle is invalid.
2314
+ *
2315
+ * @returns {number} Idle time in seconds (may be fractional).
2316
+ */
2317
+ idleTime() {
2318
+ const ns = Number(this._lib.httpcloak_session_idle_time(this._handle));
2319
+ if (ns < 0) return -1;
2320
+ return ns / 1_000_000_000;
2321
+ }
2322
+
2323
+ /**
2324
+ * Return true if the session is still usable (close() has not been called
2325
+ * and the handle is valid).
2326
+ *
2327
+ * @returns {boolean}
2328
+ */
2329
+ isActive() {
2330
+ return this._lib.httpcloak_session_is_active(this._handle) === 1;
2331
+ }
2332
+
2333
+ /**
2334
+ * Reset the idle timer to now without issuing a request. Useful in
2335
+ * long-running pools where an external heartbeat shouldn't let a session
2336
+ * look idle to a reaper.
2337
+ */
2338
+ touch() {
2339
+ this._lib.httpcloak_session_touch(this._handle);
2340
+ }
2341
+
2342
+ /**
2343
+ * Toggle the session's ETag / If-Modified-Since handling at runtime.
2344
+ * When disabled, the session stops injecting cache validators on outgoing
2345
+ * requests and stops storing them from responses; the existing cache map
2346
+ * is preserved (re-enabling resumes using it). Pair with clearCache() to
2347
+ * also wipe previously-stored validators.
2348
+ * @param {boolean} enabled
2349
+ */
2350
+ setConditionalCache(enabled) {
2351
+ this._lib.httpcloak_session_set_conditional_cache(this._handle, enabled ? 1 : 0);
2352
+ }
2353
+
2354
+ /**
2355
+ * Return the session's current conditional-cache state.
2356
+ * @returns {boolean}
2357
+ */
2358
+ getConditionalCache() {
2359
+ return this._lib.httpcloak_session_get_conditional_cache(this._handle) !== 0;
2360
+ }
2361
+
2362
+ /**
2363
+ * Toggle the session's redirect-following policy at runtime. The change
2364
+ * takes effect on the next request and persists until set again.
2365
+ * @param {boolean} enabled
2366
+ */
2367
+ setFollowRedirects(enabled) {
2368
+ this._lib.httpcloak_session_set_follow_redirects(this._handle, enabled ? 1 : 0);
2369
+ }
2370
+
2371
+ /**
2372
+ * Return the session's current redirect-following policy.
2373
+ * @returns {boolean}
2374
+ */
2375
+ getFollowRedirects() {
2376
+ return this._lib.httpcloak_session_get_follow_redirects(this._handle) !== 0;
2377
+ }
2378
+
2379
+ /**
2380
+ * Update the session's redirect cap at runtime. Values of zero or below
2381
+ * are ignored, leaving the prior cap (or the default of 10) in place.
2382
+ * @param {number} max
2383
+ */
2384
+ setMaxRedirects(max) {
2385
+ this._lib.httpcloak_session_set_max_redirects(this._handle, max);
2386
+ }
2387
+
2388
+ /**
2389
+ * Return the session's current redirect cap.
2390
+ * @returns {number}
2391
+ */
2392
+ getMaxRedirects() {
2393
+ return this._lib.httpcloak_session_get_max_redirects(this._handle);
2394
+ }
2395
+
2106
2396
  // ===========================================================================
2107
2397
  // Proxy Management
2108
2398
  // ===========================================================================
@@ -2399,7 +2689,7 @@ class Session {
2399
2689
  * stream.close();
2400
2690
  */
2401
2691
  getStream(url, options = {}) {
2402
- const { params, headers, cookies, timeout } = options;
2692
+ const { params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false } = options;
2403
2693
 
2404
2694
  // Add params to URL
2405
2695
  if (params) {
@@ -2426,6 +2716,12 @@ class Session {
2426
2716
  if (timeout) {
2427
2717
  reqOptions.timeout = timeout;
2428
2718
  }
2719
+ if (allowRedirects !== null && allowRedirects !== undefined) {
2720
+ reqOptions.follow_redirects = !!allowRedirects;
2721
+ }
2722
+ if (disableConditionalCache) {
2723
+ reqOptions.disable_conditional_cache = true;
2724
+ }
2429
2725
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
2430
2726
 
2431
2727
  // Start stream
@@ -2465,7 +2761,7 @@ class Session {
2465
2761
  * @returns {StreamResponse} - Streaming response for chunked reading
2466
2762
  */
2467
2763
  postStream(url, options = {}) {
2468
- const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout } = options;
2764
+ const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false } = options;
2469
2765
 
2470
2766
  // Add params to URL
2471
2767
  if (params) {
@@ -2505,6 +2801,12 @@ class Session {
2505
2801
  if (timeout) {
2506
2802
  reqOptions.timeout = timeout;
2507
2803
  }
2804
+ if (allowRedirects !== null && allowRedirects !== undefined) {
2805
+ reqOptions.follow_redirects = !!allowRedirects;
2806
+ }
2807
+ if (disableConditionalCache) {
2808
+ reqOptions.disable_conditional_cache = true;
2809
+ }
2508
2810
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
2509
2811
 
2510
2812
  // Start stream
@@ -2543,7 +2845,7 @@ class Session {
2543
2845
  * @returns {StreamResponse} - Streaming response for chunked reading
2544
2846
  */
2545
2847
  requestStream(method, url, options = {}) {
2546
- const { body, params, headers, cookies, timeout } = options;
2848
+ const { body, params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false } = options;
2547
2849
 
2548
2850
  // Add params to URL
2549
2851
  if (params) {
@@ -2576,6 +2878,12 @@ class Session {
2576
2878
  if (timeout) {
2577
2879
  requestConfig.timeout = timeout;
2578
2880
  }
2881
+ if (allowRedirects !== null && allowRedirects !== undefined) {
2882
+ requestConfig.follow_redirects = !!allowRedirects;
2883
+ }
2884
+ if (disableConditionalCache) {
2885
+ requestConfig.disable_conditional_cache = true;
2886
+ }
2579
2887
 
2580
2888
  // Start stream
2581
2889
  const streamHandle = this._lib.httpcloak_stream_request(this._handle, JSON.stringify(requestConfig));
@@ -2894,6 +3202,84 @@ class Session {
2894
3202
  patchFast(url, options = {}) {
2895
3203
  return this.requestFast("PATCH", url, options);
2896
3204
  }
3205
+
3206
+ /**
3207
+ * Stream an arbitrary-sized body to the wire without buffering in memory.
3208
+ *
3209
+ * `chunks` is anything iterable that yields Buffer-shaped objects: an
3210
+ * AsyncIterable&lt;Buffer&gt; (e.g. an `fs.createReadStream(path)`), an
3211
+ * Iterable&lt;Buffer&gt; (e.g. an array), or a sync generator that yields
3212
+ * Buffers. String chunks are auto-encoded as UTF-8.
3213
+ *
3214
+ * The Go side opens an `io.Pipe()` for the body when uploadStart returns;
3215
+ * each chunk is written straight through with no base64 / no JSON wrapping.
3216
+ * On error the partial upload is cancelled and the underlying connection
3217
+ * closed; on success uploadFinish reads the full response and parses it
3218
+ * into a regular `Response` (same shape as a normal post()).
3219
+ *
3220
+ * @param {string} method HTTP method (POST / PUT / PATCH typically)
3221
+ * @param {string} url
3222
+ * @param {AsyncIterable&lt;Buffer&gt;|Iterable&lt;Buffer&gt;} chunks Body chunks
3223
+ * @param {Object} [options]
3224
+ * @param {Object} [options.headers]
3225
+ * @param {string} [options.contentType="application/octet-stream"]
3226
+ * @param {number} [options.timeout] Per-request timeout in ms
3227
+ * @returns {Promise<Response>}
3228
+ */
3229
+ async uploadStream(method, url, chunks, options = {}) {
3230
+ const { headers = null, contentType = "application/octet-stream", timeout = null } = options;
3231
+ const mergedHeaders = this._mergeHeaders(headers) || {};
3232
+ const optsJson = JSON.stringify({
3233
+ method: method.toUpperCase(),
3234
+ headers: mergedHeaders,
3235
+ content_type: contentType,
3236
+ ...(timeout ? { timeout } : {}),
3237
+ });
3238
+
3239
+ const uploadHandle = this._lib.httpcloak_upload_start(this._handle, url, optsJson);
3240
+ if (uploadHandle <= 0) {
3241
+ throw new HTTPCloakError("Failed to start streaming upload");
3242
+ }
3243
+
3244
+ const startTime = Date.now();
3245
+ try {
3246
+ for await (const chunk of chunks) {
3247
+ const buf = Buffer.isBuffer(chunk)
3248
+ ? chunk
3249
+ : typeof chunk === "string"
3250
+ ? Buffer.from(chunk, "utf8")
3251
+ : Buffer.from(chunk);
3252
+ if (buf.length === 0) continue;
3253
+ const written = this._lib.httpcloak_upload_write_raw(uploadHandle, buf, buf.length);
3254
+ if (written < 0) {
3255
+ throw new HTTPCloakError("Failed to write upload chunk");
3256
+ }
3257
+ }
3258
+
3259
+ const resultPtr = this._lib.httpcloak_upload_finish(uploadHandle);
3260
+ const elapsed = Date.now() - startTime;
3261
+ const raw = resultToString(resultPtr);
3262
+ if (!raw) {
3263
+ throw new HTTPCloakError("Empty response from streaming upload");
3264
+ }
3265
+ const data = JSON.parse(raw);
3266
+ if (data.error) {
3267
+ throw new HTTPCloakError(data.error);
3268
+ }
3269
+ return new Response(data, elapsed);
3270
+ } catch (err) {
3271
+ try { this._lib.httpcloak_upload_cancel(uploadHandle); } catch { /* best-effort */ }
3272
+ throw err;
3273
+ }
3274
+ }
3275
+
3276
+ /**
3277
+ * Convenience wrapper: streaming POST. Same semantics as
3278
+ * {@link uploadStream}('POST', ...).
3279
+ */
3280
+ postUpload(url, chunks, options = {}) {
3281
+ return this.uploadStream("POST", url, chunks, options);
3282
+ }
2897
3283
  }
2898
3284
 
2899
3285
  // =============================================================================
@@ -3227,6 +3613,41 @@ class LocalProxy {
3227
3613
  return result === 1;
3228
3614
  }
3229
3615
 
3616
+ /**
3617
+ * Return the IDs of every session currently registered on this proxy.
3618
+ *
3619
+ * These are the same IDs the X-HTTPCloak-Session header accepts for
3620
+ * per-request session routing. Useful for sanity checks, GC of stale
3621
+ * registrations in long-running processes, and operational dashboards.
3622
+ *
3623
+ * @returns {string[]} List of registered session IDs (empty if none).
3624
+ */
3625
+ listSessions() {
3626
+ const resultPtr = this._lib.httpcloak_local_proxy_list_sessions(this._handle);
3627
+ const raw = resultToString(resultPtr);
3628
+ if (!raw) return [];
3629
+ try {
3630
+ const parsed = JSON.parse(raw);
3631
+ return Array.isArray(parsed) ? parsed.map(String) : [];
3632
+ } catch {
3633
+ return [];
3634
+ }
3635
+ }
3636
+
3637
+ /**
3638
+ * Return true if a session with the given ID is currently registered.
3639
+ *
3640
+ * Cheaper than listSessions() + .includes() when callers only need an
3641
+ * existence check.
3642
+ *
3643
+ * @param {string} sessionId
3644
+ * @returns {boolean}
3645
+ */
3646
+ hasSession(sessionId) {
3647
+ if (!sessionId) return false;
3648
+ return this._lib.httpcloak_local_proxy_has_session(this._handle, sessionId) === 1;
3649
+ }
3650
+
3230
3651
  /**
3231
3652
  * Stop the local proxy server.
3232
3653
  */
package/lib/index.mjs CHANGED
@@ -22,10 +22,17 @@ export const Cookie = cjs.Cookie;
22
22
  export const RedirectInfo = cjs.RedirectInfo;
23
23
  export const HTTPCloakError = cjs.HTTPCloakError;
24
24
  export const SessionCacheBackend = cjs.SessionCacheBackend;
25
+ export const PresetPool = cjs.PresetPool;
25
26
 
26
27
  // Presets
27
28
  export const Preset = cjs.Preset;
28
29
 
30
+ // Custom preset loading
31
+ export const loadPreset = cjs.loadPreset;
32
+ export const loadPresetFromJSON = cjs.loadPresetFromJSON;
33
+ export const unregisterPreset = cjs.unregisterPreset;
34
+ export const describePreset = cjs.describePreset;
35
+
29
36
  // Configuration
30
37
  export const configure = cjs.configure;
31
38
  export const configureSessionCache = cjs.configureSessionCache;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/darwin-arm64",
3
- "version": "1.6.6",
3
+ "version": "1.6.7",
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.6",
3
+ "version": "1.6.7",
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.6",
3
+ "version": "1.6.7",
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.6",
3
+ "version": "1.6.7",
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.6",
3
+ "version": "1.6.7",
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.6",
3
+ "version": "1.6.7",
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,11 @@
49
49
  "koffi": "^2.9.0"
50
50
  },
51
51
  "optionalDependencies": {
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"
52
+ "@httpcloak/darwin-arm64": "1.6.7",
53
+ "@httpcloak/darwin-x64": "1.6.7",
54
+ "@httpcloak/linux-arm64": "1.6.7",
55
+ "@httpcloak/linux-x64": "1.6.7",
56
+ "@httpcloak/win32-x64": "1.6.7"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/node": "^25.1.0",