@rester159/blacktip 0.2.0 → 0.5.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/README.md +25 -0
  3. package/dist/akamai-sensor.d.ts +128 -0
  4. package/dist/akamai-sensor.d.ts.map +1 -0
  5. package/dist/akamai-sensor.js +190 -0
  6. package/dist/akamai-sensor.js.map +1 -0
  7. package/dist/behavioral/parsers.d.ts +89 -0
  8. package/dist/behavioral/parsers.d.ts.map +1 -0
  9. package/dist/behavioral/parsers.js +223 -0
  10. package/dist/behavioral/parsers.js.map +1 -0
  11. package/dist/blacktip.d.ts +68 -1
  12. package/dist/blacktip.d.ts.map +1 -1
  13. package/dist/blacktip.js +140 -1
  14. package/dist/blacktip.js.map +1 -1
  15. package/dist/browser-core.d.ts +10 -0
  16. package/dist/browser-core.d.ts.map +1 -1
  17. package/dist/browser-core.js +49 -0
  18. package/dist/browser-core.js.map +1 -1
  19. package/dist/diagnostics.d.ts +31 -0
  20. package/dist/diagnostics.d.ts.map +1 -1
  21. package/dist/diagnostics.js +146 -0
  22. package/dist/diagnostics.js.map +1 -1
  23. package/dist/identity-pool.d.ts +160 -0
  24. package/dist/identity-pool.d.ts.map +1 -0
  25. package/dist/identity-pool.js +288 -0
  26. package/dist/identity-pool.js.map +1 -0
  27. package/dist/index.d.ts +11 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +11 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/tls-rewriter.d.ts +74 -0
  32. package/dist/tls-rewriter.d.ts.map +1 -0
  33. package/dist/tls-rewriter.js +203 -0
  34. package/dist/tls-rewriter.js.map +1 -0
  35. package/dist/tls-side-channel.d.ts +91 -0
  36. package/dist/tls-side-channel.d.ts.map +1 -0
  37. package/dist/tls-side-channel.js +248 -0
  38. package/dist/tls-side-channel.js.map +1 -0
  39. package/dist/types.d.ts +46 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/types.js.map +1 -1
  42. package/docs/akamai-bypass.md +257 -0
  43. package/docs/akamai-sensor.md +183 -0
  44. package/docs/anti-bot-validation.md +84 -0
  45. package/docs/calibration-validation.md +93 -0
  46. package/docs/identity-pool.md +176 -0
  47. package/docs/tls-rewriting.md +121 -0
  48. package/docs/tls-side-channel.md +83 -0
  49. package/native/tls-client/go.mod +21 -0
  50. package/native/tls-client/go.sum +36 -0
  51. package/native/tls-client/main.go +216 -0
  52. package/package.json +8 -2
  53. package/scripts/fit-cmu-keystroke.mjs +186 -0
@@ -0,0 +1,121 @@
1
+ # Full TLS rewriting (v0.5.0)
2
+
3
+ The v0.5.0 answer to "every wire request should present a real Chrome TLS fingerprint, not just the gating ones." Without a TCP-level MITM proxy and the OS-specific cert installation hell that entails, BlackTip uses Chrome DevTools Protocol's `Fetch.enable` to pause every HTTP request the browser issues, hand it to the Go `bogdanfinn/tls-client` daemon for upstream execution, and fulfill the response back through CDP. **The browser never opens an upstream TCP connection. Every wire request presents real Chrome TLS via Go.**
4
+
5
+ ## What this gives you over the v0.3.0 side-channel
6
+
7
+ The v0.3.0 `bt.fetchWithTls()` only handled gating requests the caller made explicitly. Page subresources, XHR, `fetch()` from page JS — all of those went through Chrome's own TLS, meaning the host OS's Chrome fingerprint reached the wire on every subresource. With the v0.5.0 rewriter installed, **every** subresource also goes through Go.
8
+
9
+ What changes:
10
+ - **Cross-platform UA spoofing is restored.** v0.2.0's L016 fix removed the broken context-level UA override because it caused User-Agent / Sec-Ch-Ua mismatch. With the rewriter, the daemon controls every header on the wire — spoof to your heart's content.
11
+ - **JA4 / GREASE / H2 fingerprint of every request matches real Chrome 133** (or whatever profile you select), regardless of what Chrome the host OS has installed. Useful when you need to impersonate a specific Chrome version that doesn't ship for your platform.
12
+ - **One source of truth for TLS.** No more "the gating request matched but the subresource didn't" inconsistency that anti-bot vendors can fingerprint at the session level.
13
+
14
+ ## Validated end-to-end
15
+
16
+ Run `npx vitest run tests/tls-rewriter.integration.test.ts` (requires the Go daemon binary). The load-bearing assertion is:
17
+
18
+ ```
19
+ JA4: t13d1516h2_8daaf6152771_d8a2da3f94cd (textbook Chrome 133)
20
+ First cipher: TLS_GREASE (0x3A3A) (rotated each connection)
21
+ HTTP/2 fingerprint: 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
22
+ ```
23
+
24
+ This is the JA4 reaching tls.peet.ws **via the browser navigation** (not via a direct daemon call). The browser navigated to `tls.peet.ws/api/all`, the JSON it received back came from an upstream connection that the Go daemon opened, not Chrome.
25
+
26
+ ## Usage
27
+
28
+ ```typescript
29
+ import { BlackTip } from '@rester159/blacktip';
30
+
31
+ const bt = new BlackTip({
32
+ logLevel: 'info',
33
+ timeout: 30_000,
34
+ // The headline knob — when set to 'all', every request the browser
35
+ // issues is intercepted via CDP Fetch and forwarded through the Go
36
+ // daemon for upstream execution.
37
+ tlsRewriting: 'all',
38
+ });
39
+
40
+ await bt.launch();
41
+ await bt.navigate('https://www.opentable.com/');
42
+
43
+ // Verify the rewriter is doing what you expect
44
+ console.log(bt.getTlsRewriterStats());
45
+ // {
46
+ // intercepted: 47,
47
+ // fulfilled: 45,
48
+ // fellThrough: 0,
49
+ // webSocketLeaks: 0,
50
+ // avgDurationMs: 73
51
+ // }
52
+
53
+ await bt.close();
54
+ ```
55
+
56
+ The daemon binary must be built first. Go is the only build dependency:
57
+
58
+ ```bash
59
+ cd native/tls-client
60
+ go build -o blacktip-tls . # Linux / macOS
61
+ go build -o blacktip-tls.exe . # Windows
62
+ ```
63
+
64
+ If the daemon binary is missing when `tlsRewriting: 'all'` is set, `bt.launch()` throws rather than silently falling back to native Chrome TLS that the caller didn't ask for.
65
+
66
+ ## Architecture
67
+
68
+ The rewriter is installed as a Patchright/Playwright `context.route('**/*', handler)` hook, which is the high-level wrapper around CDP `Fetch.enable`. We use the route handler so we don't need to manage CDP sessions ourselves.
69
+
70
+ For each intercepted request, the handler:
71
+
72
+ 1. Reads URL, method, headers, body via Playwright's `Request` API.
73
+ 2. Strips request headers Chrome's lifecycle owns: `Host`, `Content-Length`, `Connection`, `Keep-Alive`, `Transfer-Encoding`, etc.
74
+ 3. Calls `TlsSideChannel.fetch()` with the cleaned-up request — the daemon makes the upstream call with real Chrome TLS.
75
+ 4. Strips response headers Chrome re-computes: `Content-Length`, `Content-Encoding`, `Transfer-Encoding`, `Connection`, `Keep-Alive`.
76
+ 5. Multi-valued headers are joined: `Set-Cookie` with `\n` (so each cookie stays separate), everything else with `, `.
77
+ 6. Calls `route.fulfill({ status, headers, body })` with the daemon's response — the browser receives it as if Chrome had made the request itself.
78
+
79
+ The daemon stays alive for the lifetime of the browser context. On `bt.close()`, the rewriter is uninstalled and the daemon is shut down via its existing `TlsSideChannel.close()` path.
80
+
81
+ ## Chrome flags
82
+
83
+ When `tlsRewriting: 'all'` is set, BlackTip automatically launches Chrome with `--disable-quic` to force HTTP/1.1 and HTTP/2 only. This is required because Chrome handles QUIC at a layer below CDP Fetch — QUIC requests would bypass the rewriter entirely and present Chrome's native TLS to the wire. With QUIC disabled, every HTTP request flows through Fetch and through us.
84
+
85
+ Real Chrome users disable QUIC routinely (corporate network policies, debugging) so this isn't a fingerprinting tell on its own. If you need QUIC for some reason (rare), set `tlsRewriting: 'off'` and use the v0.3.0 side-channel for the requests that matter most.
86
+
87
+ ## Limitations
88
+
89
+ ### WebSocket leaks
90
+
91
+ Chrome handles WebSocket frames at a layer below CDP `Fetch.enable`. The initial HTTP `Upgrade: websocket` request can be intercepted, but the actual WebSocket frames after the upgrade go straight through Chrome's native TLS. The rewriter detects WebSocket upgrades, logs a warning, and falls through to `route.continue()` so the upgrade succeeds (and the WebSocket works) — but those frames present Chrome's host-OS TLS, not the daemon's.
92
+
93
+ Mitigation: if your target uses WebSockets and you need full TLS rewriting on them, the only honest answer today is "use a proxy that handles WebSocket framing" (which is back to TCP-level MITM). The rewriter's `webSocketLeaks` stat counts how many you saw so you can decide whether to care.
94
+
95
+ ### Streaming responses
96
+
97
+ CDP `Fetch.fulfillRequest` requires a complete body. The rewriter buffers each response fully before fulfilling, which is fine for HTML, JSON, CSS, JS, fonts, images, and typical web pages — but bad for video streams, large file downloads, and Server-Sent Events. For those, set `tlsRewriting: 'off'` and use the v0.3.0 side-channel selectively for the gating requests.
98
+
99
+ ### HTTP/3 / QUIC
100
+
101
+ Disabled via `--disable-quic` automatically when the rewriter is on (see above). If a server only serves HTTP/3, the rewriter can't reach it.
102
+
103
+ ### Per-request overhead
104
+
105
+ Each intercepted request adds 5–10ms of round-trip overhead through the daemon (measured: `avgDurationMs ≈ 70-100ms` for typical small responses, dominated by the actual upstream network latency). On a typical page with 50 subresources that's 250–500ms added to the total page load. Acceptable for stealth-critical use cases; not acceptable for high-throughput crawling.
106
+
107
+ ### Stats accounting
108
+
109
+ `intercepted` may be slightly higher than `fulfilled + fellThrough + webSocketLeaks` (off by 1–5 on a typical page) when requests are aborted by Chrome's navigation lifecycle before the route handler completes. The functional behavior is correct — the page renders properly — but the counter doesn't perfectly account for the cancelled-in-flight cases. Don't use the stats for billing.
110
+
111
+ ## When to use which
112
+
113
+ | Need | Use |
114
+ |---|---|
115
+ | Make a single gating request before launching the browser | `bt.fetchWithTls()` (v0.3.0 side-channel) |
116
+ | Make sure every subresource also presents real Chrome TLS | `tlsRewriting: 'all'` (v0.5.0 rewriter) |
117
+ | Cross-platform UA spoofing (Linux pretending to be Mac) | `tlsRewriting: 'all'` |
118
+ | WebSocket-heavy app (chat, real-time stocks) | `tlsRewriting: 'off'` + selective side-channel |
119
+ | Video streaming or large downloads | `tlsRewriting: 'off'` |
120
+ | Maximum throughput crawling | `tlsRewriting: 'off'` |
121
+ | Stealth-critical, throughput-flexible | `tlsRewriting: 'all'` |
@@ -0,0 +1,83 @@
1
+ # TLS side-channel (v0.3.0)
2
+
3
+ The v0.3.0 answer to "an edge gates the very first request before BlackTip's browser even has a session." BlackTip ships a Go-based daemon built on `bogdanfinn/tls-client` that performs HTTP requests with a real Chrome TLS ClientHello, real H2 frame settings, and real H2 frame order. You use it to make gating requests the browser can't make through itself, then inject the resulting cookies into the browser session before navigating.
4
+
5
+ ## When you need this
6
+
7
+ Most BlackTip flows do not need the TLS side-channel. The browser's own TLS via `channel: 'chrome'` is real Chrome and passes every detector we've validated against. The side-channel is for the cases where it isn't enough:
8
+
9
+ 1. **First-request edge gating.** Some Akamai-protected sites refuse to serve a session cookie to a navigation that doesn't already have one. You hit them via the side-channel first (which goes through `bm_s` → sensor data POST → `bm_sv` issuance), then inject the resulting cookies into the browser and navigate normally.
10
+ 2. **Cross-platform User-Agent spoofing.** You're running on Linux but want the target to see Windows. The browser's TLS comes from the host OS Chrome, so you can't fake the platform without a TLS rewriter. The side-channel can. v0.2.0's L016 fix removed the broken UA-override path; this is the supported alternative.
11
+ 3. **API-level operations.** You want to call a JSON API the site exposes, but the API edge enforces the same TLS profile as the browser. Use the side-channel to call the API without paying browser-render overhead per request.
12
+ 4. **Pre-warming for proxy rotation.** You're rotating residential proxies and need to "warm" each new IP with a Chrome-TLS handshake before the browser session uses it.
13
+
14
+ ## Build the daemon
15
+
16
+ The daemon is a small Go program in `native/tls-client/`. Go is the only build dependency. Install Go from https://go.dev/dl/ then:
17
+
18
+ ```bash
19
+ cd native/tls-client
20
+ go build -o blacktip-tls . # Linux / macOS
21
+ go build -o blacktip-tls.exe . # Windows
22
+ ```
23
+
24
+ The build produces a single ~14 MB statically-linked binary. BlackTip resolves it via `native/tls-client/blacktip-tls[.exe]` automatically; override with `BLACKTIP_TLS_BIN=/abs/path/to/binary` if you want to ship it elsewhere.
25
+
26
+ ## Usage
27
+
28
+ ```typescript
29
+ import { BlackTip } from '@rester159/blacktip';
30
+
31
+ const bt = new BlackTip({ logLevel: 'info' });
32
+ await bt.launch();
33
+
34
+ // 1. Make the gating request through the side-channel.
35
+ const resp = await bt.fetchWithTls({
36
+ url: 'https://www.opentable.com/',
37
+ headers: {
38
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
39
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
40
+ 'Accept-Language': 'en-US,en;q=0.9',
41
+ },
42
+ });
43
+ console.log('TLS fetch status:', resp.status);
44
+ console.log('Earned cookies:', resp.cookies.map(c => c.name));
45
+
46
+ // 2. Inject the cookies into the browser session.
47
+ const injected = await bt.injectTlsCookies(resp, 'https://www.opentable.com/');
48
+ console.log('Injected', injected, 'cookies');
49
+
50
+ // 3. Navigate normally — the browser carries the side-channel-earned tokens.
51
+ await bt.navigate('https://www.opentable.com/');
52
+ await bt.waitForStable();
53
+ // ... rest of flow
54
+
55
+ await bt.close(); // Closes the TLS daemon too
56
+ ```
57
+
58
+ ## Architecture
59
+
60
+ - **Wire protocol**: newline-delimited JSON over the daemon's stdin/stdout. Each request has a string `id`; responses match by id, so multiple in-flight `fetch()` calls don't interleave.
61
+ - **Daemon lifecycle**: spawned lazily on first `fetchWithTls()` call, kept alive until `bt.close()`. No subprocess startup cost per request.
62
+ - **Concurrency**: the daemon handles each request in its own Go goroutine. The Node side dispatches them in parallel and reassembles by id.
63
+ - **Profile selection**: defaults to `chrome_133`. Override per-request via `bt.fetchWithTls({ url, profile: 'chrome_124' })`. Available profiles are whatever `bogdanfinn/tls-client/profiles` ships — at the time of writing: `chrome_120`, `chrome_124`, `chrome_131`, `chrome_133`, `firefox_120`, `safari_ios_16_0`.
64
+ - **Body encoding**: bodies are base64 on the wire to avoid newline / Unicode issues with the line-delimited protocol. The wrapper handles encoding/decoding transparently.
65
+
66
+ ## Validated TLS fingerprint
67
+
68
+ Run via the integration test (`tests/tls-side-channel.integration.test.ts`):
69
+
70
+ ```
71
+ JA4: t13d1516h2_8daaf6152771_d8a2da3f94cd
72
+ First cipher: TLS_GREASE (0x5A5A)
73
+ HTTP/2 Akamai fingerprint: 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
74
+ ```
75
+
76
+ This is byte-for-byte identical to real Chrome 133. The leading `t13d` JA4 prefix confirms TLS 1.3 with Chrome's count signature; the GREASE first cipher confirms proper GREASE rotation; the H2 fingerprint matches Chrome's frame settings and frame order (`m,a,s,p` = method/authority/scheme/path).
77
+
78
+ ## Caveats
79
+
80
+ - **Not a Chrome browser.** The side-channel is HTTP-only — no JavaScript execution, no DOM, no Cookie JAR persistence beyond what you inject manually. You use it to acquire tokens, not to drive a flow.
81
+ - **Akamai sensor data is not bypassed.** A first request to an Akamai-protected URL through the side-channel returns 403 with `bm_s`/`bm_ss`/`bm_so` set — the same place a browser would be at after one request. Akamai expects a sensor data POST to follow before serving 200s. The side-channel collects the session cookies; the sensor POST is not yet automated. You can either (a) inject the bm_s cookies into the browser and let the browser do the sensor POST (this is what the OpenTable validation flow does), or (b) implement the sensor POST yourself.
82
+ - **Always set the User-Agent header.** The Go HTTP client defaults to `Go-http-client/2.0` if you don't override it. That's a textbook automation tell. The wrapper auto-sets `Accept-Language` to `en-US,en;q=0.9` if missing, but UA is your job.
83
+ - **Linux/macOS support is built but not validated in CI.** The `go build` command produces native binaries for whatever platform you run it on; the Node-side `resolveBinaryPath()` picks the right name based on `process.platform`. If you ship to a server, build the daemon in the same OS the server runs.
@@ -0,0 +1,21 @@
1
+ module github.com/rester159/blacktip/native/tls-client
2
+
3
+ go 1.26.2
4
+
5
+ require (
6
+ github.com/andybalholm/brotli v1.2.0 // indirect
7
+ github.com/bdandy/go-errors v1.2.2 // indirect
8
+ github.com/bdandy/go-socks4 v1.2.3 // indirect
9
+ github.com/bogdanfinn/fhttp v0.6.8 // indirect
10
+ github.com/bogdanfinn/quic-go-utls v1.0.9-utls // indirect
11
+ github.com/bogdanfinn/tls-client v1.14.0 // indirect
12
+ github.com/bogdanfinn/utls v1.7.7-barnius // indirect
13
+ github.com/bogdanfinn/websocket v1.5.5-barnius // indirect
14
+ github.com/klauspost/compress v1.18.2 // indirect
15
+ github.com/quic-go/qpack v0.6.0 // indirect
16
+ github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect
17
+ golang.org/x/crypto v0.46.0 // indirect
18
+ golang.org/x/net v0.48.0 // indirect
19
+ golang.org/x/sys v0.39.0 // indirect
20
+ golang.org/x/text v0.32.0 // indirect
21
+ )
@@ -0,0 +1,36 @@
1
+ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
2
+ github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
3
+ github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
4
+ github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
5
+ github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic=
6
+ github.com/bdandy/go-socks4 v1.2.3/go.mod h1:98kiVFgpdogR8aIGLWLvjDVZ8XcKPsSI/ypGrO+bqHI=
7
+ github.com/bogdanfinn/fhttp v0.6.8 h1:LiQyHOY3i0QoxxNB7nq27/nGNNbtPj0fuBPozhR7Ws4=
8
+ github.com/bogdanfinn/fhttp v0.6.8/go.mod h1:A+EKDzMx2hb4IUbMx4TlkoHnaJEiLl8r/1Ss1Y+5e5M=
9
+ github.com/bogdanfinn/quic-go-utls v1.0.9-utls h1:tV6eDEiRbRCcepALSzxR94JUVD3N3ACIiRLgyc2Ep8s=
10
+ github.com/bogdanfinn/quic-go-utls v1.0.9-utls/go.mod h1:aHph9B9H9yPOt5xnhWKSOum27DJAqpiHzwX+gjvaXcg=
11
+ github.com/bogdanfinn/tls-client v1.14.0 h1:vyk7Cn4BIvLAGVuMfb0tP22OqogfO1lYamquQNEZU1A=
12
+ github.com/bogdanfinn/tls-client v1.14.0/go.mod h1:LsU6mXVn8MOFDwTkyRfI7V1BZM1p0wf2ZfZsICW/1fM=
13
+ github.com/bogdanfinn/utls v1.7.7-barnius h1:OuJ497cc7F3yKNVHRsYPQdGggmk5x6+V5ZlrCR7fOLU=
14
+ github.com/bogdanfinn/utls v1.7.7-barnius/go.mod h1:aAK1VZQlpKZClF1WEQeq6kyclbkPq4hz6xTbB5xSlmg=
15
+ github.com/bogdanfinn/websocket v1.5.5-barnius h1:bY+qnxpai1qe7Jmjx+Sds/cmOSpuuLoR8x61rWltjOI=
16
+ github.com/bogdanfinn/websocket v1.5.5-barnius/go.mod h1:gvvEw6pTKHb7yOiFvIfAFTStQWyrm25BMVCTj5wRSsI=
17
+ github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
18
+ github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
19
+ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
20
+ github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
21
+ github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc=
22
+ github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng=
23
+ golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
24
+ golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
25
+ golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
26
+ golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
27
+ golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
28
+ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
29
+ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
30
+ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
31
+ golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
32
+ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
33
+ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
34
+ golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
35
+ golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
36
+ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -0,0 +1,216 @@
1
+ // BlackTip TLS side-channel daemon.
2
+ //
3
+ // Reads newline-delimited JSON requests from stdin, performs HTTP
4
+ // requests with a real Chrome TLS fingerprint via bogdanfinn/tls-client,
5
+ // and writes newline-delimited JSON responses to stdout.
6
+ //
7
+ // This is the v0.3.0 answer to the question: "what do I do when an edge
8
+ // gates the very first request before BlackTip's browser has a session?"
9
+ // You use this daemon to make the gating request — it presents a real
10
+ // Chrome TLS ClientHello, real H2 frames, and real headers — capture
11
+ // the cookies and tokens it gets back, and inject them into the
12
+ // browser session before the user-driven flow continues.
13
+ //
14
+ // Wire format:
15
+ //
16
+ // Request: {"id":"<string>","url":"<string>","method":"<GET|POST|...>","headers":{"...":"..."},"body":"<string base64>","timeoutMs":15000,"profile":"chrome_133"}
17
+ // Response: {"id":"<string>","ok":true,"status":200,"headers":{"...":["...","..."]},"body":"<string base64>","finalUrl":"<string>","durationMs":123}
18
+ // OR
19
+ // {"id":"<string>","ok":false,"error":"<message>","durationMs":123}
20
+ //
21
+ // One JSON object per line in both directions. The Node parent reads
22
+ // stdout line-by-line and matches responses by id. The daemon stays
23
+ // alive across many requests so we don't pay subprocess startup cost
24
+ // per call.
25
+ package main
26
+
27
+ import (
28
+ "bufio"
29
+ "encoding/base64"
30
+ "encoding/json"
31
+ "fmt"
32
+ "io"
33
+ "os"
34
+ "strings"
35
+ "sync"
36
+ "time"
37
+
38
+ http "github.com/bogdanfinn/fhttp"
39
+ tls_client "github.com/bogdanfinn/tls-client"
40
+ "github.com/bogdanfinn/tls-client/profiles"
41
+ )
42
+
43
+ type request struct {
44
+ ID string `json:"id"`
45
+ URL string `json:"url"`
46
+ Method string `json:"method"`
47
+ Headers map[string]string `json:"headers"`
48
+ Body string `json:"body"`
49
+ TimeoutMs int `json:"timeoutMs"`
50
+ Profile string `json:"profile"`
51
+ }
52
+
53
+ type response struct {
54
+ ID string `json:"id"`
55
+ OK bool `json:"ok"`
56
+ Status int `json:"status,omitempty"`
57
+ Headers map[string][]string `json:"headers,omitempty"`
58
+ Body string `json:"body,omitempty"`
59
+ FinalURL string `json:"finalUrl,omitempty"`
60
+ DurationMs int64 `json:"durationMs"`
61
+ Error string `json:"error,omitempty"`
62
+ }
63
+
64
+ // resolveProfile maps a profile name to a tls-client ClientProfile.
65
+ // Defaults to the latest Chrome at the time of writing.
66
+ func resolveProfile(name string) profiles.ClientProfile {
67
+ switch strings.ToLower(name) {
68
+ case "chrome_120":
69
+ return profiles.Chrome_120
70
+ case "chrome_124":
71
+ return profiles.Chrome_124
72
+ case "chrome_131":
73
+ return profiles.Chrome_131
74
+ case "chrome_133":
75
+ return profiles.Chrome_133
76
+ case "firefox_120":
77
+ return profiles.Firefox_120
78
+ case "safari_ios_16_0":
79
+ return profiles.Safari_IOS_16_0
80
+ default:
81
+ return profiles.Chrome_133
82
+ }
83
+ }
84
+
85
+ // buildClient constructs a tls-client with the requested profile and timeout.
86
+ // We rebuild on every request because timeout is per-request and the cost
87
+ // is negligible compared to the network round-trip.
88
+ func buildClient(profile profiles.ClientProfile, timeoutMs int) (tls_client.HttpClient, error) {
89
+ if timeoutMs <= 0 {
90
+ timeoutMs = 15000
91
+ }
92
+ options := []tls_client.HttpClientOption{
93
+ tls_client.WithTimeoutSeconds(timeoutMs / 1000),
94
+ tls_client.WithClientProfile(profile),
95
+ tls_client.WithNotFollowRedirects(),
96
+ }
97
+ return tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
98
+ }
99
+
100
+ func handle(req request) response {
101
+ start := time.Now()
102
+ durationFor := func() int64 { return time.Since(start).Milliseconds() }
103
+
104
+ if req.URL == "" {
105
+ return response{ID: req.ID, OK: false, Error: "url is required", DurationMs: durationFor()}
106
+ }
107
+ method := req.Method
108
+ if method == "" {
109
+ method = "GET"
110
+ }
111
+
112
+ client, err := buildClient(resolveProfile(req.Profile), req.TimeoutMs)
113
+ if err != nil {
114
+ return response{ID: req.ID, OK: false, Error: "buildClient: " + err.Error(), DurationMs: durationFor()}
115
+ }
116
+
117
+ var bodyReader io.Reader
118
+ if req.Body != "" {
119
+ decoded, decErr := base64.StdEncoding.DecodeString(req.Body)
120
+ if decErr != nil {
121
+ return response{ID: req.ID, OK: false, Error: "body base64 decode: " + decErr.Error(), DurationMs: durationFor()}
122
+ }
123
+ bodyReader = strings.NewReader(string(decoded))
124
+ }
125
+
126
+ httpReq, err := http.NewRequest(method, req.URL, bodyReader)
127
+ if err != nil {
128
+ return response{ID: req.ID, OK: false, Error: "NewRequest: " + err.Error(), DurationMs: durationFor()}
129
+ }
130
+
131
+ for k, v := range req.Headers {
132
+ httpReq.Header.Set(k, v)
133
+ }
134
+ // If the caller didn't set Accept-Language, fall back to a Chrome default.
135
+ if httpReq.Header.Get("Accept-Language") == "" {
136
+ httpReq.Header.Set("Accept-Language", "en-US,en;q=0.9")
137
+ }
138
+
139
+ resp, err := client.Do(httpReq)
140
+ if err != nil {
141
+ return response{ID: req.ID, OK: false, Error: "Do: " + err.Error(), DurationMs: durationFor()}
142
+ }
143
+ defer resp.Body.Close()
144
+
145
+ bodyBytes, err := io.ReadAll(resp.Body)
146
+ if err != nil {
147
+ return response{ID: req.ID, OK: false, Error: "read body: " + err.Error(), DurationMs: durationFor()}
148
+ }
149
+
150
+ headers := make(map[string][]string, len(resp.Header))
151
+ for k, v := range resp.Header {
152
+ headers[k] = v
153
+ }
154
+
155
+ finalURL := req.URL
156
+ if resp.Request != nil && resp.Request.URL != nil {
157
+ finalURL = resp.Request.URL.String()
158
+ }
159
+
160
+ return response{
161
+ ID: req.ID,
162
+ OK: true,
163
+ Status: resp.StatusCode,
164
+ Headers: headers,
165
+ Body: base64.StdEncoding.EncodeToString(bodyBytes),
166
+ FinalURL: finalURL,
167
+ DurationMs: durationFor(),
168
+ }
169
+ }
170
+
171
+ func main() {
172
+ scanner := bufio.NewScanner(os.Stdin)
173
+ // Allow large request bodies (e.g. POSTed forms with file fields).
174
+ scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
175
+
176
+ // Stdout writes are mutex-protected so concurrent handlers don't
177
+ // interleave their JSON lines.
178
+ var stdoutMu sync.Mutex
179
+ emit := func(r response) {
180
+ out, err := json.Marshal(r)
181
+ if err != nil {
182
+ out = []byte(fmt.Sprintf(`{"id":%q,"ok":false,"error":"marshal failed: %s"}`, r.ID, err.Error()))
183
+ }
184
+ stdoutMu.Lock()
185
+ defer stdoutMu.Unlock()
186
+ os.Stdout.Write(out)
187
+ os.Stdout.Write([]byte("\n"))
188
+ }
189
+
190
+ var wg sync.WaitGroup
191
+ for scanner.Scan() {
192
+ line := append([]byte(nil), scanner.Bytes()...)
193
+ if len(line) == 0 {
194
+ continue
195
+ }
196
+ var req request
197
+ if err := json.Unmarshal(line, &req); err != nil {
198
+ emit(response{ID: "", OK: false, Error: "unmarshal: " + err.Error()})
199
+ continue
200
+ }
201
+ // Handle requests concurrently — the Go TLS client is goroutine-safe.
202
+ wg.Add(1)
203
+ go func(r request) {
204
+ defer wg.Done()
205
+ emit(handle(r))
206
+ }(req)
207
+ }
208
+ if err := scanner.Err(); err != nil {
209
+ fmt.Fprintln(os.Stderr, "scan error:", err)
210
+ os.Exit(1)
211
+ }
212
+ // Wait for any in-flight requests to drain before exiting. Without
213
+ // this, closing stdin races the goroutines and the parent never
214
+ // sees the response.
215
+ wg.Wait()
216
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rester159/blacktip",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "description": "Stealth browser instrument for AI agents. Real Chrome + patchright CDP stealth + human-calibrated behavioral simulation. Every action is indistinguishable from a human.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,8 +21,14 @@
21
21
  "dist",
22
22
  "README.md",
23
23
  "AGENTS.md",
24
+ "CHANGELOG.md",
24
25
  "LICENSE",
25
- "examples"
26
+ "examples",
27
+ "docs",
28
+ "native/tls-client/main.go",
29
+ "native/tls-client/go.mod",
30
+ "native/tls-client/go.sum",
31
+ "scripts"
26
32
  ],
27
33
  "scripts": {
28
34
  "build": "tsc",