h2-fingerprint-client 1.0.0

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 ADDED
@@ -0,0 +1,152 @@
1
+ # h2-fingerprint-client
2
+
3
+ > HTTP/2 fingerprint-aware request library for Node.js — mimics real browser SETTINGS frames, pseudo-header order, and header casing for research and educational purposes.
4
+
5
+ ---
6
+
7
+ ## Background
8
+
9
+ Modern bot detection systems don't just inspect your IP or User-Agent. They fingerprint the structure of your HTTP/2 connection at the transport layer — analyzing:
10
+
11
+ - **SETTINGS frames** — every browser sends unique `HEADER_TABLE_SIZE`, `INITIAL_WINDOW_SIZE`, `MAX_HEADER_LIST_SIZE` values on session open
12
+ - **Pseudo-header order** — Chrome sends `:method :authority :scheme :path`, Firefox sends `:method :path :authority :scheme`
13
+ - **Header casing and order** — real browsers send headers in a consistent, deterministic sequence
14
+ - **WINDOW_UPDATE size** — the initial flow control window increment differs per browser
15
+
16
+ A standard Node.js `http2` or `axios` request is immediately identifiable because it sends none of these signals correctly.
17
+
18
+ This library lets you make HTTP/2 requests that structurally match a real browser's fingerprint.
19
+
20
+ ---
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install h2-fingerprint-client
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Usage
31
+
32
+ ### Basic GET request
33
+
34
+ ```js
35
+ const { get } = require("h2-fingerprint-client");
36
+
37
+ const res = await get("https://example.com", {
38
+ profile: "chrome120", // chrome120 | firefox121 | safari17
39
+ });
40
+
41
+ console.log(res.status); // 200
42
+ console.log(res.body); // HTML response
43
+ console.log(res.timings); // { connect: 120, total: 340 }
44
+ console.log(res.profile); // "Chrome 120 / Windows 11"
45
+ ```
46
+
47
+ ### POST request
48
+
49
+ ```js
50
+ const { post } = require("h2-fingerprint-client");
51
+
52
+ const res = await post("https://example.com/api/data", {
53
+ profile: "firefox121",
54
+ headers: { "content-type": "application/json" },
55
+ body: JSON.stringify({ key: "value" }),
56
+ });
57
+ ```
58
+
59
+ ### Custom headers (merged with profile)
60
+
61
+ ```js
62
+ const { request } = require("h2-fingerprint-client");
63
+
64
+ const res = await request("https://example.com", {
65
+ profile: "safari17",
66
+ method: "GET",
67
+ headers: {
68
+ "accept-language": "fr-FR,fr;q=0.9",
69
+ "cookie": "session=abc123",
70
+ },
71
+ timeout: 10000,
72
+ });
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Profiles
78
+
79
+ | Profile | Browser | OS | SETTINGS |
80
+ |---|---|---|---|
81
+ | `chrome120` | Chrome 120 | Windows 11 | `HEADER_TABLE_SIZE=65536, ENABLE_PUSH=0, INITIAL_WINDOW_SIZE=6291456, MAX_HEADER_LIST_SIZE=262144` |
82
+ | `firefox121` | Firefox 121 | Windows 11 | `HEADER_TABLE_SIZE=65536, INITIAL_WINDOW_SIZE=131072, MAX_FRAME_SIZE=16384, ENABLE_PUSH=0` |
83
+ | `safari17` | Safari 17 | macOS Sonoma | `HEADER_TABLE_SIZE=4096, ENABLE_PUSH=0, INITIAL_WINDOW_SIZE=4194304, MAX_FRAME_SIZE=16384` |
84
+
85
+ ---
86
+
87
+ ## Response Object
88
+
89
+ ```js
90
+ {
91
+ status: 200, // HTTP status code
92
+ headers: { ... }, // Response headers (pseudo-headers stripped)
93
+ body: "...", // Response body as string
94
+ profile: "Chrome 120 / Windows 11",
95
+ timings: {
96
+ connect: 112, // ms to establish HTTP/2 session
97
+ total: 348, // ms total
98
+ }
99
+ }
100
+ ```
101
+
102
+ ---
103
+
104
+ ## How It Works
105
+
106
+ ### 1. SETTINGS Frame
107
+ On session open, Node.js's `http2.connect()` accepts a `settings` object. This library passes the exact settings values that each real browser sends, rather than the Node.js defaults.
108
+
109
+ ### 2. Pseudo-Header Order
110
+ HTTP/2 uses pseudo-headers (`:method`, `:path`, `:authority`, `:scheme`) that must appear before regular headers. The **order** of these pseudo-headers differs between browsers and is a key fingerprinting signal. This library replicates the correct order per profile.
111
+
112
+ ### 3. Header Order
113
+ Regular headers are ordered to match the real browser's typical output — not alphabetically or randomly as most HTTP clients do.
114
+
115
+ ### 4. Header Values
116
+ Each profile ships with the correct `user-agent`, `accept`, `sec-ch-ua`, `sec-fetch-*` and other headers matching that browser version.
117
+
118
+ ---
119
+
120
+ ## Examples
121
+
122
+ ```bash
123
+ # Basic request
124
+ node examples/basic.js
125
+
126
+ # Compare all profiles side by side
127
+ node examples/compare-profiles.js
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Limitations
133
+
134
+ - HTTP/2 requires **HTTPS**. HTTP/1.1 sites are not supported.
135
+ - TLS fingerprinting (JA3/JA4) is a separate layer — this library does not spoof TLS ClientHello. For that, look into solutions using BoringSSL or `curl-impersonate`.
136
+ - WINDOW_UPDATE frame timing is not yet controllable.
137
+
138
+ ---
139
+
140
+ ## Research References
141
+
142
+ - [RFC 7540 — HTTP/2](https://www.rfc-editor.org/rfc/rfc7540)
143
+ - [RFC 7541 — HPACK Header Compression](https://www.rfc-editor.org/rfc/rfc7541)
144
+ - [HTTP/2 Fingerprinting — BrowserLeaks](https://browserleaks.com/http2)
145
+ - [TLS + HTTP/2 fingerprinting — tls.peet.ws](https://tls.peet.ws/)
146
+ - [Akamai HTTP/2 fingerprinting research](https://www.blackhat.com/docs/eu-17/materials/eu-17-Shuster-Passive-Fingerprinting-Of-HTTP2-Clients-wp.pdf)
147
+
148
+ ---
149
+
150
+ ## License
151
+
152
+ MIT — for research and educational use.
@@ -0,0 +1,16 @@
1
+ const { get } = require("../src");
2
+
3
+ (async () => {
4
+ try {
5
+ console.log("Testing Chrome 120 fingerprint...");
6
+ const res = await get("https://httpbin.org/headers", { profile: "chrome120" });
7
+ console.log("Status:", res.status);
8
+ console.log("Profile used:", res.profile);
9
+ console.log("Timings:", res.timings);
10
+ console.log("Response headers sent (as seen by server):");
11
+ const parsed = JSON.parse(res.body);
12
+ console.log(JSON.stringify(parsed.headers, null, 2));
13
+ } catch (err) {
14
+ console.error("Error:", err.message);
15
+ }
16
+ })();
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Compare how different browser fingerprint profiles appear to a server.
3
+ * Run: node examples/compare-profiles.js
4
+ */
5
+ const { get, PROFILES } = require("../src");
6
+
7
+ (async () => {
8
+ const profileNames = Object.keys(PROFILES);
9
+ console.log(`Comparing ${profileNames.length} profiles against httpbin.org/headers\n`);
10
+
11
+ for (const profileName of profileNames) {
12
+ try {
13
+ console.log(`\n--- ${PROFILES[profileName].name} ---`);
14
+ const res = await get("https://httpbin.org/headers", { profile: profileName });
15
+ const parsed = JSON.parse(res.body);
16
+ console.log("Status:", res.status);
17
+ console.log("User-Agent:", parsed.headers["User-Agent"]);
18
+ console.log("Total time:", res.timings.total + "ms");
19
+ } catch (err) {
20
+ console.error(` Error: ${err.message}`);
21
+ }
22
+ }
23
+ })();
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "h2-fingerprint-client",
3
+ "version": "1.0.0",
4
+ "description": "HTTP/2 fingerprint-aware request library for Node.js — mimics real browser SETTINGS frames, pseudo-header order, and header ordering for research purposes.",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "example": "node examples/basic.js",
8
+ "compare": "node examples/compare-profiles.js"
9
+ },
10
+ "keywords": [
11
+ "http2",
12
+ "fingerprint",
13
+ "h2",
14
+ "browser",
15
+ "tls",
16
+ "anti-bot",
17
+ "scraping",
18
+ "research",
19
+ "settings-frame",
20
+ "header-order"
21
+ ],
22
+ "author": "Israze",
23
+ "license": "MIT",
24
+ "engines": {
25
+ "node": ">=14.0.0"
26
+ }
27
+ }
package/src/client.js ADDED
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+
3
+ const http2 = require("http2");
4
+ const { PROFILES } = require("./profiles");
5
+
6
+ function buildOrderedHeaders(profile, method, urlObj, extraHeaders = {}) {
7
+ const merged = { ...profile.headers, ...extraHeaders };
8
+ const ordered = {};
9
+
10
+ for (const pseudo of profile.pseudoHeaderOrder) {
11
+ if (pseudo === ":method") ordered[":method"] = method.toUpperCase();
12
+ if (pseudo === ":authority") ordered[":authority"] = urlObj.host;
13
+ if (pseudo === ":scheme") ordered[":scheme"] = urlObj.protocol.replace(":", "");
14
+ if (pseudo === ":path") ordered[":path"] = urlObj.pathname + urlObj.search;
15
+ }
16
+
17
+ for (const key of profile.headerOrder) {
18
+ if (merged[key] !== undefined) ordered[key] = merged[key];
19
+ }
20
+
21
+ for (const [key, value] of Object.entries(merged)) {
22
+ if (!ordered[key]) ordered[key] = value;
23
+ }
24
+
25
+ return ordered;
26
+ }
27
+
28
+ function request(url, options = {}) {
29
+ const {
30
+ profile: profileName = "chrome120",
31
+ method = "GET",
32
+ headers: extraHeaders = {},
33
+ body = null,
34
+ timeout = 15000,
35
+ } = options;
36
+
37
+ const profile = PROFILES[profileName];
38
+ if (!profile) {
39
+ return Promise.reject(
40
+ new Error(`Unknown profile "${profileName}". Available: ${Object.keys(PROFILES).join(", ")}`)
41
+ );
42
+ }
43
+
44
+ const urlObj = new URL(url);
45
+ if (urlObj.protocol !== "https:") {
46
+ return Promise.reject(new Error("HTTP/2 requires HTTPS. Use an https:// URL."));
47
+ }
48
+
49
+ return new Promise((resolve, reject) => {
50
+ const startTime = Date.now();
51
+ let connectTime = null;
52
+
53
+ const client = http2.connect(urlObj.origin, {
54
+ settings: profile.settings,
55
+ ALPNProtocols: ["h2"],
56
+ });
57
+
58
+ client.on("connect", () => { connectTime = Date.now() - startTime; });
59
+ client.on("error", (err) => { client.destroy(); reject(err); });
60
+
61
+ const orderedHeaders = buildOrderedHeaders(profile, method, urlObj, extraHeaders);
62
+ const req = client.request(orderedHeaders);
63
+
64
+ if (body) req.write(body);
65
+ req.end();
66
+
67
+ const timer = setTimeout(() => {
68
+ req.destroy();
69
+ client.destroy();
70
+ reject(new Error(`Request timed out after ${timeout}ms`));
71
+ }, timeout);
72
+
73
+ let responseHeaders = {};
74
+ let status = null;
75
+
76
+ req.on("response", (headers) => {
77
+ status = headers[":status"];
78
+ for (const [k, v] of Object.entries(headers)) {
79
+ if (!k.startsWith(":")) responseHeaders[k] = v;
80
+ }
81
+ });
82
+
83
+ const chunks = [];
84
+ req.on("data", (chunk) => chunks.push(chunk));
85
+
86
+ req.on("end", () => {
87
+ clearTimeout(timer);
88
+ client.close();
89
+ resolve({
90
+ status,
91
+ headers: responseHeaders,
92
+ body: Buffer.concat(chunks).toString("utf8"),
93
+ profile: profile.name,
94
+ timings: { connect: connectTime, total: Date.now() - startTime },
95
+ });
96
+ });
97
+
98
+ req.on("error", (err) => {
99
+ clearTimeout(timer);
100
+ client.destroy();
101
+ reject(err);
102
+ });
103
+ });
104
+ }
105
+
106
+ const get = (url, options = {}) => request(url, { ...options, method: "GET" });
107
+ const post = (url, options = {}) => request(url, { ...options, method: "POST" });
108
+
109
+ module.exports = { request, get, post, PROFILES };
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * HTTP/2 SETTINGS frame values and pseudo-header ordering
5
+ * based on real browser captures.
6
+ *
7
+ * References:
8
+ * - https://www.rfc-editor.org/rfc/rfc7540
9
+ * - https://browserleaks.com/http2
10
+ * - https://tls.peet.ws/
11
+ */
12
+
13
+ const FINGERPRINTS = {
14
+ chrome120: {
15
+ name: 'Chrome 120',
16
+ settings: {
17
+ HEADER_TABLE_SIZE: 65536,
18
+ ENABLE_PUSH: 0,
19
+ INITIAL_WINDOW_SIZE: 6291456,
20
+ MAX_HEADER_LIST_SIZE: 262144,
21
+ },
22
+ windowUpdate: 15663105,
23
+ pseudoHeaderOrder: [':method', ':authority', ':scheme', ':path'],
24
+ headerOrder: [
25
+ 'cache-control',
26
+ 'sec-ch-ua',
27
+ 'sec-ch-ua-mobile',
28
+ 'sec-ch-ua-platform',
29
+ 'upgrade-insecure-requests',
30
+ 'user-agent',
31
+ 'accept',
32
+ 'sec-fetch-site',
33
+ 'sec-fetch-mode',
34
+ 'sec-fetch-user',
35
+ 'sec-fetch-dest',
36
+ 'accept-encoding',
37
+ 'accept-language',
38
+ ],
39
+ headers: {
40
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
41
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
42
+ 'accept-encoding': 'gzip, deflate, br',
43
+ 'accept-language': 'en-US,en;q=0.9',
44
+ 'cache-control': 'max-age=0',
45
+ 'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
46
+ 'sec-ch-ua-mobile': '?0',
47
+ 'sec-ch-ua-platform': '"Windows"',
48
+ 'sec-fetch-dest': 'document',
49
+ 'sec-fetch-mode': 'navigate',
50
+ 'sec-fetch-site': 'none',
51
+ 'sec-fetch-user': '?1',
52
+ 'upgrade-insecure-requests': '1',
53
+ },
54
+ priority: {
55
+ exclusive: true,
56
+ streamDependency: 0,
57
+ weight: 255,
58
+ },
59
+ },
60
+
61
+ firefox121: {
62
+ name: 'Firefox 121',
63
+ settings: {
64
+ HEADER_TABLE_SIZE: 65536,
65
+ INITIAL_WINDOW_SIZE: 131072,
66
+ MAX_FRAME_SIZE: 16384,
67
+ },
68
+ windowUpdate: 12517377,
69
+ pseudoHeaderOrder: [':method', ':path', ':authority', ':scheme'],
70
+ headerOrder: [
71
+ 'user-agent',
72
+ 'accept',
73
+ 'accept-language',
74
+ 'accept-encoding',
75
+ 'connection',
76
+ ],
77
+ headers: {
78
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
79
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
80
+ 'accept-encoding': 'gzip, deflate, br',
81
+ 'accept-language': 'en-US,en;q=0.5',
82
+ 'connection': 'keep-alive',
83
+ 'upgrade-insecure-requests': '1',
84
+ },
85
+ priority: {
86
+ exclusive: false,
87
+ streamDependency: 0,
88
+ weight: 42,
89
+ },
90
+ },
91
+
92
+ safari17: {
93
+ name: 'Safari 17',
94
+ settings: {
95
+ INITIAL_WINDOW_SIZE: 4194304,
96
+ MAX_CONCURRENT_STREAMS: 100,
97
+ },
98
+ windowUpdate: 10485760,
99
+ pseudoHeaderOrder: [':method', ':scheme', ':path', ':authority'],
100
+ headerOrder: [
101
+ 'user-agent',
102
+ 'accept',
103
+ 'accept-language',
104
+ 'accept-encoding',
105
+ ],
106
+ headers: {
107
+ 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
108
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
109
+ 'accept-encoding': 'gzip, deflate, br',
110
+ 'accept-language': 'en-US,en;q=0.9',
111
+ },
112
+ priority: null,
113
+ },
114
+ };
115
+
116
+ function getFingerprint(name) {
117
+ const fp = FINGERPRINTS[name];
118
+ if (!fp) {
119
+ throw new Error(
120
+ `Unknown fingerprint: "${name}". Available: ${Object.keys(FINGERPRINTS).join(', ')}`
121
+ );
122
+ }
123
+ return fp;
124
+ }
125
+
126
+ function listFingerprints() {
127
+ return Object.keys(FINGERPRINTS);
128
+ }
129
+
130
+ module.exports = { getFingerprint, listFingerprints, FINGERPRINTS };
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ const { request, get, post, PROFILES } = require("./client");
4
+
5
+ module.exports = { request, get, post, PROFILES };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Browser HTTP/2 fingerprint profiles
3
+ * Based on public research into browser HTTP/2 SETTINGS frames and header ordering.
4
+ *
5
+ * References:
6
+ * - https://www.rfc-editor.org/rfc/rfc7540 (HTTP/2 spec)
7
+ * - https://www.rfc-editor.org/rfc/rfc7541 (HPACK spec)
8
+ * - https://browserleaks.com/http2
9
+ * - https://tls.peet.ws/
10
+ */
11
+
12
+ const PROFILES = {
13
+ chrome120: {
14
+ name: "Chrome 120 / Windows 11",
15
+ settings: {
16
+ HEADER_TABLE_SIZE: 65536,
17
+ ENABLE_PUSH: 0,
18
+ INITIAL_WINDOW_SIZE: 6291456,
19
+ MAX_HEADER_LIST_SIZE: 262144,
20
+ },
21
+ windowUpdate: 15663105,
22
+ pseudoHeaderOrder: [":method", ":authority", ":scheme", ":path"],
23
+ headerOrder: [
24
+ "cache-control",
25
+ "sec-ch-ua",
26
+ "sec-ch-ua-mobile",
27
+ "sec-ch-ua-platform",
28
+ "upgrade-insecure-requests",
29
+ "user-agent",
30
+ "accept",
31
+ "sec-fetch-site",
32
+ "sec-fetch-mode",
33
+ "sec-fetch-user",
34
+ "sec-fetch-dest",
35
+ "accept-encoding",
36
+ "accept-language",
37
+ ],
38
+ headers: {
39
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
40
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
41
+ "accept-language": "en-US,en;q=0.9",
42
+ "accept-encoding": "gzip, deflate, br",
43
+ "cache-control": "max-age=0",
44
+ "sec-ch-ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
45
+ "sec-ch-ua-mobile": "?0",
46
+ "sec-ch-ua-platform": '"Windows"',
47
+ "sec-fetch-dest": "document",
48
+ "sec-fetch-mode": "navigate",
49
+ "sec-fetch-site": "none",
50
+ "sec-fetch-user": "?1",
51
+ "upgrade-insecure-requests": "1",
52
+ },
53
+ },
54
+
55
+ firefox121: {
56
+ name: "Firefox 121 / Windows 11",
57
+ settings: {
58
+ HEADER_TABLE_SIZE: 65536,
59
+ INITIAL_WINDOW_SIZE: 131072,
60
+ MAX_FRAME_SIZE: 16384,
61
+ ENABLE_PUSH: 0,
62
+ },
63
+ windowUpdate: 12517377,
64
+ pseudoHeaderOrder: [":method", ":path", ":authority", ":scheme"],
65
+ headerOrder: [
66
+ "user-agent",
67
+ "accept",
68
+ "accept-language",
69
+ "accept-encoding",
70
+ "upgrade-insecure-requests",
71
+ ],
72
+ headers: {
73
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
74
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
75
+ "accept-language": "en-US,en;q=0.5",
76
+ "accept-encoding": "gzip, deflate, br",
77
+ "upgrade-insecure-requests": "1",
78
+ "sec-fetch-dest": "document",
79
+ "sec-fetch-mode": "navigate",
80
+ "sec-fetch-site": "none",
81
+ "sec-fetch-user": "?1",
82
+ },
83
+ },
84
+
85
+ safari17: {
86
+ name: "Safari 17 / macOS Sonoma",
87
+ settings: {
88
+ HEADER_TABLE_SIZE: 4096,
89
+ ENABLE_PUSH: 0,
90
+ INITIAL_WINDOW_SIZE: 4194304,
91
+ MAX_FRAME_SIZE: 16384,
92
+ MAX_HEADER_LIST_SIZE: 16384,
93
+ },
94
+ windowUpdate: 10420225,
95
+ pseudoHeaderOrder: [":method", ":scheme", ":path", ":authority"],
96
+ headerOrder: [
97
+ "accept",
98
+ "user-agent",
99
+ "accept-language",
100
+ "accept-encoding",
101
+ ],
102
+ headers: {
103
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
104
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
105
+ "accept-language": "en-US,en;q=0.9",
106
+ "accept-encoding": "gzip, deflate, br",
107
+ },
108
+ },
109
+ };
110
+
111
+ module.exports = { PROFILES };
package/src/session.js ADDED
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ const { request } = require('./client');
4
+ const { getFingerprint, listFingerprints } = require('./fingerprints');
5
+
6
+ /**
7
+ * H2Session - maintains a consistent fingerprint identity
8
+ * across multiple requests (same headers, same settings).
9
+ *
10
+ * Useful for multi-step scraping flows where consistency matters.
11
+ */
12
+ class H2Session {
13
+ /**
14
+ * @param {object} options
15
+ * @param {string} [options.fingerprint='chrome120']
16
+ * @param {object} [options.headers={}] - extra headers applied to every request
17
+ * @param {number} [options.timeout=15000]
18
+ */
19
+ constructor(options = {}) {
20
+ this.fingerprint = options.fingerprint || 'chrome120';
21
+ this.baseHeaders = options.headers || {};
22
+ this.timeout = options.timeout || 15000;
23
+ this.cookieJar = {};
24
+
25
+ // Validate fingerprint exists on construction
26
+ getFingerprint(this.fingerprint);
27
+ }
28
+
29
+ /**
30
+ * Merges stored cookies into request headers.
31
+ * @param {string} host
32
+ * @returns {object}
33
+ */
34
+ _getCookieHeader(host) {
35
+ const cookies = this.cookieJar[host];
36
+ if (!cookies || Object.keys(cookies).length === 0) return {};
37
+ const cookieStr = Object.entries(cookies)
38
+ .map(([k, v]) => `${k}=${v}`)
39
+ .join('; ');
40
+ return { cookie: cookieStr };
41
+ }
42
+
43
+ /**
44
+ * Parses and stores set-cookie headers from a response.
45
+ * @param {string} host
46
+ * @param {object} responseHeaders
47
+ */
48
+ _storeCookies(host, responseHeaders) {
49
+ const raw = responseHeaders['set-cookie'];
50
+ if (!raw) return;
51
+ const list = Array.isArray(raw) ? raw : [raw];
52
+ if (!this.cookieJar[host]) this.cookieJar[host] = {};
53
+ for (const entry of list) {
54
+ const [pair] = entry.split(';');
55
+ const [name, ...rest] = pair.split('=');
56
+ if (name) this.cookieJar[host][name.trim()] = rest.join('=').trim();
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Makes a request using this session's fingerprint + cookies.
62
+ * @param {string} url
63
+ * @param {object} [options]
64
+ * @returns {Promise<{ status, headers, body }>}
65
+ */
66
+ async request(url, options = {}) {
67
+ const parsed = new URL(url);
68
+ const cookieHeaders = this._getCookieHeader(parsed.host);
69
+
70
+ const res = await request(url, {
71
+ fingerprint: this.fingerprint,
72
+ timeout: this.timeout,
73
+ ...options,
74
+ headers: {
75
+ ...this.baseHeaders,
76
+ ...cookieHeaders,
77
+ ...(options.headers || {}),
78
+ },
79
+ });
80
+
81
+ this._storeCookies(parsed.host, res.headers);
82
+ return res;
83
+ }
84
+
85
+ get(url, options = {}) {
86
+ return this.session(url, { ...options, method: 'GET' });
87
+ }
88
+
89
+ post(url, options = {}) {
90
+ return this.request(url, { ...options, method: 'POST' });
91
+ }
92
+
93
+ /**
94
+ * Returns current fingerprint profile info.
95
+ */
96
+ inspect() {
97
+ const fp = getFingerprint(this.fingerprint);
98
+ return {
99
+ fingerprint: this.fingerprint,
100
+ name: fp.name,
101
+ settings: fp.settings,
102
+ pseudoHeaderOrder: fp.pseudoHeaderOrder,
103
+ cookieJar: this.cookieJar,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Clears stored cookies.
109
+ */
110
+ clearCookies() {
111
+ this.cookieJar = {};
112
+ }
113
+ }
114
+
115
+ module.exports = { H2Session };