httpcloak 1.6.5 → 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;
@@ -241,12 +241,30 @@ export interface SessionOptions {
241
241
  enableSpeculativeTls?: boolean;
242
242
  /** Protocol to switch to after Refresh() (e.g., "h1", "h2", "h3") */
243
243
  switchProtocol?: string;
244
+ /** Disable internal cookie jar entirely — caller manages cookies via per-request headers (default: false) */
245
+ withoutCookieJar?: boolean;
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;
244
252
  /** Custom JA3 fingerprint string (e.g., "771,4865-4866-4867-...,0-23-65281-...,29-23-24,0") */
245
253
  ja3?: string;
246
254
  /** Custom Akamai HTTP/2 fingerprint string (e.g., "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p") */
247
255
  akamai?: string;
248
256
  /** Extra fingerprint options: { tls_alpn, tls_signature_algorithms, tls_cert_compression, tls_permute_extensions } */
249
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;
250
268
  }
251
269
 
252
270
  export interface RequestOptions {
@@ -281,6 +299,64 @@ export interface RequestOptions {
281
299
  * Set this explicitly when the auto-sniff gets it wrong (e.g., POST to a CORS endpoint without a JSON Accept header).
282
300
  */
283
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>;
284
360
  }
285
361
 
286
362
  export class Session {
@@ -311,8 +387,11 @@ export class Session {
311
387
  /** Refresh the session by closing all connections while keeping TLS session tickets.
312
388
  * This simulates a browser page refresh - connections are severed but 0-RTT
313
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.
314
393
  */
315
- refresh(): void;
394
+ refresh(switchProtocol?: "h1" | "h2" | "h3"): void;
316
395
 
317
396
  // Synchronous methods
318
397
  /** Perform a synchronous GET request */
@@ -357,17 +436,11 @@ export class Session {
357
436
  /** Get a specific cookie by name with full metadata */
358
437
  getCookieDetailed(name: string): Cookie | null;
359
438
 
360
- /**
361
- * Get all cookies as a flat name-value object.
362
- * @deprecated Will return Cookie[] with full metadata in a future release. Use getCookiesDetailed() for the new format now.
363
- */
364
- 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[];
365
441
 
366
- /**
367
- * Get a specific cookie value by name.
368
- * @deprecated Will return Cookie|null in a future release. Use getCookieDetailed() for the new format now.
369
- */
370
- 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;
371
444
 
372
445
  /** Set a cookie in the session */
373
446
  setCookie(
@@ -390,11 +463,72 @@ export class Session {
390
463
  /** Clear all cookies from the session */
391
464
  clearCookies(): void;
392
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
+
393
524
  /**
394
- * Get cookies as a property.
395
- * @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.
396
527
  */
397
- readonly cookies: Record<string, string>;
528
+ setMaxRedirects(max: number): void;
529
+
530
+ /** Read the session's current redirect cap. */
531
+ getMaxRedirects(): number;
398
532
 
399
533
  // Proxy management
400
534
 
@@ -655,6 +789,37 @@ export class Session {
655
789
  * @returns FastResponse with Buffer body
656
790
  */
657
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>;
658
823
  }
659
824
 
660
825
  export interface LocalProxyOptions {
@@ -675,16 +840,20 @@ export interface LocalProxyOptions {
675
840
  }
676
841
 
677
842
  export interface LocalProxyStats {
678
- /** Total number of requests processed */
679
- totalRequests: number;
680
- /** Number of active connections */
681
- activeConnections: number;
682
- /** Number of failed requests */
683
- failedRequests: number;
684
- /** Bytes sent */
685
- bytesSent: number;
686
- /** Bytes received */
687
- 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;
688
857
  }
689
858
 
690
859
  /**
@@ -788,6 +957,22 @@ export class LocalProxy {
788
957
  */
789
958
  unregisterSession(sessionId: string): boolean;
790
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
+
791
976
  /**
792
977
  * Stop and close the proxy.
793
978
  * After closing, the LocalProxy instance cannot be reused.
@@ -798,8 +983,30 @@ export class LocalProxy {
798
983
  /** Get the httpcloak library version */
799
984
  export function version(): string;
800
985
 
801
- /** Get list of available browser presets */
802
- 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;
803
1010
 
804
1011
  /**
805
1012
  * Configure the DNS servers used for ECH (Encrypted Client Hello) config queries.
@@ -857,6 +1064,28 @@ export function request(method: string, url: string, options?: RequestOptions):
857
1064
 
858
1065
  /** Available browser presets */
859
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;
860
1089
  CHROME_146: string;
861
1090
  CHROME_146_WINDOWS: string;
862
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
  }
@@ -1361,6 +1474,10 @@ class Session {
1361
1474
  keyLogFile = null,
1362
1475
  enableSpeculativeTls = false,
1363
1476
  switchProtocol = null,
1477
+ withoutCookieJar = false,
1478
+ withoutConditionalCache = false,
1479
+ disableEch = false,
1480
+ disableHttp3 = false,
1364
1481
  ja3 = null,
1365
1482
  akamai = null,
1366
1483
  extraFp = null,
@@ -1435,6 +1552,18 @@ class Session {
1435
1552
  if (switchProtocol) {
1436
1553
  config.switch_protocol = switchProtocol;
1437
1554
  }
1555
+ if (withoutCookieJar) {
1556
+ config.without_cookie_jar = true;
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
+ }
1438
1567
  if (ja3) {
1439
1568
  config.ja3 = ja3;
1440
1569
  }
@@ -1598,7 +1727,7 @@ class Session {
1598
1727
  * @returns {Response} Response object
1599
1728
  */
1600
1729
  getSync(url, options = {}) {
1601
- 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;
1602
1731
 
1603
1732
  url = addParamsToUrl(url, params);
1604
1733
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1615,6 +1744,12 @@ class Session {
1615
1744
  if (fetchMode) {
1616
1745
  reqOptions.fetch_mode = fetchMode;
1617
1746
  }
1747
+ if (allowRedirects !== null && allowRedirects !== undefined) {
1748
+ reqOptions.follow_redirects = !!allowRedirects;
1749
+ }
1750
+ if (disableConditionalCache) {
1751
+ reqOptions.disable_conditional_cache = true;
1752
+ }
1618
1753
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1619
1754
 
1620
1755
  const startTime = Date.now();
@@ -1644,7 +1779,7 @@ class Session {
1644
1779
  * @returns {Response} Response object
1645
1780
  */
1646
1781
  postSync(url, options = {}) {
1647
- 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;
1648
1783
 
1649
1784
  url = addParamsToUrl(url, params);
1650
1785
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1689,6 +1824,12 @@ class Session {
1689
1824
  if (fetchMode) {
1690
1825
  reqOptions.fetch_mode = fetchMode;
1691
1826
  }
1827
+ if (allowRedirects !== null && allowRedirects !== undefined) {
1828
+ reqOptions.follow_redirects = !!allowRedirects;
1829
+ }
1830
+ if (disableConditionalCache) {
1831
+ reqOptions.disable_conditional_cache = true;
1832
+ }
1692
1833
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1693
1834
 
1694
1835
  const bodyPtr = bodyBuffer || Buffer.alloc(0);
@@ -1713,7 +1854,7 @@ class Session {
1713
1854
  * @returns {Response} Response object
1714
1855
  */
1715
1856
  requestSync(method, url, options = {}) {
1716
- 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;
1717
1858
 
1718
1859
  url = addParamsToUrl(url, params);
1719
1860
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1756,6 +1897,8 @@ class Session {
1756
1897
  if (mergedHeaders) requestConfig.headers = mergedHeaders;
1757
1898
  if (timeout) requestConfig.timeout = timeout;
1758
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;
1759
1902
 
1760
1903
  const bodyPtr = bodyBuffer || Buffer.alloc(0);
1761
1904
  const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
@@ -1786,7 +1929,7 @@ class Session {
1786
1929
  * @returns {Promise<Response>} Response object
1787
1930
  */
1788
1931
  get(url, options = {}) {
1789
- 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;
1790
1933
 
1791
1934
  url = addParamsToUrl(url, params);
1792
1935
  let mergedHeaders = this._mergeHeaders(headers);
@@ -1809,11 +1952,17 @@ class Session {
1809
1952
  // httpcloak_request_async — that path also uses time.Second).
1810
1953
  reqOptions.timeout = timeout;
1811
1954
  }
1955
+ if (allowRedirects !== null && allowRedirects !== undefined) {
1956
+ reqOptions.follow_redirects = !!allowRedirects;
1957
+ }
1958
+ if (disableConditionalCache) {
1959
+ reqOptions.disable_conditional_cache = true;
1960
+ }
1812
1961
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1813
1962
 
1814
1963
  // Register async request with callback manager
1815
1964
  const manager = getAsyncManager();
1816
- const { callbackId, promise } = manager.registerRequest(this._lib);
1965
+ const { callbackId, promise } = manager.registerRequest(this._lib, signal);
1817
1966
 
1818
1967
  // Start async request
1819
1968
  this._lib.httpcloak_get_async(this._handle, url, optionsJson, callbackId);
@@ -1829,16 +1978,23 @@ class Session {
1829
1978
  * @returns {Promise<Response>} Response object
1830
1979
  */
1831
1980
  post(url, options = {}) {
1832
- 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;
1833
1982
 
1834
1983
  url = addParamsToUrl(url, params);
1835
1984
  let mergedHeaders = this._mergeHeaders(headers);
1836
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
+
1837
1992
  // Handle multipart file upload
1838
1993
  if (files !== null) {
1839
1994
  const formData = (data !== null && typeof data === "object") ? data : null;
1840
1995
  const multipart = encodeMultipart(formData, files);
1841
- body = multipart.body.toString("latin1");
1996
+ body = multipart.body.toString("base64");
1997
+ bodyEncoding = "base64";
1842
1998
  mergedHeaders = mergedHeaders || {};
1843
1999
  mergedHeaders["Content-Type"] = multipart.contentType;
1844
2000
  }
@@ -1858,9 +2014,11 @@ class Session {
1858
2014
  mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1859
2015
  }
1860
2016
  }
1861
- // 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.
1862
2019
  else if (Buffer.isBuffer(body)) {
1863
- body = body.toString("utf8");
2020
+ body = body.toString("base64");
2021
+ bodyEncoding = "base64";
1864
2022
  }
1865
2023
 
1866
2024
  // Use request auth if provided, otherwise fall back to session auth
@@ -1880,11 +2038,20 @@ class Session {
1880
2038
  // Public API: seconds. clib post_async path enforces in seconds.
1881
2039
  reqOptions.timeout = timeout;
1882
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
+ }
1883
2050
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1884
2051
 
1885
2052
  // Register async request with callback manager
1886
2053
  const manager = getAsyncManager();
1887
- const { callbackId, promise } = manager.registerRequest(this._lib);
2054
+ const { callbackId, promise } = manager.registerRequest(this._lib, signal);
1888
2055
 
1889
2056
  // Start async request
1890
2057
  this._lib.httpcloak_post_async(this._handle, url, body, optionsJson, callbackId);
@@ -1901,16 +2068,22 @@ class Session {
1901
2068
  * @returns {Promise<Response>} Response object
1902
2069
  */
1903
2070
  request(method, url, options = {}) {
1904
- 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;
1905
2072
 
1906
2073
  url = addParamsToUrl(url, params);
1907
2074
  let mergedHeaders = this._mergeHeaders(headers);
1908
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
+
1909
2081
  // Handle multipart file upload
1910
2082
  if (files !== null) {
1911
2083
  const formData = (data !== null && typeof data === "object") ? data : null;
1912
2084
  const multipart = encodeMultipart(formData, files);
1913
- body = multipart.body.toString("latin1");
2085
+ body = multipart.body.toString("base64");
2086
+ bodyEncoding = "base64";
1914
2087
  mergedHeaders = mergedHeaders || {};
1915
2088
  mergedHeaders["Content-Type"] = multipart.contentType;
1916
2089
  }
@@ -1930,9 +2103,11 @@ class Session {
1930
2103
  mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1931
2104
  }
1932
2105
  }
1933
- // 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.
1934
2108
  else if (Buffer.isBuffer(body)) {
1935
- body = body.toString("utf8");
2109
+ body = body.toString("base64");
2110
+ bodyEncoding = "base64";
1936
2111
  }
1937
2112
 
1938
2113
  // Use request auth if provided, otherwise fall back to session auth
@@ -1946,12 +2121,15 @@ class Session {
1946
2121
  };
1947
2122
  if (mergedHeaders) requestConfig.headers = mergedHeaders;
1948
2123
  if (body) requestConfig.body = body;
2124
+ if (bodyEncoding) requestConfig.body_encoding = bodyEncoding;
1949
2125
  if (timeout) requestConfig.timeout = timeout;
1950
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;
1951
2129
 
1952
2130
  // Register async request with callback manager
1953
2131
  const manager = getAsyncManager();
1954
- const { callbackId, promise } = manager.registerRequest(this._lib);
2132
+ const { callbackId, promise } = manager.registerRequest(this._lib, signal);
1955
2133
 
1956
2134
  // Start async request
1957
2135
  this._lib.httpcloak_request_async(this._handle, JSON.stringify(requestConfig), callbackId);
@@ -2099,6 +2277,122 @@ class Session {
2099
2277
  return this.getCookies();
2100
2278
  }
2101
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
+
2102
2396
  // ===========================================================================
2103
2397
  // Proxy Management
2104
2398
  // ===========================================================================
@@ -2395,7 +2689,7 @@ class Session {
2395
2689
  * stream.close();
2396
2690
  */
2397
2691
  getStream(url, options = {}) {
2398
- const { params, headers, cookies, timeout } = options;
2692
+ const { params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false } = options;
2399
2693
 
2400
2694
  // Add params to URL
2401
2695
  if (params) {
@@ -2422,6 +2716,12 @@ class Session {
2422
2716
  if (timeout) {
2423
2717
  reqOptions.timeout = timeout;
2424
2718
  }
2719
+ if (allowRedirects !== null && allowRedirects !== undefined) {
2720
+ reqOptions.follow_redirects = !!allowRedirects;
2721
+ }
2722
+ if (disableConditionalCache) {
2723
+ reqOptions.disable_conditional_cache = true;
2724
+ }
2425
2725
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
2426
2726
 
2427
2727
  // Start stream
@@ -2461,7 +2761,7 @@ class Session {
2461
2761
  * @returns {StreamResponse} - Streaming response for chunked reading
2462
2762
  */
2463
2763
  postStream(url, options = {}) {
2464
- 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;
2465
2765
 
2466
2766
  // Add params to URL
2467
2767
  if (params) {
@@ -2501,6 +2801,12 @@ class Session {
2501
2801
  if (timeout) {
2502
2802
  reqOptions.timeout = timeout;
2503
2803
  }
2804
+ if (allowRedirects !== null && allowRedirects !== undefined) {
2805
+ reqOptions.follow_redirects = !!allowRedirects;
2806
+ }
2807
+ if (disableConditionalCache) {
2808
+ reqOptions.disable_conditional_cache = true;
2809
+ }
2504
2810
  const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
2505
2811
 
2506
2812
  // Start stream
@@ -2539,7 +2845,7 @@ class Session {
2539
2845
  * @returns {StreamResponse} - Streaming response for chunked reading
2540
2846
  */
2541
2847
  requestStream(method, url, options = {}) {
2542
- const { body, params, headers, cookies, timeout } = options;
2848
+ const { body, params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false } = options;
2543
2849
 
2544
2850
  // Add params to URL
2545
2851
  if (params) {
@@ -2572,6 +2878,12 @@ class Session {
2572
2878
  if (timeout) {
2573
2879
  requestConfig.timeout = timeout;
2574
2880
  }
2881
+ if (allowRedirects !== null && allowRedirects !== undefined) {
2882
+ requestConfig.follow_redirects = !!allowRedirects;
2883
+ }
2884
+ if (disableConditionalCache) {
2885
+ requestConfig.disable_conditional_cache = true;
2886
+ }
2575
2887
 
2576
2888
  // Start stream
2577
2889
  const streamHandle = this._lib.httpcloak_stream_request(this._handle, JSON.stringify(requestConfig));
@@ -2890,6 +3202,84 @@ class Session {
2890
3202
  patchFast(url, options = {}) {
2891
3203
  return this.requestFast("PATCH", url, options);
2892
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
+ }
2893
3283
  }
2894
3284
 
2895
3285
  // =============================================================================
@@ -3223,6 +3613,41 @@ class LocalProxy {
3223
3613
  return result === 1;
3224
3614
  }
3225
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
+
3226
3651
  /**
3227
3652
  * Stop the local proxy server.
3228
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.5",
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.5",
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.5",
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.5",
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.5",
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.5",
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,12 +49,11 @@
49
49
  "koffi": "^2.9.0"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@httpcloak/darwin-arm64": "1.6.5",
53
- "@httpcloak/darwin-x64": "1.6.5",
54
- "@httpcloak/linux-arm64": "1.6.5",
55
- "@httpcloak/linux-x64": "1.6.5",
56
- "@httpcloak/win32-arm64": "1.6.5",
57
- "@httpcloak/win32-x64": "1.6.5"
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"
58
57
  },
59
58
  "devDependencies": {
60
59
  "@types/node": "^25.1.0",
@@ -8,13 +8,17 @@ const path = require("path");
8
8
 
9
9
  const VERSION = "1.4.0";
10
10
 
11
+ // Platforms we actually build and publish via CI's npm-platform matrix.
12
+ // Keep in sync with .github/workflows/bindings.yml's `npm_platform` matrix —
13
+ // adding a row here without adding it to the matrix means npm install will
14
+ // fail with an unresolvable optional dependency on yarn classic / pnpm
15
+ // strict modes.
11
16
  const PLATFORMS = [
12
17
  { name: "linux-x64", os: "linux", cpu: "x64", libName: "libhttpcloak-linux-amd64.so" },
13
18
  { name: "linux-arm64", os: "linux", cpu: "arm64", libName: "libhttpcloak-linux-arm64.so" },
14
19
  { name: "darwin-x64", os: "darwin", cpu: "x64", libName: "libhttpcloak-darwin-amd64.dylib" },
15
20
  { name: "darwin-arm64", os: "darwin", cpu: "arm64", libName: "libhttpcloak-darwin-arm64.dylib" },
16
21
  { name: "win32-x64", os: "win32", cpu: "x64", libName: "libhttpcloak-windows-amd64.dll" },
17
- { name: "win32-arm64", os: "win32", cpu: "arm64", libName: "libhttpcloak-windows-arm64.dll" },
18
22
  ];
19
23
 
20
24
  const npmDir = path.join(__dirname, "..", "npm");
@@ -1,3 +0,0 @@
1
- // Auto-generated - exports path to native library
2
- const path = require("path");
3
- module.exports = path.join(__dirname, "libhttpcloak-windows-arm64.dll");
@@ -1,6 +0,0 @@
1
- // Auto-generated - exports path to native library (ESM)
2
- import { fileURLToPath } from "url";
3
- import { dirname, join } from "path";
4
-
5
- const __dirname = dirname(fileURLToPath(import.meta.url));
6
- export default join(__dirname, "libhttpcloak-windows-arm64.dll");
@@ -1,27 +0,0 @@
1
- {
2
- "name": "@httpcloak/win32-arm64",
3
- "version": "1.6.5",
4
- "description": "HTTPCloak native binary for win32 arm64",
5
- "os": [
6
- "win32"
7
- ],
8
- "cpu": [
9
- "arm64"
10
- ],
11
- "main": "lib.js",
12
- "module": "lib.mjs",
13
- "exports": {
14
- ".": {
15
- "import": "./lib.mjs",
16
- "require": "./lib.js"
17
- }
18
- },
19
- "license": "MIT",
20
- "repository": {
21
- "type": "git",
22
- "url": "https://github.com/sardanioss/httpcloak"
23
- },
24
- "publishConfig": {
25
- "access": "public"
26
- }
27
- }