httpcloak 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -99,13 +99,123 @@ session.postCb(
99
99
  );
100
100
  ```
101
101
 
102
- ### With Proxy
102
+ ## Proxy Support
103
+
104
+ HTTPCloak supports HTTP, SOCKS5, and HTTP/3 (MASQUE) proxies with full fingerprint preservation.
105
+
106
+ ### HTTP Proxy
103
107
 
104
108
  ```javascript
109
+ const { Session } = require("httpcloak");
110
+
111
+ // Basic HTTP proxy
105
112
  const session = new Session({
113
+ preset: "chrome-143",
114
+ proxy: "http://host:port",
115
+ });
116
+
117
+ // With authentication
118
+ const sessionAuth = new Session({
106
119
  preset: "chrome-143",
107
120
  proxy: "http://user:pass@host:port",
108
121
  });
122
+
123
+ // HTTPS proxy
124
+ const sessionHttps = new Session({
125
+ preset: "chrome-143",
126
+ proxy: "https://user:pass@host:port",
127
+ });
128
+ ```
129
+
130
+ ### SOCKS5 Proxy
131
+
132
+ ```javascript
133
+ const { Session } = require("httpcloak");
134
+
135
+ // SOCKS5 proxy (with DNS resolution on proxy)
136
+ const session = new Session({
137
+ preset: "chrome-143",
138
+ proxy: "socks5h://host:port",
139
+ });
140
+
141
+ // With authentication
142
+ const sessionAuth = new Session({
143
+ preset: "chrome-143",
144
+ proxy: "socks5h://user:pass@host:port",
145
+ });
146
+
147
+ const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
148
+ console.log(response.protocol); // h3 (HTTP/3 through SOCKS5!)
149
+ ```
150
+
151
+ ### HTTP/3 MASQUE Proxy
152
+
153
+ MASQUE (RFC 9484) enables HTTP/3 connections through compatible proxies:
154
+
155
+ ```javascript
156
+ const { Session } = require("httpcloak");
157
+
158
+ // MASQUE proxy (auto-detected for known providers like Bright Data)
159
+ const session = new Session({
160
+ preset: "chrome-143",
161
+ proxy: "https://user:pass@brd.superproxy.io:10001",
162
+ });
163
+
164
+ const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
165
+ console.log(response.protocol); // h3
166
+ ```
167
+
168
+ ## Advanced Features
169
+
170
+ ### Encrypted Client Hello (ECH)
171
+
172
+ ECH encrypts the SNI (Server Name Indication) to prevent traffic analysis. Works with all Cloudflare domains:
173
+
174
+ ```javascript
175
+ const { Session } = require("httpcloak");
176
+
177
+ // Enable ECH for Cloudflare domains
178
+ const session = new Session({
179
+ preset: "chrome-143",
180
+ echConfigDomain: "cloudflare-ech.com",
181
+ });
182
+
183
+ const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
184
+ console.log(response.text);
185
+ // Output includes: sni=encrypted, http=http/3
186
+ ```
187
+
188
+ ### Domain Fronting (Connect-To)
189
+
190
+ Connect to one server while requesting a different domain:
191
+
192
+ ```javascript
193
+ const { Session } = require("httpcloak");
194
+
195
+ // Connect to claude.ai's IP but request www.cloudflare.com
196
+ const session = new Session({
197
+ preset: "chrome-143",
198
+ connectTo: { "www.cloudflare.com": "claude.ai" },
199
+ });
200
+
201
+ const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
202
+ ```
203
+
204
+ ### Combined: SOCKS5 + ECH
205
+
206
+ Get HTTP/3 with encrypted SNI through a SOCKS5 proxy:
207
+
208
+ ```javascript
209
+ const { Session } = require("httpcloak");
210
+
211
+ const session = new Session({
212
+ preset: "chrome-143",
213
+ proxy: "socks5h://user:pass@host:port",
214
+ echConfigDomain: "cloudflare-ech.com",
215
+ });
216
+
217
+ const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
218
+ // Response shows: http=http/3, sni=encrypted
109
219
  ```
110
220
 
111
221
  ## Cookie Management
package/lib/index.d.ts CHANGED
@@ -6,6 +6,22 @@ export class HTTPCloakError extends Error {
6
6
  name: "HTTPCloakError";
7
7
  }
8
8
 
9
+ export class Cookie {
10
+ /** Cookie name */
11
+ name: string;
12
+ /** Cookie value */
13
+ value: string;
14
+ }
15
+
16
+ export class RedirectInfo {
17
+ /** HTTP status code */
18
+ statusCode: number;
19
+ /** Request URL */
20
+ url: string;
21
+ /** Response headers */
22
+ headers: Record<string, string>;
23
+ }
24
+
9
25
  export class Response {
10
26
  /** HTTP status code */
11
27
  statusCode: number;
@@ -13,21 +29,40 @@ export class Response {
13
29
  headers: Record<string, string>;
14
30
  /** Raw response body as Buffer */
15
31
  body: Buffer;
32
+ /** Response body as Buffer (alias for body) */
33
+ content: Buffer;
16
34
  /** Response body as string */
17
35
  text: string;
18
36
  /** Final URL after redirects */
19
37
  finalUrl: string;
38
+ /** Final URL after redirects (alias for finalUrl) */
39
+ url: string;
20
40
  /** Protocol used (http/1.1, h2, h3) */
21
41
  protocol: string;
42
+ /** Elapsed time in milliseconds */
43
+ elapsed: number;
44
+ /** Cookies set by this response */
45
+ cookies: Cookie[];
46
+ /** Redirect history */
47
+ history: RedirectInfo[];
48
+ /** True if status code < 400 */
49
+ ok: boolean;
50
+ /** HTTP status reason phrase (e.g., 'OK', 'Not Found') */
51
+ reason: string;
52
+ /** Response encoding from Content-Type header */
53
+ encoding: string | null;
22
54
 
23
55
  /** Parse response body as JSON */
24
56
  json<T = any>(): T;
57
+
58
+ /** Raise error if status >= 400 */
59
+ raiseForStatus(): void;
25
60
  }
26
61
 
27
62
  export interface SessionOptions {
28
63
  /** Browser preset to use (default: "chrome-143") */
29
64
  preset?: string;
30
- /** Proxy URL (e.g., "http://user:pass@host:port") */
65
+ /** Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port") */
31
66
  proxy?: string;
32
67
  /** Request timeout in seconds (default: 30) */
33
68
  timeout?: number;
@@ -43,17 +78,33 @@ export interface SessionOptions {
43
78
  retry?: number;
44
79
  /** Status codes to retry on (default: [429, 500, 502, 503, 504]) */
45
80
  retryOnStatus?: number[];
81
+ /** Prefer IPv4 addresses over IPv6 (default: false) */
82
+ preferIpv4?: boolean;
83
+ /** Default basic auth [username, password] */
84
+ auth?: [string, string];
85
+ /** Domain fronting map {requestHost: connectHost} - DNS resolves connectHost but SNI/Host uses requestHost */
86
+ connectTo?: Record<string, string>;
87
+ /** Domain to fetch ECH config from (e.g., "cloudflare-ech.com" for any Cloudflare domain) */
88
+ echConfigDomain?: string;
46
89
  }
47
90
 
48
91
  export interface RequestOptions {
49
- /** HTTP method */
50
- method: string;
51
- /** Request URL */
52
- url: string;
53
92
  /** Optional custom headers */
54
93
  headers?: Record<string, string>;
55
- /** Optional request body */
94
+ /** Optional request body (for POST, PUT, PATCH) */
56
95
  body?: string | Buffer | Record<string, any>;
96
+ /** JSON body (will be serialized) */
97
+ json?: Record<string, any>;
98
+ /** Form data (will be URL encoded) */
99
+ data?: Record<string, any>;
100
+ /** Files to upload as multipart/form-data */
101
+ files?: Record<string, Buffer | { filename: string; content: Buffer; contentType?: string }>;
102
+ /** Query parameters */
103
+ params?: Record<string, string | number | boolean>;
104
+ /** Cookies to send with this request */
105
+ cookies?: Record<string, string>;
106
+ /** Basic auth [username, password] */
107
+ auth?: [string, string];
57
108
  /** Optional request timeout in seconds */
58
109
  timeout?: number;
59
110
  }
@@ -61,67 +112,66 @@ export interface RequestOptions {
61
112
  export class Session {
62
113
  constructor(options?: SessionOptions);
63
114
 
115
+ /** Default headers for all requests */
116
+ headers: Record<string, string>;
117
+
118
+ /** Default auth for all requests [username, password] */
119
+ auth: [string, string] | null;
120
+
64
121
  /** Close the session and release resources */
65
122
  close(): void;
66
123
 
67
124
  // Synchronous methods
68
125
  /** Perform a synchronous GET request */
69
- getSync(url: string, headers?: Record<string, string>): Response;
126
+ getSync(url: string, options?: RequestOptions): Response;
70
127
 
71
128
  /** Perform a synchronous POST request */
72
- postSync(
73
- url: string,
74
- body?: string | Buffer | Record<string, any>,
75
- headers?: Record<string, string>
76
- ): Response;
129
+ postSync(url: string, options?: RequestOptions): Response;
77
130
 
78
131
  /** Perform a synchronous custom HTTP request */
79
- requestSync(options: RequestOptions): Response;
132
+ requestSync(method: string, url: string, options?: RequestOptions): Response;
80
133
 
81
134
  // Promise-based methods
82
135
  /** Perform an async GET request */
83
- get(url: string, headers?: Record<string, string>): Promise<Response>;
136
+ get(url: string, options?: RequestOptions): Promise<Response>;
84
137
 
85
138
  /** Perform an async POST request */
86
- post(
87
- url: string,
88
- body?: string | Buffer | Record<string, any>,
89
- headers?: Record<string, string>
90
- ): Promise<Response>;
139
+ post(url: string, options?: RequestOptions): Promise<Response>;
91
140
 
92
141
  /** Perform an async custom HTTP request */
93
- request(options: RequestOptions): Promise<Response>;
142
+ request(method: string, url: string, options?: RequestOptions): Promise<Response>;
94
143
 
95
144
  /** Perform an async PUT request */
96
- put(
97
- url: string,
98
- body?: string | Buffer | Record<string, any>,
99
- headers?: Record<string, string>
100
- ): Promise<Response>;
145
+ put(url: string, options?: RequestOptions): Promise<Response>;
101
146
 
102
147
  /** Perform an async DELETE request */
103
- delete(url: string, headers?: Record<string, string>): Promise<Response>;
148
+ delete(url: string, options?: RequestOptions): Promise<Response>;
104
149
 
105
150
  /** Perform an async PATCH request */
106
- patch(
107
- url: string,
108
- body?: string | Buffer | Record<string, any>,
109
- headers?: Record<string, string>
110
- ): Promise<Response>;
151
+ patch(url: string, options?: RequestOptions): Promise<Response>;
111
152
 
112
153
  /** Perform an async HEAD request */
113
- head(url: string, headers?: Record<string, string>): Promise<Response>;
154
+ head(url: string, options?: RequestOptions): Promise<Response>;
114
155
 
115
156
  /** Perform an async OPTIONS request */
116
- options(url: string, headers?: Record<string, string>): Promise<Response>;
157
+ options(url: string, options?: RequestOptions): Promise<Response>;
117
158
 
118
159
  // Cookie management
119
160
  /** Get all cookies from the session */
120
161
  getCookies(): Record<string, string>;
121
162
 
163
+ /** Get a specific cookie by name */
164
+ getCookie(name: string): string | null;
165
+
122
166
  /** Set a cookie in the session */
123
167
  setCookie(name: string, value: string): void;
124
168
 
169
+ /** Delete a specific cookie by name */
170
+ deleteCookie(name: string): void;
171
+
172
+ /** Clear all cookies from the session */
173
+ clearCookies(): void;
174
+
125
175
  /** Get cookies as a property */
126
176
  readonly cookies: Record<string, string>;
127
177
  }
@@ -162,7 +212,8 @@ export function patch(url: string, options?: RequestOptions): Promise<Response>;
162
212
  export function head(url: string, options?: RequestOptions): Promise<Response>;
163
213
 
164
214
  /** Perform an OPTIONS request */
165
- export function options(url: string, options?: RequestOptions): Promise<Response>;
215
+ declare function opts(url: string, options?: RequestOptions): Promise<Response>;
216
+ export { opts as options };
166
217
 
167
218
  /** Perform a custom HTTP request */
168
219
  export function request(method: string, url: string, options?: RequestOptions): Promise<Response>;
package/lib/index.js CHANGED
@@ -69,17 +69,105 @@ const Preset = {
69
69
  },
70
70
  };
71
71
 
72
+ /**
73
+ * HTTP status reason phrases
74
+ */
75
+ const HTTP_STATUS_PHRASES = {
76
+ 100: "Continue", 101: "Switching Protocols", 102: "Processing",
77
+ 200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information",
78
+ 204: "No Content", 205: "Reset Content", 206: "Partial Content", 207: "Multi-Status",
79
+ 300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other",
80
+ 304: "Not Modified", 305: "Use Proxy", 307: "Temporary Redirect", 308: "Permanent Redirect",
81
+ 400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden",
82
+ 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable",
83
+ 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict",
84
+ 410: "Gone", 411: "Length Required", 412: "Precondition Failed",
85
+ 413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type",
86
+ 416: "Range Not Satisfiable", 417: "Expectation Failed", 418: "I'm a teapot",
87
+ 421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked",
88
+ 424: "Failed Dependency", 425: "Too Early", 426: "Upgrade Required",
89
+ 428: "Precondition Required", 429: "Too Many Requests",
90
+ 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons",
91
+ 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway",
92
+ 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported",
93
+ 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected",
94
+ 510: "Not Extended", 511: "Network Authentication Required",
95
+ };
96
+
97
+ /**
98
+ * Cookie object from Set-Cookie header
99
+ */
100
+ class Cookie {
101
+ /**
102
+ * @param {string} name - Cookie name
103
+ * @param {string} value - Cookie value
104
+ */
105
+ constructor(name, value) {
106
+ this.name = name;
107
+ this.value = value;
108
+ }
109
+
110
+ toString() {
111
+ return `Cookie(name=${this.name}, value=${this.value})`;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Redirect info from history
117
+ */
118
+ class RedirectInfo {
119
+ /**
120
+ * @param {number} statusCode - HTTP status code
121
+ * @param {string} url - Request URL
122
+ * @param {Object} headers - Response headers
123
+ */
124
+ constructor(statusCode, url, headers) {
125
+ this.statusCode = statusCode;
126
+ this.url = url;
127
+ this.headers = headers || {};
128
+ }
129
+
130
+ toString() {
131
+ return `RedirectInfo(statusCode=${this.statusCode}, url=${this.url})`;
132
+ }
133
+ }
134
+
72
135
  /**
73
136
  * Response object returned from HTTP requests
74
137
  */
75
138
  class Response {
76
- constructor(data) {
139
+ /**
140
+ * @param {Object} data - Response data from native library
141
+ * @param {number} [elapsed=0] - Elapsed time in milliseconds
142
+ */
143
+ constructor(data, elapsed = 0) {
77
144
  this.statusCode = data.status_code || 0;
78
145
  this.headers = data.headers || {};
79
146
  this._body = Buffer.from(data.body || "", "utf8");
80
147
  this._text = data.body || "";
81
148
  this.finalUrl = data.final_url || "";
82
149
  this.protocol = data.protocol || "";
150
+ this.elapsed = elapsed; // milliseconds
151
+
152
+ // Parse cookies from response
153
+ this._cookies = (data.cookies || []).map(c => new Cookie(c.name || "", c.value || ""));
154
+
155
+ // Parse redirect history
156
+ this._history = (data.history || []).map(h => new RedirectInfo(
157
+ h.status_code || 0,
158
+ h.url || "",
159
+ h.headers || {}
160
+ ));
161
+ }
162
+
163
+ /** Cookies set by this response */
164
+ get cookies() {
165
+ return this._cookies;
166
+ }
167
+
168
+ /** Redirect history (list of RedirectInfo objects) */
169
+ get history() {
170
+ return this._history;
83
171
  }
84
172
 
85
173
  /** Response body as string */
@@ -107,6 +195,29 @@ class Response {
107
195
  return this.statusCode < 400;
108
196
  }
109
197
 
198
+ /** HTTP status reason phrase (e.g., 'OK', 'Not Found') */
199
+ get reason() {
200
+ return HTTP_STATUS_PHRASES[this.statusCode] || "Unknown";
201
+ }
202
+
203
+ /**
204
+ * Response encoding from Content-Type header.
205
+ * Returns null if not specified.
206
+ */
207
+ get encoding() {
208
+ let contentType = this.headers["content-type"] || this.headers["Content-Type"] || "";
209
+ if (contentType.includes("charset=")) {
210
+ const parts = contentType.split(";");
211
+ for (const part of parts) {
212
+ const trimmed = part.trim();
213
+ if (trimmed.toLowerCase().startsWith("charset=")) {
214
+ return trimmed.split("=")[1].trim().replace(/['"]/g, "");
215
+ }
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+
110
221
  /**
111
222
  * Parse response body as JSON
112
223
  */
@@ -119,7 +230,7 @@ class Response {
119
230
  */
120
231
  raiseForStatus() {
121
232
  if (!this.ok) {
122
- throw new HTTPCloakError(`HTTP ${this.statusCode}`);
233
+ throw new HTTPCloakError(`HTTP ${this.statusCode}: ${this.reason}`);
123
234
  }
124
235
  }
125
236
  }
@@ -215,32 +326,158 @@ function getLibPath() {
215
326
  );
216
327
  }
217
328
 
329
+ // Define callback proto globally for koffi (must be before getLib)
330
+ const AsyncCallbackProto = koffi.proto("void AsyncCallback(int64 callbackId, str responseJson, str error)");
331
+
218
332
  // Load the native library
219
333
  let lib = null;
334
+ let nativeLibHandle = null;
220
335
 
221
336
  function getLib() {
222
337
  if (lib === null) {
223
338
  const libPath = getLibPath();
224
- const nativeLib = koffi.load(libPath);
339
+ nativeLibHandle = koffi.load(libPath);
225
340
 
226
341
  // Use str for string returns - koffi handles the string copy automatically
227
342
  // Note: The C strings allocated by Go are not freed, but Go's GC handles them
228
343
  lib = {
229
- httpcloak_session_new: nativeLib.func("httpcloak_session_new", "int64", ["str"]),
230
- httpcloak_session_free: nativeLib.func("httpcloak_session_free", "void", ["int64"]),
231
- httpcloak_get: nativeLib.func("httpcloak_get", "str", ["int64", "str", "str"]),
232
- httpcloak_post: nativeLib.func("httpcloak_post", "str", ["int64", "str", "str", "str"]),
233
- httpcloak_request: nativeLib.func("httpcloak_request", "str", ["int64", "str"]),
234
- httpcloak_get_cookies: nativeLib.func("httpcloak_get_cookies", "str", ["int64"]),
235
- httpcloak_set_cookie: nativeLib.func("httpcloak_set_cookie", "void", ["int64", "str", "str"]),
236
- httpcloak_free_string: nativeLib.func("httpcloak_free_string", "void", ["void*"]),
237
- httpcloak_version: nativeLib.func("httpcloak_version", "str", []),
238
- httpcloak_available_presets: nativeLib.func("httpcloak_available_presets", "str", []),
344
+ httpcloak_session_new: nativeLibHandle.func("httpcloak_session_new", "int64", ["str"]),
345
+ httpcloak_session_free: nativeLibHandle.func("httpcloak_session_free", "void", ["int64"]),
346
+ httpcloak_get: nativeLibHandle.func("httpcloak_get", "str", ["int64", "str", "str"]),
347
+ httpcloak_post: nativeLibHandle.func("httpcloak_post", "str", ["int64", "str", "str", "str"]),
348
+ httpcloak_request: nativeLibHandle.func("httpcloak_request", "str", ["int64", "str"]),
349
+ httpcloak_get_cookies: nativeLibHandle.func("httpcloak_get_cookies", "str", ["int64"]),
350
+ httpcloak_set_cookie: nativeLibHandle.func("httpcloak_set_cookie", "void", ["int64", "str", "str"]),
351
+ httpcloak_free_string: nativeLibHandle.func("httpcloak_free_string", "void", ["void*"]),
352
+ httpcloak_version: nativeLibHandle.func("httpcloak_version", "str", []),
353
+ httpcloak_available_presets: nativeLibHandle.func("httpcloak_available_presets", "str", []),
354
+ // Async functions
355
+ httpcloak_register_callback: nativeLibHandle.func("httpcloak_register_callback", "int64", [koffi.pointer(AsyncCallbackProto)]),
356
+ httpcloak_unregister_callback: nativeLibHandle.func("httpcloak_unregister_callback", "void", ["int64"]),
357
+ httpcloak_get_async: nativeLibHandle.func("httpcloak_get_async", "void", ["int64", "str", "str", "int64"]),
358
+ httpcloak_post_async: nativeLibHandle.func("httpcloak_post_async", "void", ["int64", "str", "str", "str", "int64"]),
359
+ httpcloak_request_async: nativeLibHandle.func("httpcloak_request_async", "void", ["int64", "str", "int64"]),
239
360
  };
240
361
  }
241
362
  return lib;
242
363
  }
243
364
 
365
+ /**
366
+ * Async callback manager for native Go goroutine-based async
367
+ *
368
+ * Each async request registers a callback with Go and receives a unique ID.
369
+ * When Go completes the request, it invokes the callback with that ID.
370
+ */
371
+ class AsyncCallbackManager {
372
+ constructor() {
373
+ // callbackId -> { resolve, reject, startTime }
374
+ this._pendingRequests = new Map();
375
+ this._callbackPtr = null;
376
+ this._refTimer = null; // Timer to keep event loop alive
377
+ }
378
+
379
+ /**
380
+ * Ref the event loop to prevent Node.js from exiting while requests are pending
381
+ */
382
+ _ref() {
383
+ if (this._refTimer === null) {
384
+ // Create a timer that keeps the event loop alive
385
+ this._refTimer = setInterval(() => {}, 2147483647); // Max interval
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Unref the event loop when no more pending requests
391
+ */
392
+ _unref() {
393
+ if (this._pendingRequests.size === 0 && this._refTimer !== null) {
394
+ clearInterval(this._refTimer);
395
+ this._refTimer = null;
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Ensure the callback is set up with koffi
401
+ */
402
+ _ensureCallback() {
403
+ if (this._callbackPtr !== null) {
404
+ return;
405
+ }
406
+
407
+ // Create callback function that will be invoked by Go
408
+ // koffi.register expects koffi.pointer(proto) as the type
409
+ this._callbackPtr = koffi.register((callbackId, responseJson, error) => {
410
+ const pending = this._pendingRequests.get(Number(callbackId));
411
+ if (!pending) {
412
+ return;
413
+ }
414
+ this._pendingRequests.delete(Number(callbackId));
415
+ this._unref(); // Check if we can release the event loop
416
+
417
+ const { resolve, reject, startTime } = pending;
418
+ const elapsed = Date.now() - startTime;
419
+
420
+ if (error && error !== "") {
421
+ let errMsg = error;
422
+ try {
423
+ const errData = JSON.parse(error);
424
+ errMsg = errData.error || error;
425
+ } catch (e) {
426
+ // Use raw error string
427
+ }
428
+ reject(new HTTPCloakError(errMsg));
429
+ } else if (responseJson) {
430
+ try {
431
+ const data = JSON.parse(responseJson);
432
+ if (data.error) {
433
+ reject(new HTTPCloakError(data.error));
434
+ } else {
435
+ resolve(new Response(data, elapsed));
436
+ }
437
+ } catch (e) {
438
+ reject(new HTTPCloakError(`Failed to parse response: ${e.message}`));
439
+ }
440
+ } else {
441
+ reject(new HTTPCloakError("No response received"));
442
+ }
443
+ }, koffi.pointer(AsyncCallbackProto));
444
+ }
445
+
446
+ /**
447
+ * Register a new async request
448
+ * @returns {{ callbackId: number, promise: Promise<Response> }}
449
+ */
450
+ registerRequest(nativeLib) {
451
+ this._ensureCallback();
452
+
453
+ // Register callback with Go (each request gets unique ID)
454
+ const callbackId = nativeLib.httpcloak_register_callback(this._callbackPtr);
455
+
456
+ // Create promise for this request with start time
457
+ let resolve, reject;
458
+ const promise = new Promise((res, rej) => {
459
+ resolve = res;
460
+ reject = rej;
461
+ });
462
+ const startTime = Date.now();
463
+
464
+ this._pendingRequests.set(Number(callbackId), { resolve, reject, startTime });
465
+ this._ref(); // Keep event loop alive
466
+
467
+ return { callbackId, promise };
468
+ }
469
+ }
470
+
471
+ // Global async callback manager
472
+ let asyncManager = null;
473
+
474
+ function getAsyncManager() {
475
+ if (asyncManager === null) {
476
+ asyncManager = new AsyncCallbackManager();
477
+ }
478
+ return asyncManager;
479
+ }
480
+
244
481
  /**
245
482
  * Convert result to string (handles both direct strings and null)
246
483
  * With "str" return type, koffi automatically handles the conversion
@@ -254,8 +491,11 @@ function resultToString(result) {
254
491
 
255
492
  /**
256
493
  * Parse response from the native library
494
+ * @param {string} resultPtr - Result pointer from native function
495
+ * @param {number} [elapsed=0] - Elapsed time in milliseconds
496
+ * @returns {Response}
257
497
  */
258
- function parseResponse(resultPtr) {
498
+ function parseResponse(resultPtr, elapsed = 0) {
259
499
  const result = resultToString(resultPtr);
260
500
  if (!result) {
261
501
  throw new HTTPCloakError("No response received");
@@ -267,7 +507,7 @@ function parseResponse(resultPtr) {
267
507
  throw new HTTPCloakError(data.error);
268
508
  }
269
509
 
270
- return new Response(data);
510
+ return new Response(data, elapsed);
271
511
  }
272
512
 
273
513
  /**
@@ -434,7 +674,7 @@ class Session {
434
674
  * Create a new session
435
675
  * @param {Object} options - Session options
436
676
  * @param {string} [options.preset="chrome-143"] - Browser preset to use
437
- * @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port")
677
+ * @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port")
438
678
  * @param {number} [options.timeout=30] - Request timeout in seconds
439
679
  * @param {string} [options.httpVersion="auto"] - HTTP version: "auto", "h1", "h2", "h3"
440
680
  * @param {boolean} [options.verify=true] - SSL certificate verification
@@ -442,6 +682,9 @@ class Session {
442
682
  * @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
443
683
  * @param {number} [options.retry=3] - Number of retries on failure (set to 0 to disable)
444
684
  * @param {number[]} [options.retryOnStatus] - Status codes to retry on
685
+ * @param {Array} [options.auth] - Default auth [username, password] for all requests
686
+ * @param {Object} [options.connectTo] - Domain fronting map {requestHost: connectHost}
687
+ * @param {string} [options.echConfigDomain] - Domain to fetch ECH config from (e.g., "cloudflare-ech.com")
445
688
  */
446
689
  constructor(options = {}) {
447
690
  const {
@@ -455,10 +698,14 @@ class Session {
455
698
  retry = 3,
456
699
  retryOnStatus = null,
457
700
  preferIpv4 = false,
701
+ auth = null,
702
+ connectTo = null,
703
+ echConfigDomain = null,
458
704
  } = options;
459
705
 
460
706
  this._lib = getLib();
461
707
  this.headers = {}; // Default headers
708
+ this.auth = auth; // Default auth for all requests
462
709
 
463
710
  const config = {
464
711
  preset,
@@ -484,6 +731,12 @@ class Session {
484
731
  if (preferIpv4) {
485
732
  config.prefer_ipv4 = true;
486
733
  }
734
+ if (connectTo) {
735
+ config.connect_to = connectTo;
736
+ }
737
+ if (echConfigDomain) {
738
+ config.ech_config_domain = echConfigDomain;
739
+ }
487
740
 
488
741
  this._handle = this._lib.httpcloak_session_new(JSON.stringify(config));
489
742
 
@@ -512,6 +765,31 @@ class Session {
512
765
  return { ...this.headers, ...headers };
513
766
  }
514
767
 
768
+ /**
769
+ * Apply cookies to headers
770
+ * @param {Object} headers - Existing headers
771
+ * @param {Object} cookies - Cookies to apply as key-value pairs
772
+ * @returns {Object} Headers with cookies applied
773
+ */
774
+ _applyCookies(headers, cookies) {
775
+ if (!cookies || Object.keys(cookies).length === 0) {
776
+ return headers;
777
+ }
778
+
779
+ const cookieStr = Object.entries(cookies)
780
+ .map(([k, v]) => `${k}=${v}`)
781
+ .join("; ");
782
+
783
+ headers = headers ? { ...headers } : {};
784
+ const existing = headers["Cookie"] || "";
785
+ if (existing) {
786
+ headers["Cookie"] = `${existing}; ${cookieStr}`;
787
+ } else {
788
+ headers["Cookie"] = cookieStr;
789
+ }
790
+ return headers;
791
+ }
792
+
515
793
  // ===========================================================================
516
794
  // Synchronous Methods
517
795
  // ===========================================================================
@@ -522,19 +800,25 @@ class Session {
522
800
  * @param {Object} [options] - Request options
523
801
  * @param {Object} [options.headers] - Custom headers
524
802
  * @param {Object} [options.params] - Query parameters
803
+ * @param {Object} [options.cookies] - Cookies to send with this request
525
804
  * @param {Array} [options.auth] - Basic auth [username, password]
526
805
  * @returns {Response} Response object
527
806
  */
528
807
  getSync(url, options = {}) {
529
- const { headers = null, params = null, auth = null } = options;
808
+ const { headers = null, params = null, cookies = null, auth = null } = options;
530
809
 
531
810
  url = addParamsToUrl(url, params);
532
811
  let mergedHeaders = this._mergeHeaders(headers);
533
- mergedHeaders = applyAuth(mergedHeaders, auth);
812
+ // Use request auth if provided, otherwise fall back to session auth
813
+ const effectiveAuth = auth !== null ? auth : this.auth;
814
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
815
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
534
816
 
535
817
  const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
818
+ const startTime = Date.now();
536
819
  const result = this._lib.httpcloak_get(this._handle, url, headersJson);
537
- return parseResponse(result);
820
+ const elapsed = Date.now() - startTime;
821
+ return parseResponse(result, elapsed);
538
822
  }
539
823
 
540
824
  /**
@@ -550,11 +834,12 @@ class Session {
550
834
  * - { filename, content, contentType? }: file with metadata
551
835
  * @param {Object} [options.headers] - Custom headers
552
836
  * @param {Object} [options.params] - Query parameters
837
+ * @param {Object} [options.cookies] - Cookies to send with this request
553
838
  * @param {Array} [options.auth] - Basic auth [username, password]
554
839
  * @returns {Response} Response object
555
840
  */
556
841
  postSync(url, options = {}) {
557
- let { body = null, json = null, data = null, files = null, headers = null, params = null, auth = null } = options;
842
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
558
843
 
559
844
  url = addParamsToUrl(url, params);
560
845
  let mergedHeaders = this._mergeHeaders(headers);
@@ -588,11 +873,16 @@ class Session {
588
873
  body = body.toString("utf8");
589
874
  }
590
875
 
591
- mergedHeaders = applyAuth(mergedHeaders, auth);
876
+ // Use request auth if provided, otherwise fall back to session auth
877
+ const effectiveAuth = auth !== null ? auth : this.auth;
878
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
879
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
592
880
 
593
881
  const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
882
+ const startTime = Date.now();
594
883
  const result = this._lib.httpcloak_post(this._handle, url, body, headersJson);
595
- return parseResponse(result);
884
+ const elapsed = Date.now() - startTime;
885
+ return parseResponse(result, elapsed);
596
886
  }
597
887
 
598
888
  /**
@@ -600,11 +890,12 @@ class Session {
600
890
  * @param {string} method - HTTP method
601
891
  * @param {string} url - Request URL
602
892
  * @param {Object} [options] - Request options
893
+ * @param {Object} [options.cookies] - Cookies to send with this request
603
894
  * @param {Object} [options.files] - Files to upload as multipart/form-data
604
895
  * @returns {Response} Response object
605
896
  */
606
897
  requestSync(method, url, options = {}) {
607
- let { body = null, json = null, data = null, files = null, headers = null, params = null, auth = null, timeout = null } = options;
898
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null } = options;
608
899
 
609
900
  url = addParamsToUrl(url, params);
610
901
  let mergedHeaders = this._mergeHeaders(headers);
@@ -638,7 +929,10 @@ class Session {
638
929
  body = body.toString("utf8");
639
930
  }
640
931
 
641
- mergedHeaders = applyAuth(mergedHeaders, auth);
932
+ // Use request auth if provided, otherwise fall back to session auth
933
+ const effectiveAuth = auth !== null ? auth : this.auth;
934
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
935
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
642
936
 
643
937
  const requestConfig = {
644
938
  method: method.toUpperCase(),
@@ -648,70 +942,171 @@ class Session {
648
942
  if (body) requestConfig.body = body;
649
943
  if (timeout) requestConfig.timeout = timeout;
650
944
 
945
+ const startTime = Date.now();
651
946
  const result = this._lib.httpcloak_request(
652
947
  this._handle,
653
948
  JSON.stringify(requestConfig)
654
949
  );
655
- return parseResponse(result);
950
+ const elapsed = Date.now() - startTime;
951
+ return parseResponse(result, elapsed);
656
952
  }
657
953
 
658
954
  // ===========================================================================
659
- // Promise-based Methods
955
+ // Promise-based Methods (Native async using Go goroutines)
660
956
  // ===========================================================================
661
957
 
662
958
  /**
663
- * Perform an async GET request
959
+ * Perform an async GET request using native Go goroutines
664
960
  * @param {string} url - Request URL
665
961
  * @param {Object} [options] - Request options
962
+ * @param {Object} [options.cookies] - Cookies to send with this request
666
963
  * @returns {Promise<Response>} Response object
667
964
  */
668
965
  get(url, options = {}) {
669
- return new Promise((resolve, reject) => {
670
- setImmediate(() => {
671
- try {
672
- resolve(this.getSync(url, options));
673
- } catch (err) {
674
- reject(err);
675
- }
676
- });
677
- });
966
+ const { headers = null, params = null, cookies = null, auth = null } = options;
967
+
968
+ url = addParamsToUrl(url, params);
969
+ let mergedHeaders = this._mergeHeaders(headers);
970
+ // Use request auth if provided, otherwise fall back to session auth
971
+ const effectiveAuth = auth !== null ? auth : this.auth;
972
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
973
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
974
+
975
+ const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
976
+
977
+ // Register async request with callback manager
978
+ const manager = getAsyncManager();
979
+ const { callbackId, promise } = manager.registerRequest(this._lib);
980
+
981
+ // Start async request
982
+ this._lib.httpcloak_get_async(this._handle, url, headersJson, callbackId);
983
+
984
+ return promise;
678
985
  }
679
986
 
680
987
  /**
681
- * Perform an async POST request
988
+ * Perform an async POST request using native Go goroutines
682
989
  * @param {string} url - Request URL
683
990
  * @param {Object} [options] - Request options
991
+ * @param {Object} [options.cookies] - Cookies to send with this request
684
992
  * @returns {Promise<Response>} Response object
685
993
  */
686
994
  post(url, options = {}) {
687
- return new Promise((resolve, reject) => {
688
- setImmediate(() => {
689
- try {
690
- resolve(this.postSync(url, options));
691
- } catch (err) {
692
- reject(err);
693
- }
694
- });
695
- });
995
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
996
+
997
+ url = addParamsToUrl(url, params);
998
+ let mergedHeaders = this._mergeHeaders(headers);
999
+
1000
+ // Handle multipart file upload
1001
+ if (files !== null) {
1002
+ const formData = (data !== null && typeof data === "object") ? data : null;
1003
+ const multipart = encodeMultipart(formData, files);
1004
+ body = multipart.body.toString("latin1");
1005
+ mergedHeaders = mergedHeaders || {};
1006
+ mergedHeaders["Content-Type"] = multipart.contentType;
1007
+ }
1008
+ // Handle JSON body
1009
+ else if (json !== null) {
1010
+ body = JSON.stringify(json);
1011
+ mergedHeaders = mergedHeaders || {};
1012
+ if (!mergedHeaders["Content-Type"]) {
1013
+ mergedHeaders["Content-Type"] = "application/json";
1014
+ }
1015
+ }
1016
+ // Handle form data
1017
+ else if (data !== null && typeof data === "object") {
1018
+ body = new URLSearchParams(data).toString();
1019
+ mergedHeaders = mergedHeaders || {};
1020
+ if (!mergedHeaders["Content-Type"]) {
1021
+ mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1022
+ }
1023
+ }
1024
+ // Handle Buffer body
1025
+ else if (Buffer.isBuffer(body)) {
1026
+ body = body.toString("utf8");
1027
+ }
1028
+
1029
+ // Use request auth if provided, otherwise fall back to session auth
1030
+ const effectiveAuth = auth !== null ? auth : this.auth;
1031
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
1032
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
1033
+
1034
+ const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
1035
+
1036
+ // Register async request with callback manager
1037
+ const manager = getAsyncManager();
1038
+ const { callbackId, promise } = manager.registerRequest(this._lib);
1039
+
1040
+ // Start async request
1041
+ this._lib.httpcloak_post_async(this._handle, url, body, headersJson, callbackId);
1042
+
1043
+ return promise;
696
1044
  }
697
1045
 
698
1046
  /**
699
- * Perform an async custom HTTP request
1047
+ * Perform an async custom HTTP request using native Go goroutines
700
1048
  * @param {string} method - HTTP method
701
1049
  * @param {string} url - Request URL
702
1050
  * @param {Object} [options] - Request options
1051
+ * @param {Object} [options.cookies] - Cookies to send with this request
703
1052
  * @returns {Promise<Response>} Response object
704
1053
  */
705
1054
  request(method, url, options = {}) {
706
- return new Promise((resolve, reject) => {
707
- setImmediate(() => {
708
- try {
709
- resolve(this.requestSync(method, url, options));
710
- } catch (err) {
711
- reject(err);
712
- }
713
- });
714
- });
1055
+ let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null } = options;
1056
+
1057
+ url = addParamsToUrl(url, params);
1058
+ let mergedHeaders = this._mergeHeaders(headers);
1059
+
1060
+ // Handle multipart file upload
1061
+ if (files !== null) {
1062
+ const formData = (data !== null && typeof data === "object") ? data : null;
1063
+ const multipart = encodeMultipart(formData, files);
1064
+ body = multipart.body.toString("latin1");
1065
+ mergedHeaders = mergedHeaders || {};
1066
+ mergedHeaders["Content-Type"] = multipart.contentType;
1067
+ }
1068
+ // Handle JSON body
1069
+ else if (json !== null) {
1070
+ body = JSON.stringify(json);
1071
+ mergedHeaders = mergedHeaders || {};
1072
+ if (!mergedHeaders["Content-Type"]) {
1073
+ mergedHeaders["Content-Type"] = "application/json";
1074
+ }
1075
+ }
1076
+ // Handle form data
1077
+ else if (data !== null && typeof data === "object") {
1078
+ body = new URLSearchParams(data).toString();
1079
+ mergedHeaders = mergedHeaders || {};
1080
+ if (!mergedHeaders["Content-Type"]) {
1081
+ mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1082
+ }
1083
+ }
1084
+ // Handle Buffer body
1085
+ else if (Buffer.isBuffer(body)) {
1086
+ body = body.toString("utf8");
1087
+ }
1088
+
1089
+ // Use request auth if provided, otherwise fall back to session auth
1090
+ const effectiveAuth = auth !== null ? auth : this.auth;
1091
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
1092
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
1093
+
1094
+ const requestConfig = {
1095
+ method: method.toUpperCase(),
1096
+ url,
1097
+ };
1098
+ if (mergedHeaders) requestConfig.headers = mergedHeaders;
1099
+ if (body) requestConfig.body = body;
1100
+ if (timeout) requestConfig.timeout = timeout;
1101
+
1102
+ // Register async request with callback manager
1103
+ const manager = getAsyncManager();
1104
+ const { callbackId, promise } = manager.registerRequest(this._lib);
1105
+
1106
+ // Start async request
1107
+ this._lib.httpcloak_request_async(this._handle, JSON.stringify(requestConfig), callbackId);
1108
+
1109
+ return promise;
715
1110
  }
716
1111
 
717
1112
  /**
@@ -766,6 +1161,16 @@ class Session {
766
1161
  return {};
767
1162
  }
768
1163
 
1164
+ /**
1165
+ * Get a specific cookie by name
1166
+ * @param {string} name - Cookie name
1167
+ * @returns {string|null} Cookie value or null if not found
1168
+ */
1169
+ getCookie(name) {
1170
+ const cookies = this.getCookies();
1171
+ return cookies[name] || null;
1172
+ }
1173
+
769
1174
  /**
770
1175
  * Set a cookie in the session
771
1176
  * @param {string} name - Cookie name
@@ -775,6 +1180,25 @@ class Session {
775
1180
  this._lib.httpcloak_set_cookie(this._handle, name, value);
776
1181
  }
777
1182
 
1183
+ /**
1184
+ * Delete a specific cookie by name
1185
+ * @param {string} name - Cookie name to delete
1186
+ */
1187
+ deleteCookie(name) {
1188
+ // Set cookie to empty value - effectively deletes it
1189
+ this._lib.httpcloak_set_cookie(this._handle, name, "");
1190
+ }
1191
+
1192
+ /**
1193
+ * Clear all cookies from the session
1194
+ */
1195
+ clearCookies() {
1196
+ const cookies = this.getCookies();
1197
+ for (const name of Object.keys(cookies)) {
1198
+ this.deleteCookie(name);
1199
+ }
1200
+ }
1201
+
778
1202
  /**
779
1203
  * Get cookies as a property
780
1204
  */
@@ -960,6 +1384,8 @@ module.exports = {
960
1384
  // Classes
961
1385
  Session,
962
1386
  Response,
1387
+ Cookie,
1388
+ RedirectInfo,
963
1389
  HTTPCloakError,
964
1390
  // Presets
965
1391
  Preset,
package/lib/index.mjs ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * HTTPCloak Node.js Client - ESM Module
3
+ *
4
+ * A fetch/axios-compatible HTTP client with browser fingerprint emulation.
5
+ * Provides TLS fingerprinting for HTTP requests.
6
+ */
7
+
8
+ import { createRequire } from "module";
9
+ const require = createRequire(import.meta.url);
10
+
11
+ // Import the CommonJS module
12
+ const cjs = require("./index.js");
13
+
14
+ // Re-export all named exports
15
+ export const Session = cjs.Session;
16
+ export const Response = cjs.Response;
17
+ export const HTTPCloakError = cjs.HTTPCloakError;
18
+ export const Preset = cjs.Preset;
19
+ export const configure = cjs.configure;
20
+ export const get = cjs.get;
21
+ export const post = cjs.post;
22
+ export const put = cjs.put;
23
+ export const patch = cjs.patch;
24
+ export const head = cjs.head;
25
+ export const options = cjs.options;
26
+ export const request = cjs.request;
27
+ export const version = cjs.version;
28
+ export const availablePresets = cjs.availablePresets;
29
+
30
+ // 'delete' is a reserved word in ESM, so we export it specially
31
+ const del = cjs.delete;
32
+ export { del as delete };
33
+
34
+ // Default export (the entire module)
35
+ export default cjs;
@@ -0,0 +1,6 @@
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-darwin-arm64.dylib");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/darwin-arm64",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "HTTPCloak native binary for darwin arm64",
5
5
  "os": [
6
6
  "darwin"
@@ -9,6 +9,13 @@
9
9
  "arm64"
10
10
  ],
11
11
  "main": "lib.js",
12
+ "module": "lib.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib.mjs",
16
+ "require": "./lib.js"
17
+ }
18
+ },
12
19
  "license": "MIT",
13
20
  "repository": {
14
21
  "type": "git",
@@ -0,0 +1,6 @@
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-darwin-amd64.dylib");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/darwin-x64",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "HTTPCloak native binary for darwin x64",
5
5
  "os": [
6
6
  "darwin"
@@ -9,6 +9,13 @@
9
9
  "x64"
10
10
  ],
11
11
  "main": "lib.js",
12
+ "module": "lib.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib.mjs",
16
+ "require": "./lib.js"
17
+ }
18
+ },
12
19
  "license": "MIT",
13
20
  "repository": {
14
21
  "type": "git",
@@ -0,0 +1,6 @@
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-linux-arm64.so");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/linux-arm64",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "HTTPCloak native binary for linux arm64",
5
5
  "os": [
6
6
  "linux"
@@ -9,6 +9,13 @@
9
9
  "arm64"
10
10
  ],
11
11
  "main": "lib.js",
12
+ "module": "lib.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib.mjs",
16
+ "require": "./lib.js"
17
+ }
18
+ },
12
19
  "license": "MIT",
13
20
  "repository": {
14
21
  "type": "git",
@@ -0,0 +1,6 @@
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-linux-amd64.so");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/linux-x64",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "HTTPCloak native binary for linux x64",
5
5
  "os": [
6
6
  "linux"
@@ -9,6 +9,13 @@
9
9
  "x64"
10
10
  ],
11
11
  "main": "lib.js",
12
+ "module": "lib.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib.mjs",
16
+ "require": "./lib.js"
17
+ }
18
+ },
12
19
  "license": "MIT",
13
20
  "repository": {
14
21
  "type": "git",
@@ -0,0 +1,6 @@
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/win32-arm64",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "HTTPCloak native binary for win32 arm64",
5
5
  "os": [
6
6
  "win32"
@@ -9,6 +9,13 @@
9
9
  "arm64"
10
10
  ],
11
11
  "main": "lib.js",
12
+ "module": "lib.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib.mjs",
16
+ "require": "./lib.js"
17
+ }
18
+ },
12
19
  "license": "MIT",
13
20
  "repository": {
14
21
  "type": "git",
@@ -0,0 +1,6 @@
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-amd64.dll");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@httpcloak/win32-x64",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "HTTPCloak native binary for win32 x64",
5
5
  "os": [
6
6
  "win32"
@@ -9,6 +9,13 @@
9
9
  "x64"
10
10
  ],
11
11
  "main": "lib.js",
12
+ "module": "lib.mjs",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./lib.mjs",
16
+ "require": "./lib.js"
17
+ }
18
+ },
12
19
  "license": "MIT",
13
20
  "repository": {
14
21
  "type": "git",
package/package.json CHANGED
@@ -1,11 +1,25 @@
1
1
  {
2
2
  "name": "httpcloak",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
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
+ "module": "lib/index.mjs",
6
7
  "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./lib/index.d.ts",
12
+ "default": "./lib/index.mjs"
13
+ },
14
+ "require": {
15
+ "types": "./lib/index.d.ts",
16
+ "default": "./lib/index.js"
17
+ }
18
+ }
19
+ },
7
20
  "scripts": {
8
21
  "test": "node test.js",
22
+ "test:esm": "node test.mjs",
9
23
  "setup-packages": "node scripts/setup-npm-packages.js"
10
24
  },
11
25
  "keywords": [
@@ -35,11 +49,11 @@
35
49
  "koffi": "^2.9.0"
36
50
  },
37
51
  "optionalDependencies": {
38
- "@httpcloak/linux-x64": "1.5.0",
39
- "@httpcloak/linux-arm64": "1.5.0",
40
- "@httpcloak/darwin-x64": "1.5.0",
41
- "@httpcloak/darwin-arm64": "1.5.0",
42
- "@httpcloak/win32-x64": "1.5.0",
43
- "@httpcloak/win32-arm64": "1.5.0"
52
+ "@httpcloak/linux-x64": "1.5.1",
53
+ "@httpcloak/linux-arm64": "1.5.1",
54
+ "@httpcloak/darwin-x64": "1.5.1",
55
+ "@httpcloak/darwin-arm64": "1.5.1",
56
+ "@httpcloak/win32-x64": "1.5.1",
57
+ "@httpcloak/win32-arm64": "1.5.1"
44
58
  }
45
59
  }