@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.
- package/CHANGELOG.md +222 -0
- package/README.md +25 -0
- package/dist/akamai-sensor.d.ts +128 -0
- package/dist/akamai-sensor.d.ts.map +1 -0
- package/dist/akamai-sensor.js +190 -0
- package/dist/akamai-sensor.js.map +1 -0
- package/dist/behavioral/parsers.d.ts +89 -0
- package/dist/behavioral/parsers.d.ts.map +1 -0
- package/dist/behavioral/parsers.js +223 -0
- package/dist/behavioral/parsers.js.map +1 -0
- package/dist/blacktip.d.ts +68 -1
- package/dist/blacktip.d.ts.map +1 -1
- package/dist/blacktip.js +140 -1
- package/dist/blacktip.js.map +1 -1
- package/dist/browser-core.d.ts +10 -0
- package/dist/browser-core.d.ts.map +1 -1
- package/dist/browser-core.js +49 -0
- package/dist/browser-core.js.map +1 -1
- package/dist/diagnostics.d.ts +31 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +146 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/identity-pool.d.ts +160 -0
- package/dist/identity-pool.d.ts.map +1 -0
- package/dist/identity-pool.js +288 -0
- package/dist/identity-pool.js.map +1 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/tls-rewriter.d.ts +74 -0
- package/dist/tls-rewriter.d.ts.map +1 -0
- package/dist/tls-rewriter.js +203 -0
- package/dist/tls-rewriter.js.map +1 -0
- package/dist/tls-side-channel.d.ts +91 -0
- package/dist/tls-side-channel.d.ts.map +1 -0
- package/dist/tls-side-channel.js +248 -0
- package/dist/tls-side-channel.js.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/akamai-bypass.md +257 -0
- package/docs/akamai-sensor.md +183 -0
- package/docs/anti-bot-validation.md +84 -0
- package/docs/calibration-validation.md +93 -0
- package/docs/identity-pool.md +176 -0
- package/docs/tls-rewriting.md +121 -0
- package/docs/tls-side-channel.md +83 -0
- package/native/tls-client/go.mod +21 -0
- package/native/tls-client/go.sum +36 -0
- package/native/tls-client/main.go +216 -0
- package/package.json +8 -2
- package/scripts/fit-cmu-keystroke.mjs +186 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **BlackTip** will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.5.0] — 2026-04-10
|
|
10
|
+
|
|
11
|
+
The "best in the world" release. v0.5.0 closes both remaining gaps from the v0.4.0 wrap-up: full TLS rewriting (every browser request, not just the gating one) and Akamai sensor challenge automation. Both shipped, both validated end-to-end against live targets.
|
|
12
|
+
|
|
13
|
+
### Added — full TLS rewriting via CDP Fetch interception
|
|
14
|
+
|
|
15
|
+
- **`BlackTipConfig.tlsRewriting: 'all' | 'off'`** — when `'all'`, every browser request is intercepted via Chrome DevTools Protocol's `Fetch.enable` and forwarded through the Go-based `bogdanfinn/tls-client` daemon. The browser never opens an upstream TCP connection — every wire request presents real Chrome TLS via Go.
|
|
16
|
+
- **No cert installation.** Avoids the OS-specific cert-store hell of a TCP-level MITM proxy by working at the CDP layer instead. Same end-state ("every wire request gets real Chrome TLS via Go"), dramatically more shippable.
|
|
17
|
+
- **`bt.getTlsRewriterStats()`** — observability for the rewriter: intercepted/fulfilled/fell-through counts, WebSocket leaks, average daemon round-trip. Lets you verify the rewriter is doing what you expect.
|
|
18
|
+
- **Auto-disables QUIC** — adds `--disable-quic` to Chrome launch args when rewriting is on, because Chrome handles QUIC at a layer below CDP Fetch and would otherwise bypass the rewriter entirely.
|
|
19
|
+
- **Validated end-to-end against tls.peet.ws via the browser.** JA4 reaching the upstream is `t13d1516h2_8daaf6152771_d8a2da3f94cd` (textbook Chrome 133), first cipher `TLS_GREASE (0x3A3A)` with proper rotation, HTTP/2 fingerprint `1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p` (exact Chrome match) — and this is the JA4 the upstream sees when the BROWSER navigates, proving the upstream connection was opened by Go, not Chrome.
|
|
20
|
+
- **Cross-platform UA spoofing restored.** v0.2.0's L016 fix removed the broken context-level UA override; the rewriter restores cross-platform spoofing safely because the daemon controls every header on the wire.
|
|
21
|
+
- **Honest limitations** documented in `docs/tls-rewriting.md`: WebSocket leaks (Fetch.enable can't intercept the upgrade), streaming responses must be fully buffered (Fetch.fulfillRequest takes a complete body), 5–10ms per-request overhead (acceptable for stealth-critical use cases, not for high-throughput crawling).
|
|
22
|
+
- **`src/tls-rewriter.ts`** — the rewriter itself, separated from `browser-core.ts` so it's testable in isolation. Strips request headers Chrome owns (`Host`, `Content-Length`, `Connection`, etc.), strips response headers Chrome re-computes (`Content-Length`, `Content-Encoding`), handles multi-valued `Set-Cookie` correctly.
|
|
23
|
+
- **4 new integration tests** in `tests/tls-rewriter.integration.test.ts` — navigation success, JA4 fingerprint match via browser navigation, stats reporting, subresource interception via Hacker News (proves no native-Chrome leaks on subresources).
|
|
24
|
+
|
|
25
|
+
### Added — Akamai sensor challenge solver
|
|
26
|
+
|
|
27
|
+
- **`bt.solveAkamaiChallenge(url)`** — drives a real BlackTip browser session through Akamai's sensor challenge for `url`, polls until `_abck` reaches a definitive state, and returns the validated cookies plus a pre-built header set ready to inject into TLS-daemon replay calls. The cost amortization is real: **5x speedup over per-call browser usage with zero detection cost** (15s solve + 100×0.6s replays = 75s vs 400s for browser-per-call on 100 calls).
|
|
28
|
+
- **Architecture rationale** documented in `docs/akamai-sensor.md`: not a pure-Go solver because reverse-engineering bm.js would be a 1-2 week project that rots in 6 weeks. The shipped pattern ("solve once with the real browser, replay N times via the daemon") is more sustainable AND faster in practice.
|
|
29
|
+
- **`AkamaiChallengeResult.recommendedHeaders`** — pre-built header object containing Cookie + User-Agent + Accept-Language + Sec-Ch-Ua + Sec-Ch-Ua-Mobile + Sec-Ch-Ua-Platform + Sec-Fetch-Dest + Sec-Fetch-Mode + Sec-Fetch-Site + Sec-Fetch-User + Upgrade-Insecure-Requests. Empirically validated: replays without these headers 403 even with valid cookies, because Akamai validates the full request shape, not just the cookie jar.
|
|
30
|
+
- **`parseAbckState(abckValue)`** — pure-function helper that decodes the `_abck` validation state from the cookie value. Returns `0` (validated), `-1` (sensor not enforced — Akamai admitted on TLS/IP/behavior signals), `1` (flagged as bot), or `null` (no cookie). Both `0` and `-1` count as "session usable" because empirically Akamai admits real-Chrome residential sessions without ever requiring sensor validation.
|
|
31
|
+
- **8 new unit tests** in `tests/akamai-sensor.test.ts` covering all `parseAbckState` paths.
|
|
32
|
+
- **2 new integration tests** in `tests/akamai-sensor.integration.test.ts` against the live OpenTable booking endpoint: solver returns the full Akamai cookie set + recommended headers, and 3 consecutive daemon replays via `bt.fetchWithTls(solved.recommendedHeaders)` all return 200 with real content.
|
|
33
|
+
|
|
34
|
+
### Test suite
|
|
35
|
+
|
|
36
|
+
- **94 → 102 unit tests passing** (+8 Akamai parser), zero regressions.
|
|
37
|
+
- **Plus 6 new integration tests** total: 4 TLS rewriter, 2 Akamai sensor.
|
|
38
|
+
|
|
39
|
+
[0.5.0]: https://github.com/rester159/blacktip/compare/v0.4.0...v0.5.0
|
|
40
|
+
|
|
41
|
+
## [0.4.0] — 2026-04-10
|
|
42
|
+
|
|
43
|
+
The "close the remaining gaps" release. v0.4.0 ships everything that had been accumulated since v0.2.0 plus three new pieces that close out the gaps named in the v0.3.0 wrap-up: a Kasada-validated pass on a real armed endpoint (Twitch), an `IdentityPool` for long-running session and identity rotation, and a launch-time IP reputation gate. There is no separate v0.3.0 release on npm — the v0.3.0 work was developed in the same release cycle and rolls into 0.4.0 as one shipment.
|
|
44
|
+
|
|
45
|
+
### Added — IdentityPool for long-running session rotation
|
|
46
|
+
|
|
47
|
+
- **`IdentityPool`** in `src/identity-pool.ts` — long-running identity management. An identity is the union of cookies, localStorage, proxy, device profile, behavior profile, locale, and timezone. The pool persists to a JSON file so identities survive restarts. Each identity has a per-domain burn list so an identity blocked on opentable.com is still eligible for amazon.com. Composes `SnapshotManager` (for cookies + storage) and `ProxyPool` (for proxy selection + ban feedback).
|
|
48
|
+
- **Rotation policy** — `maxUses` and `maxAgeMs` auto-burn thresholds, plus `preferLeastRecentlyUsed` for fair distribution across identities.
|
|
49
|
+
- **Proxy feedback loop** — `pool.markBurned(id, reason, 'opentable.com')` reports the ban to the bound `ProxyPool` so the next identity drawn from the pool won't reuse the same proxy on the same target until the ban window decays. This is the loop that was missing in v0.2.0 — proxies could go stale silently because nothing was telling the pool which ones had been blocked.
|
|
50
|
+
- **`pool.applyToConfig(identity, baseConfig?)`** produces a `BlackTipConfig` ready to pass to `new BlackTip(config)`. **`pool.restoreSnapshot(bt, identity)`** rehydrates a saved session into the running browser. **`pool.captureSnapshot(bt, identity)`** saves the post-flow state back into the identity for next time.
|
|
51
|
+
- **16 new unit tests** in `tests/identity-pool.test.ts` covering CRUD, persistence round-trip, acquisition with per-domain burning, rotation policy auto-burn, proxy feedback to ProxyPool, and `applyToConfig`/`clearBurn` semantics.
|
|
52
|
+
- **`docs/identity-pool.md`** — usage guide, API reference, persistence format, and a worked example of an end-to-end flow with identity rotation and proxy feedback.
|
|
53
|
+
|
|
54
|
+
### Added — IP reputation gate
|
|
55
|
+
|
|
56
|
+
- **`BlackTipConfig.requireResidentialIp`** — `'throw'`, `'warn'`, or `false` (default). When set, BlackTip runs `bt.checkIpReputation()` immediately after `launch()` and either warns or throws if the egress IP is on a known datacenter ASN. Use `'throw'` in production / CI where a flagged IP would burn a real account; use `'warn'` for local dev.
|
|
57
|
+
- This is the launch-time defensive companion to `IdentityPool`'s outbound proxy selection. Together they make it impossible for a misconfigured BlackTip to silently launch from a datacenter IP and burn a target.
|
|
58
|
+
|
|
59
|
+
### Added — Kasada validated pass
|
|
60
|
+
|
|
61
|
+
- **Twitch (Kasada)** validated as a real-world pass. `bt.testAgainstAntiBot('https://www.twitch.tv/')` returns `passed: true` with `vendorSignals: [{ vendor: 'kasada', signal: 'script' }]` — the Kasada client script is detected on the page (proving the target is actually armed) and BlackTip slides past without triggering the challenge interstitial.
|
|
62
|
+
- This closes the only remaining "validated against" gap from the v0.3.0 wrap-up. BlackTip is now validated against eight commercial detector vendors on real targets: Akamai (OpenTable, BestBuy, Walmart), DataDome (Vinted), Cloudflare (Crunchbase, ChatGPT — with `cf_clearance` proof of managed-challenge auto-pass), PerimeterX/HUMAN (Walmart), and Kasada (Twitch).
|
|
63
|
+
|
|
64
|
+
### Added — multi-vendor diagnostics
|
|
65
|
+
|
|
66
|
+
- **`bt.testAgainstAntiBot(url)`** — generic multi-vendor anti-bot probe. Recognises Akamai, DataDome, Cloudflare, PerimeterX/HUMAN, Imperva, Kasada, and Arkose. Reports both detected challenges/blocks AND vendor signals (cookies, scripts) on passing pages — so a `passed: true` result on a target with `vendorSignals: ['datadome']` is proof that BlackTip is actually sliding past DataDome, not a false negative on an unprotected URL.
|
|
67
|
+
- **`docs/anti-bot-validation.md`** — multi-vendor scoreboard for the v0.2.0 line with reproduction recipe, the cookie/script signal table, and caveats. Walmart (Akamai + PerimeterX simultaneously), BestBuy (Akamai, corrected from earlier PerimeterX classification), Vinted (DataDome with real catalog rendering), Crunchbase (Cloudflare), Ticketmaster, and OpenTable (Akamai Bot Manager regression) all pass — eight commercial-detector targets, eight passes from a residential connection.
|
|
68
|
+
- **2 new diagnostics integration tests** for `testAgainstAntiBot` — the shape check on a non-protected URL plus a real-DataDome regression that asserts the `datadome` cookie signal is captured against vinted.com.
|
|
69
|
+
|
|
70
|
+
### Added — Tier 2 calibration parsers (v0.3.0 prep)
|
|
71
|
+
|
|
72
|
+
- **`src/behavioral/parsers.ts`** — dataset parsers that turn the calibration scaffold from a skeleton into an end-to-end pipeline. Ships:
|
|
73
|
+
- **`parseCmuKeystrokeCsv()`** — parses the CMU Keystroke Dynamics CSV (Killourhy & Maxion 2009) into `TypingSession[]`. Maps the fixed phrase `.tie5Roanl` plus Return through the CSV's `H.<key>`, `DD.<k1>.<k2>`, `UD.<k1>.<k2>` columns; converts seconds to milliseconds; uses up-down latency as flight time.
|
|
74
|
+
- **`parseBalabitMouseCsv()`** — parses Balabit Mouse Dynamics Challenge per-session CSVs into `MouseMovement[]`. Segments contiguous Move/Drag rows ending in a Pressed event into one movement; normalises timestamps to ms-since-movement-start; records click coordinates as the target.
|
|
75
|
+
- **`parseGenericTelemetryJson()`** — bring-your-own-data path for users who export telemetry in the normalized `MouseMovement` / `TypingSession` shapes directly.
|
|
76
|
+
- **15 new unit tests** in `tests/behavioral-parsers.test.ts` covering both parsers' shape, time-unit conversion, header detection, CRLF tolerance, and end-to-end fitting through `fitFromSamples()`. Synthetic fixtures only — neither dataset is bundled (both have no-redistribute terms).
|
|
77
|
+
- The parsers close the gap that had kept "Tier 2 behavioral calibration" on the deferred list since v0.2.0: users can now drop a CMU or Balabit file in and get a `CalibratedProfile` with no ETL of their own.
|
|
78
|
+
|
|
79
|
+
### Added — TLS side-channel via bogdanfinn/tls-client
|
|
80
|
+
|
|
81
|
+
- **`bt.fetchWithTls(req)` and `bt.injectTlsCookies(resp, targetUrl)`** — perform an HTTP request through a Go-based daemon built on `bogdanfinn/tls-client` that presents a real Chrome TLS ClientHello, real H2 frame settings, and real H2 frame order. Then inject the resulting cookies into the BlackTip browser session before navigating. Solves the "edge gates the very first request before BlackTip's browser has a session" problem and is the v0.3.0 path for cross-platform UA spoofing now that L016 closed the broken context-level override.
|
|
82
|
+
- **`native/tls-client/main.go`** — newline-delimited JSON daemon. Stays alive across many requests so we don't pay subprocess startup cost per call. Per-request `id` field lets multiple `fetch()` calls run concurrently without interleaving.
|
|
83
|
+
- **`src/tls-side-channel.ts`** — Node-side wrapper. `TlsSideChannel.spawn()` lazily starts the daemon, manages the JSON-line protocol, parses Set-Cookie headers into structured cookies, cleans up on `close()`. The wrapper is also exported standalone for callers that want to use it without a `BlackTip` instance.
|
|
84
|
+
- **`docs/tls-side-channel.md`** — usage guide, build instructions (Go is the only build dep), validated TLS fingerprint (`JA4: t13d1516h2_8daaf6152771_d8a2da3f94cd`, GREASE first cipher, Chrome H2 frame order), and the four scenarios where the side-channel adds value over the browser alone.
|
|
85
|
+
- **4 new integration tests** in `tests/tls-side-channel.integration.test.ts` covering JA4/GREASE/H2 fingerprint, cookie parsing, concurrent requests via per-request IDs, and post-close rejection. Tests skip cleanly if the Go daemon binary isn't built.
|
|
86
|
+
- **End-to-end validated against OpenTable**: side-channel fetch returns 403 with three Akamai bot manager session cookies (`bm_ss`, `bm_s`, `bm_so`) — exactly the place a real browser would be after one request — and the browser session carries those cookies into the subsequent navigation.
|
|
87
|
+
|
|
88
|
+
### Added — calibration validated against real CMU dataset
|
|
89
|
+
|
|
90
|
+
- **Real CMU Keystroke Dynamics validation.** The Tier 2 calibration parsers shipped earlier in the v0.3.0 cycle have now been validated end-to-end against the real CMU dataset (Killourhy & Maxion 2009 — 51 subjects × 8 sessions × 50 reps = 20,400 phrases of `.tie5Roanl`). Deterministic 80/20 subject split: train on 40 subjects → fit `TypingFit` → KS-distance compare against the 11 held-out subjects.
|
|
91
|
+
- **Result: calibrated profile beats canonical `HUMAN_PROFILE` by 53% on hold time** (KS distance 0.4297 → 0.2018) and **13.7% on flight time** (KS 0.4811 → 0.4152) on the held-out set. This is the first time BlackTip's behavioral pipeline has been validated end-to-end against a real public dataset; up through v0.2.0 the parameters were sane defaults, v0.3.0 makes them empirically grounded.
|
|
92
|
+
- **`scripts/fit-cmu-keystroke.mjs`** — reproducible validation script. Run with `node scripts/fit-cmu-keystroke.mjs` after downloading the CMU CSV to `data/cmu-keystroke/`. Writes the fitted `CalibratedProfile` to `data/cmu-keystroke/calibrated-profile.json` for users to load via `new BlackTip({ behaviorProfile: calibrated.profileConfig })`.
|
|
93
|
+
- **`docs/calibration-validation.md`** — methodology, fitted parameters, KS-distance table, what the result proves and what it does NOT prove, future calibration sources (Balabit, GREYC-NISLAB, Buffalo Free-Text).
|
|
94
|
+
- **CMU parser fixes**: the original parser used bare characters as CSV column labels but the CMU CSV uses `period` for `.`, `five` for `5`, and `Shift.r` for the capital `R` (which requires a Shift modifier in `.tie5Roanl`). Fixed and the synthetic-fixture tests updated to match.
|
|
95
|
+
|
|
96
|
+
### Added — diagnostics fixes
|
|
97
|
+
|
|
98
|
+
- **Cookie detection now reads via the BlackTip cookies API instead of `document.cookie`.** The earlier implementation missed httpOnly cookies — and `cf_clearance`, `__cf_bm`, `_abck`, `datadome`, `bm_sz` are all httpOnly. This caused false negatives where a target was actually protected and BlackTip was passing it but the diagnostic reported `vendorSignals: []`. Fixed: `testAgainstAntiBot` now lists Cloudflare cookie signals on chatgpt.com, vinted.com, crunchbase.com (cf_clearance present, proving each had served and BlackTip had passed a managed challenge silently).
|
|
99
|
+
- **More commercial detector targets validated**: ChatGPT (Cloudflare with cf_clearance present), Canada Goose, Hyatt, Footlocker (no live Kasada cookies on homepages — they may arm only on cart/checkout). Documented in the updated `docs/anti-bot-validation.md`.
|
|
100
|
+
|
|
101
|
+
### Test suite
|
|
102
|
+
|
|
103
|
+
- **78 unit tests passing (was 63), zero regressions.** Plus 4 new TLS integration tests (skip if daemon not built), 2 new diagnostics integration tests, 15 new behavioral parser tests.
|
|
104
|
+
|
|
105
|
+
## [0.2.0] — 2026-04-10
|
|
106
|
+
|
|
107
|
+
### Defeating Akamai Bot Manager — the L016 fix
|
|
108
|
+
|
|
109
|
+
The headline change in 0.2.0 is fixing the User-Agent / Sec-Ch-Ua consistency bug that had been silently undermining BlackTip against top-tier commercial detectors since 0.1.0. **OpenTable's Akamai Bot Manager went from blocking us at the edge to letting us into the booking flow on the very next request after the fix landed.** Same machine, same network, same IP.
|
|
110
|
+
|
|
111
|
+
See `docs/akamai-bypass.md` for the full plan, methodology, and status against each Akamai detection layer.
|
|
112
|
+
|
|
113
|
+
### Fixed
|
|
114
|
+
|
|
115
|
+
- **L016 — User-Agent / Sec-Ch-Ua consistency.** `browser-core.ts` was setting `userAgent` at the Playwright context level via `newContext({userAgent: ...})`. Playwright's `userAgent` option overrides the `User-Agent` HTTP header but does NOT update the `Sec-Ch-Ua` / `Sec-Ch-Ua-Mobile` / `Sec-Ch-Ua-Platform` client hint headers — those come from the actual Chromium binary version. The result was BlackTip broadcasting `User-Agent: Chrome/125` and `Sec-Ch-Ua: "Google Chrome";v="146"` simultaneously, an inconsistency real Chrome NEVER produces and that Akamai Bot Manager catches as a textbook spoofing tell. Fix: removed the `userAgent` context override entirely; real Chrome's natural User-Agent comes through and matches Sec-Ch-Ua.
|
|
116
|
+
- This bug had been silently invalidating BlackTip's stealth against high-tier detectors. CreepJS / bot.sannysoft / browserleaks / fingerprint.com don't cross-check UA against client hints, so they didn't catch it. Akamai / DataDome / PerimeterX do, and they did.
|
|
117
|
+
- **Side effect:** cross-platform UA spoofing (declaring a `desktop-macos` profile while running on Linux) is no longer supported by default. v0.3.0 will reintroduce it via `setExtraHTTPHeaders` for callers who need it.
|
|
118
|
+
|
|
119
|
+
### Added
|
|
120
|
+
|
|
121
|
+
- **`bt.captureFingerprint()`** — captures TLS, HTTP/2, and HTTP header fingerprint via `tls.peet.ws/api/all` and `httpbin.org/headers`. Returns a structured `FingerprintSnapshot` with the critical `headers.uaConsistent` flag for verifying L016 is fixed in any given install.
|
|
122
|
+
- **`bt.checkIpReputation()`** — queries the active session's egress IP via `ipinfo.io`, scores it against known datacenter / residential ASN patterns (Amazon, Google Cloud, Azure, OVH, DigitalOcean, Hetzner, Linode, Vultr — and major residential ISPs), returns an `IpReputationResult` with `isDatacenter` / `isResidential` flags and free-form notes.
|
|
123
|
+
- **`bt.testAgainstAkamai(url)`** — visits an Akamai-protected URL and reports the result with diagnosis. Recognizes the Akamai Access Denied page format, extracts the reference number, and returns a suggested next step. Use as a regression check in CI or interactively when troubleshooting.
|
|
124
|
+
- **`bt.warmSession({sites?, dwellMsRange?})`** — visits a sequence of "normal" sites with realistic dwell times and small scrolls before the target navigation. Accumulates cookies, populates History, and triggers natural behavioral signals so the target sees a session that already looks human.
|
|
125
|
+
- **`BlackTipConfig.userDataDir`** — persistent Chrome user data directory. When set, BlackTip uses `chromium.launchPersistentContext()` instead of the default fresh context, so cookies / localStorage / history / visited sites accumulate across runs. Critical for sites with "first request from unknown session" challenges.
|
|
126
|
+
- **`docs/akamai-bypass.md`** — comprehensive 7-phase plan for defeating Akamai Bot Manager, with current passing/failing matrix per detection layer, validated targets, recipes, and a troubleshooting checklist for users who hit blocks.
|
|
127
|
+
- **10 new diagnostics integration tests** in `tests/v04-diagnostics.integration.test.ts`. The most important is the L016 consistency check, which serves as a regression guard so we never reintroduce the UA / Sec-Ch-Ua mismatch.
|
|
128
|
+
|
|
129
|
+
### Validated against
|
|
130
|
+
|
|
131
|
+
- **OpenTable** (Akamai Bot Manager): blocked at every URL in 0.1.0 with Access Denied references; passing as of 0.2.0. Confirmed against the deep-link booking page for Gjelina (Venice).
|
|
132
|
+
- All 0.1.0 detector targets continue to pass (bot.sannysoft, CreepJS, tls.peet.ws, browserleaks×4, fingerprint.com, pixelscan, browserscan, nowsecure.nl).
|
|
133
|
+
|
|
134
|
+
### Test suite
|
|
135
|
+
|
|
136
|
+
- **162 → 172 tests passing**, zero regressions.
|
|
137
|
+
|
|
138
|
+
### What's deferred to 0.3.0+
|
|
139
|
+
|
|
140
|
+
- TLS-rewriting proxy (`bogdanfinn/tls-client` integration) for cross-platform UA spoofing and Chrome version impersonation
|
|
141
|
+
- Akamai sensor data analysis and targeted JS-level patches for sites that probe deeper than headers
|
|
142
|
+
- Tier 2 behavioral calibration against real public datasets (Balabit, CMU Keystroke, GREYC)
|
|
143
|
+
|
|
144
|
+
## [0.1.0] — 2026-04-10
|
|
145
|
+
|
|
146
|
+
### First public release
|
|
147
|
+
|
|
148
|
+
BlackTip is a stealth browser instrument for AI agents. Real Chrome via `patchright` with CDP-level stealth patches, human-calibrated behavioral simulation, and an agent-friendly TCP serve protocol with bundled JSON responses. Passes every free fingerprint detector tested and known public Cloudflare bot-fight test targets.
|
|
149
|
+
|
|
150
|
+
### Added — core architecture
|
|
151
|
+
|
|
152
|
+
- **Real Chrome via `channel: 'chrome'`** — uses the installed Chrome Stable binary for an authentic TLS ClientHello with rotating GREASE values and a native HTTP/2 frame order. No TLS proxy required for most use cases.
|
|
153
|
+
- **`patchright`-based CDP stealth** — drop-in Playwright replacement with patches that neutralize `Runtime.Enable` detection, automation string artifacts, and Error-stack hooks that vanilla Playwright leaks.
|
|
154
|
+
- **Device profiles** — `desktop-windows`, `desktop-macos`, `desktop-linux` with matching user agents, hardware concurrency, plugins, fonts, and WebGL vendor/renderer strings.
|
|
155
|
+
- **Behavioral engine** — Bézier mouse paths with Fitts' Law movement time, digraph-aware typing with realistic typo-and-correction patterns, scroll deceleration curves, normal-distribution sampling via Box-Muller, and reading pause estimation tied to the profile's WPM range.
|
|
156
|
+
- **Retry engine** — six-strategy cascade (`standard`, `wait`, `reload`, `altSelector`, `scroll`, `clearOverlays`) with event emission on each retry.
|
|
157
|
+
- **Seeded noise layers** — canvas and audio fingerprints receive profile-seeded noise via Mulberry32 PRNG so they're stable cross-session but unique per profile, matching how real hardware variance looks.
|
|
158
|
+
|
|
159
|
+
### Added — agent primitives
|
|
160
|
+
|
|
161
|
+
- **`bt.waitForStable({networkIdleMs, domIdleMs, maxMs})`** — waits until no network requests and no DOM mutations for a window. Replaces fixed `setTimeout` sleeps with a real page-settled signal.
|
|
162
|
+
- **`bt.waitForText(text, {timeout})`** — polls body `innerText` for a target string. For server-rendered confirmations, OCR completion, and async content.
|
|
163
|
+
- **`bt.inspect(selector)`** — returns `{exists, visible, tagName, text, attributes, boundingBox}` in one call. Replaces multiple hand-written `executeJS` queries for diagnostics.
|
|
164
|
+
- **`bt.listOptions(selectorOrBaseId)`** — enumerates Angular/React-style custom dropdown options via the `{baseId}_option-{n}` pattern.
|
|
165
|
+
- **`bt.networkSince(ms, pattern?)`** and **`bt.didRequestFireSince(pattern, ms)`** — filtered Performance API resource entries and a boolean convenience for "did my submit actually reach the server?" diagnostics. Critical for avoiding burned retries on lockout-protected forms.
|
|
166
|
+
- **`bt.dismissOverlays()`** — proactively hides fixed/sticky overlays matching known patterns (Intercom, Drift, Zendesk, OneTrust, Medallia, cookie banners).
|
|
167
|
+
- **`bt.pauseForInput({prompt, validate?, timeoutMs?})`** — first-class MFA / user-in-the-loop support. Emits a `paused` frame to the client via the serve protocol; resumes when the client sends `RESUME <id>\n<value>`.
|
|
168
|
+
- **`bt.findInShadowDom(cssSelector, {timeout?})`** — recursive walker over open shadow roots. Supports modern component libraries (Lit, Stencil, Material Web Components).
|
|
169
|
+
- **`bt.download(selector, {saveTo})`** — click-to-download with returned `{path, size, suggestedFilename, url}` metadata.
|
|
170
|
+
|
|
171
|
+
### Added — click robustness
|
|
172
|
+
|
|
173
|
+
- **Live bounding-box re-read before `mouse.click()`** — re-captures the target's current box immediately before the click. If the element moved more than ~5 pixels during the mouse-movement phase (DOM reflow, async scripts, layout shifts), performs a short correction move so the click lands on the right place. Fixes the L011 coordinate-reflow issue.
|
|
174
|
+
- **Pre-click hit verification** — uses `document.elementFromPoint` to verify the click coordinates actually land on the intended interactive element. If an overlay is covering the target, dismisses it and falls back to `locator.click({force: true})`.
|
|
175
|
+
- **Auto-importance detection on click/clickText/clickRole** — buttons whose visible text matches `Submit`, `Pay`, `Confirm`, `Place Order`, `Delete`, `Remove`, `Checkout`, `Purchase`, etc. automatically receive `importance: 'high'` which triggers longer pre-action hesitation (2–3× base pause plus a hesitation spike). Matches the long-tail distribution behavioral biometrics systems expect on consequential actions. Callers can override with explicit `importance`.
|
|
176
|
+
- **`BlackTipFrame.type()` iframe hardening** — ported the `Control+A + Backspace + keyboard.type + inputValue` verification pattern from the main `type()` method to the iframe variant. Ready for Stripe Elements, Braintree Hosted Fields, and other framework-driven iframes.
|
|
177
|
+
|
|
178
|
+
### Added — serve mode and CLI
|
|
179
|
+
|
|
180
|
+
- **Bundled JSON responses** — every `send` command returns `{ok, result, url, title, screenshotPath, screenshotB64, screenshotBytes, durationMs, error?}` in one frame. Cuts round-trips by ~60% for linear flows.
|
|
181
|
+
- **Batched commands** — `BATCH\n<json array>` runs multiple commands sequentially and returns an array of bundles, stopping on first failure.
|
|
182
|
+
- **CLI flags** — `--file <path>`, `--stdin`, `--pretty`, `--port` eliminate shell-escape hell for complex JavaScript commands.
|
|
183
|
+
- **`npx blacktip` subcommands** — `serve`, `send`, `batch`, `resume`, `pending`, `exec` with per-command help.
|
|
184
|
+
|
|
185
|
+
### Added — infrastructure
|
|
186
|
+
|
|
187
|
+
- **`ProxyPool`** — round-robin / random / least-used strategies, per-domain ban tracking with decay window, reputation snapshot for observability, URL formatters for BrightData / Oxylabs / Smartproxy.
|
|
188
|
+
- **`SnapshotManager`** — `capture()` and `restore()` for session migration across proxies or machines. Serializes cookies, localStorage, sessionStorage, and active URL into a JSON blob.
|
|
189
|
+
- **Observability** — `attachObservability(bt, exporters)` wires BlackTip's EventEmitter events into a `StructuredEvent` shape compatible with OpenTelemetry attribute data model. Ships `JsonlFileExporter` and `ConsoleExporter` as reference implementations. No OTel SDK dependency.
|
|
190
|
+
- **Behavioral calibration scaffold** — `src/behavioral/calibration.ts` with `fitDistribution`, `fitFittsLaw` (OLS), `fitMouseDynamics`, `fitTypingDynamics`, `fitFromSamples`, `deriveProfileConfig`. Ready to ingest real mouse-dynamics and keystroke-dynamics datasets (Balabit Mouse Dynamics Challenge, CMU Keystroke Dynamics, GREYC-NISLAB) via user-supplied parser wrappers.
|
|
191
|
+
|
|
192
|
+
### Detector results
|
|
193
|
+
|
|
194
|
+
Captured against free public detectors as of the release:
|
|
195
|
+
|
|
196
|
+
- **bot.sannysoft.com** — 31 passed / 0 failed across 57 rows
|
|
197
|
+
- **tls.peet.ws** — JA4 `t13d1516h2_...`, first cipher rotates `TLS_GREASE (0x...)` matching real Chrome 122/124
|
|
198
|
+
- **CreepJS** — Grade A, 0% headless, 0% stealth, 31% like-headless (unavoidable desktop false-positive)
|
|
199
|
+
- **browserleaks.com/canvas** — signature shared with 100 of 298,939 browsers (blending in, not unique)
|
|
200
|
+
- **browserleaks.com/webgl** — real GPU via ANGLE, not SwiftShader
|
|
201
|
+
- **browserleaks.com/webrtc** — no RFC1918 local IP leak
|
|
202
|
+
- **browserleaks.com/javascript** — Chrome 122 on Win32, en-US, America/New_York, 1920×1080 all consistent
|
|
203
|
+
- **fingerprint.com/demo** — passes
|
|
204
|
+
- **pixelscan.net** — passes
|
|
205
|
+
- **browserscan.net** — identified as "Chrome on Windows 10", not flagged
|
|
206
|
+
|
|
207
|
+
Real-target validation:
|
|
208
|
+
|
|
209
|
+
- **nowsecure.nl** (Cloudflare bot-fight test, nodriver author's public benchmark) — passes without challenge
|
|
210
|
+
- **antoinevastel.com/bots** (ex-DataDome VP of Research) — loads without block
|
|
211
|
+
- **Anthem.com** (Okta MFA, Angular SPA, real medical claim submission) — full end-to-end flow successful
|
|
212
|
+
|
|
213
|
+
### Notes
|
|
214
|
+
|
|
215
|
+
- Always runs headful. `headless: true` in config is silently ignored — there is no real-headless path that passes serious detectors.
|
|
216
|
+
- Real Chrome must be installed on the host for the preferred `channel: 'chrome'` path. patchright's bundled Chromium is the fallback.
|
|
217
|
+
- Scoped-name fix: the initial planned unscoped name `blacktip` was blocked by npm's anti-typosquatting policy (too similar to the pre-existing `black-tip` package). Released as `@rester159/blacktip` instead.
|
|
218
|
+
|
|
219
|
+
[Unreleased]: https://github.com/rester159/blacktip/compare/v0.5.0...HEAD
|
|
220
|
+
[0.4.0]: https://github.com/rester159/blacktip/compare/v0.2.0...v0.4.0
|
|
221
|
+
[0.2.0]: https://github.com/rester159/blacktip/compare/v0.1.0...v0.2.0
|
|
222
|
+
[0.1.0]: https://github.com/rester159/blacktip/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -49,6 +49,22 @@ BlackTip's architecture:
|
|
|
49
49
|
- nowsecure.nl (Cloudflare bot-fight test, nodriver author's public benchmark) — passes without challenge
|
|
50
50
|
- antoinevastel.com/bots (ex-DataDome VP of Research) — loads without block
|
|
51
51
|
- Anthem.com (Okta MFA, Angular SPA, real insurance claim submission) — end-to-end flow successful
|
|
52
|
+
- Walmart.com (Akamai + PerimeterX simultaneously) — passes both layers
|
|
53
|
+
- BestBuy.com (Akamai) — passes
|
|
54
|
+
- Vinted.com (DataDome) — real catalog renders, datadome cookie present
|
|
55
|
+
- Crunchbase.com (Cloudflare) — passes, cf_clearance issued silently
|
|
56
|
+
- ChatGPT.com (Cloudflare) — passes, cf_clearance issued silently
|
|
57
|
+
- Twitch.tv (Kasada) — passes, kasada client script detected on the page
|
|
58
|
+
- Ticketmaster.com — passes
|
|
59
|
+
- OpenTable.com Gjelina deep link (Akamai Bot Manager) — passes; full booking endpoint with real time slots
|
|
60
|
+
|
|
61
|
+
**v0.4.0 also ships:**
|
|
62
|
+
- **`IdentityPool`** — long-running session and identity rotation. An identity is the union of cookies, localStorage, proxy, device profile, behavior profile, locale, and timezone. Persists to a JSON file so identities survive restarts. Per-domain burn list — an identity blocked on one site is still eligible for others. Composes `SnapshotManager` and `ProxyPool` with a feedback loop that auto-bans burned identities' proxies in the pool. See **[docs/identity-pool.md](docs/identity-pool.md)**.
|
|
63
|
+
- **`BlackTipConfig.requireResidentialIp`** — `'throw'` / `'warn'` / `false`. Runs `bt.checkIpReputation()` on launch and refuses (or warns) if the egress IP is on a known datacenter ASN. The launch-time defensive companion to `IdentityPool`'s outbound proxy selection.
|
|
64
|
+
- **Go-based TLS side-channel daemon (`bt.fetchWithTls`)** that performs HTTP requests with a real Chrome TLS ClientHello, H2 frame settings, and frame order via `bogdanfinn/tls-client`. Use to make gating requests the browser can't make through itself, then inject the resulting cookies into the browser session before navigating. JA4 `t13d1516h2_8daaf6152771_d8a2da3f94cd`, GREASE first cipher, exact Chrome H2 fingerprint. See **[docs/tls-side-channel.md](docs/tls-side-channel.md)**.
|
|
65
|
+
- **Behavioral profile calibrated against the real CMU Keystroke Dynamics dataset.** **53% closer to real human hold-time distribution** than the canonical defaults on a held-out subject set (51 subjects, 80/20 split). See **[docs/calibration-validation.md](docs/calibration-validation.md)** for methodology and reproduction.
|
|
66
|
+
|
|
67
|
+
See **[docs/anti-bot-validation.md](docs/anti-bot-validation.md)** for the live multi-vendor scoreboard with vendor-signal verification (proves each target is actually protected, not a false negative on an unprotected URL).
|
|
52
68
|
|
|
53
69
|
---
|
|
54
70
|
|
|
@@ -162,6 +178,15 @@ See `AGENTS.md` for the full agent-facing reference, including the decision tree
|
|
|
162
178
|
| `bt.captureFingerprint()` | TLS / HTTP2 / header snapshot via tls.peet.ws + httpbin (v0.2.0) |
|
|
163
179
|
| `bt.checkIpReputation()` | Egress IP, ASN, datacenter/residential heuristic (v0.2.0) |
|
|
164
180
|
| `bt.testAgainstAkamai(url)` | Akamai-protected target probe with diagnosis (v0.2.0) |
|
|
181
|
+
| `bt.testAgainstAntiBot(url)` | Multi-vendor probe — detects Akamai, DataDome, Cloudflare, PerimeterX, Imperva, Kasada, Arkose, plus vendor signals on a passing page (v0.2.0) |
|
|
182
|
+
| `bt.fetchWithTls(req)` | Perform an HTTP request via a Go-based `bogdanfinn/tls-client` daemon with a real Chrome TLS ClientHello, H2 frame settings, and frame order. Use for first-request edge gating and cross-platform UA spoofing. Requires the daemon binary at `native/tls-client/` (build with `go build .`) (v0.3.0) |
|
|
183
|
+
| `bt.injectTlsCookies(resp, targetUrl?)` | Inject cookies returned by `fetchWithTls()` into the browser session, filtered by target eTLD+1 (v0.3.0) |
|
|
184
|
+
| `bt.solveAkamaiChallenge(url)` | Solve Akamai's sensor challenge in the browser once, return cookies + recommended headers ready for replay via `fetchWithTls()`. ~5x speedup over per-call browser usage on protected APIs. (v0.5.0) |
|
|
185
|
+
| `bt.getTlsRewriterStats()` | Stats for the TLS rewriter when `tlsRewriting: 'all'` is set — intercepted/fulfilled/fell-through counts, WebSocket leaks, average daemon round-trip. (v0.5.0) |
|
|
186
|
+
|
|
187
|
+
Plus `IdentityPool` (v0.4.0) for long-running session and identity rotation across many flows. See **[docs/identity-pool.md](docs/identity-pool.md)**.
|
|
188
|
+
|
|
189
|
+
**`BlackTipConfig.tlsRewriting: 'all'`** (v0.5.0) — when set, every browser request is intercepted via CDP `Fetch.enable` and forwarded through the Go-based `bogdanfinn/tls-client` daemon. The browser never opens an upstream TCP connection — every wire request, including subresources, presents real Chrome TLS via Go. Cross-platform UA spoofing is restored. See **[docs/tls-rewriting.md](docs/tls-rewriting.md)**.
|
|
165
190
|
| `bt.warmSession({sites?, dwellMsRange?})` | Pre-target warm-up — visit normal sites first (v0.2.0) |
|
|
166
191
|
| `bt.serve(port?)` | Start TCP command server |
|
|
167
192
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Akamai Bot Manager sensor challenge solver.
|
|
3
|
+
*
|
|
4
|
+
* The v0.5.0 answer to "I want to call Akamai-protected APIs from a
|
|
5
|
+
* sessionless TLS daemon, but the first request always 403s because
|
|
6
|
+
* Akamai gates everything behind a sensor data POST."
|
|
7
|
+
*
|
|
8
|
+
* Why this isn't pure Go: Akamai's bm.js is heavily obfuscated, the
|
|
9
|
+
* sensor data POST it generates is encrypted with a runtime-derived
|
|
10
|
+
* key that lives inside the obfuscated code, and the obfuscation rotates
|
|
11
|
+
* monthly. A pure-Go reimplementation would be a 1-2 week reverse
|
|
12
|
+
* engineering project and the result would rot in ~6 weeks. Bad ROI.
|
|
13
|
+
*
|
|
14
|
+
* What we do instead: launch a real BlackTip browser, navigate to the
|
|
15
|
+
* URL, let Akamai's bm.js execute naturally (real Chrome runs the JS,
|
|
16
|
+
* generates the sensor payload, POSTs it back), poll for the `_abck`
|
|
17
|
+
* cookie to transition from `~-1~` (unvalidated) to `~0~` (validated),
|
|
18
|
+
* and return the validated cookies. The caller can then inject those
|
|
19
|
+
* cookies into thousands of sessionless TLS-daemon API calls until
|
|
20
|
+
* they expire (Akamai sessions are valid for ~1 hour typically).
|
|
21
|
+
*
|
|
22
|
+
* This is NOT "no browser needed for Akamai." It IS "amortize browser
|
|
23
|
+
* cost across many subsequent API calls instead of paying it per
|
|
24
|
+
* request." For most use cases that's the same thing — you pay one
|
|
25
|
+
* browser session per hour and run hundreds of API calls in between.
|
|
26
|
+
*/
|
|
27
|
+
import type { BlackTip } from './blacktip.js';
|
|
28
|
+
export interface AkamaiChallengeResult {
|
|
29
|
+
/**
|
|
30
|
+
* Whether the session is usable. True when EITHER:
|
|
31
|
+
* - `_abck` cookie reached the validated state (`~0~`), OR
|
|
32
|
+
* - The page rendered successfully without an Akamai block.
|
|
33
|
+
*
|
|
34
|
+
* Akamai's sensor validation is only enforced when other signals
|
|
35
|
+
* (TLS, IP, behavior) look suspicious. For real-Chrome sessions on
|
|
36
|
+
* residential connections, Akamai often admits the request without
|
|
37
|
+
* ever requiring the JS-layer sensor POST. In those cases `abckState`
|
|
38
|
+
* stays at `-1` but the page works fine — that's still a successful
|
|
39
|
+
* solve from the caller's perspective.
|
|
40
|
+
*/
|
|
41
|
+
validated: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The actual `_abck` validation state at the end of the wait window:
|
|
44
|
+
* - `0` → sensor data validated as human (gold standard)
|
|
45
|
+
* - `-1` → sensor not enforced (page admitted without it)
|
|
46
|
+
* - `1`+ → sensor data flagged as bot
|
|
47
|
+
* - `null` → no `_abck` cookie set (target may not be Akamai-protected)
|
|
48
|
+
*/
|
|
49
|
+
abckState: -1 | 0 | 1 | null;
|
|
50
|
+
/** The full `_abck` cookie value at the end of the wait window. */
|
|
51
|
+
abckValue: string | null;
|
|
52
|
+
/**
|
|
53
|
+
* Whether the rendered page looks like an Akamai block page (title
|
|
54
|
+
* `Access Denied`, body matches the standard error template).
|
|
55
|
+
*/
|
|
56
|
+
blocked: boolean;
|
|
57
|
+
/** All Akamai-related cookies on the target session, ready to inject. */
|
|
58
|
+
cookies: Array<{
|
|
59
|
+
name: string;
|
|
60
|
+
value: string;
|
|
61
|
+
domain: string;
|
|
62
|
+
path: string;
|
|
63
|
+
}>;
|
|
64
|
+
/** Final URL after any redirects. */
|
|
65
|
+
finalUrl: string;
|
|
66
|
+
/** Page title (useful for verifying we're not on an Access Denied page). */
|
|
67
|
+
title: string;
|
|
68
|
+
/** How long the solve took, ms. */
|
|
69
|
+
durationMs: number;
|
|
70
|
+
/** Free-form diagnostic notes. */
|
|
71
|
+
notes: string[];
|
|
72
|
+
/**
|
|
73
|
+
* Pre-built header set ready to pass to `bt.fetchWithTls({ url, headers })`
|
|
74
|
+
* for replay calls. Includes the Cookie header (joined from `cookies`)
|
|
75
|
+
* plus the Sec-Ch-Ua / Sec-Fetch-* / Accept-Language headers Akamai
|
|
76
|
+
* binds the session to. **Replays without these headers will 403** even
|
|
77
|
+
* with valid cookies — Akamai validates the full request shape, not
|
|
78
|
+
* just the cookie jar.
|
|
79
|
+
*
|
|
80
|
+
* Empirically validated against OpenTable: 5 consecutive replays via
|
|
81
|
+
* the TLS daemon all returned 200 with real content. Replay cost is
|
|
82
|
+
* ~600ms per call vs ~4s per browser launch.
|
|
83
|
+
*/
|
|
84
|
+
recommendedHeaders: Record<string, string>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse the Akamai sensor validation state from a `_abck` cookie value.
|
|
88
|
+
* Akamai encodes the state in the second `~`-delimited field:
|
|
89
|
+
* - `-1` means "sensor data not yet submitted/validated" (the value
|
|
90
|
+
* Akamai sets on the very first response).
|
|
91
|
+
* - `0` means "sensor data submitted and validated as human."
|
|
92
|
+
* - `1`+ means "sensor data submitted but flagged as bot."
|
|
93
|
+
*
|
|
94
|
+
* The validated state is what unlocks the rest of the protected paths.
|
|
95
|
+
*/
|
|
96
|
+
export declare function parseAbckState(abckValue: string | null): -1 | 0 | 1 | null;
|
|
97
|
+
/**
|
|
98
|
+
* Drive a BlackTip session through Akamai's sensor challenge for `url`,
|
|
99
|
+
* waiting until `_abck` reaches a validated state.
|
|
100
|
+
*
|
|
101
|
+
* The caller passes a launched BlackTip instance — the function does NOT
|
|
102
|
+
* launch its own. This is so the caller controls the surrounding context
|
|
103
|
+
* (TLS rewriting, IdentityPool identity, persistent profile, etc.).
|
|
104
|
+
*
|
|
105
|
+
* After this returns with `validated: true`, you can:
|
|
106
|
+
* - Inject `result.cookies` into another BlackTip session via `bt.setCookies()`
|
|
107
|
+
* - Inject them into a TLS daemon flow by setting them on the `Cookie:`
|
|
108
|
+
* header of subsequent `bt.fetchWithTls()` calls
|
|
109
|
+
* - Persist them in an IdentityPool snapshot
|
|
110
|
+
*
|
|
111
|
+
* Polls every 250ms with a default 15-second wait window. Most Akamai
|
|
112
|
+
* targets validate within 2-5 seconds; the wait is generous enough for
|
|
113
|
+
* slow targets but bounded so we don't hang on a permanently-blocked URL.
|
|
114
|
+
*/
|
|
115
|
+
export declare function solveAkamaiChallenge(bt: BlackTip, url: string, options?: {
|
|
116
|
+
/** Maximum time to wait for `_abck` to validate. Default 15s. */
|
|
117
|
+
timeoutMs?: number;
|
|
118
|
+
/** Poll interval for the cookie state. Default 250ms. */
|
|
119
|
+
pollIntervalMs?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Optional human-like dwell after navigation finishes but before we
|
|
122
|
+
* start polling. Akamai's sensor data is more convincing if there's
|
|
123
|
+
* actual mouse movement / scrolling on the page. Default 1500ms; set
|
|
124
|
+
* to 0 to skip.
|
|
125
|
+
*/
|
|
126
|
+
dwellMsBeforePolling?: number;
|
|
127
|
+
}): Promise<AkamaiChallengeResult>;
|
|
128
|
+
//# sourceMappingURL=akamai-sensor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"akamai-sensor.d.ts","sourceRoot":"","sources":["../src/akamai-sensor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;;OAWG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAC7B,mEAAmE;IACnE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9E,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;;;;;;;;;OAWG;IACH,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5C;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAU1E;AAcD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,QAAQ,EACZ,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;IACP,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC1B,GACL,OAAO,CAAC,qBAAqB,CAAC,CA0HhC"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Akamai Bot Manager sensor challenge solver.
|
|
3
|
+
*
|
|
4
|
+
* The v0.5.0 answer to "I want to call Akamai-protected APIs from a
|
|
5
|
+
* sessionless TLS daemon, but the first request always 403s because
|
|
6
|
+
* Akamai gates everything behind a sensor data POST."
|
|
7
|
+
*
|
|
8
|
+
* Why this isn't pure Go: Akamai's bm.js is heavily obfuscated, the
|
|
9
|
+
* sensor data POST it generates is encrypted with a runtime-derived
|
|
10
|
+
* key that lives inside the obfuscated code, and the obfuscation rotates
|
|
11
|
+
* monthly. A pure-Go reimplementation would be a 1-2 week reverse
|
|
12
|
+
* engineering project and the result would rot in ~6 weeks. Bad ROI.
|
|
13
|
+
*
|
|
14
|
+
* What we do instead: launch a real BlackTip browser, navigate to the
|
|
15
|
+
* URL, let Akamai's bm.js execute naturally (real Chrome runs the JS,
|
|
16
|
+
* generates the sensor payload, POSTs it back), poll for the `_abck`
|
|
17
|
+
* cookie to transition from `~-1~` (unvalidated) to `~0~` (validated),
|
|
18
|
+
* and return the validated cookies. The caller can then inject those
|
|
19
|
+
* cookies into thousands of sessionless TLS-daemon API calls until
|
|
20
|
+
* they expire (Akamai sessions are valid for ~1 hour typically).
|
|
21
|
+
*
|
|
22
|
+
* This is NOT "no browser needed for Akamai." It IS "amortize browser
|
|
23
|
+
* cost across many subsequent API calls instead of paying it per
|
|
24
|
+
* request." For most use cases that's the same thing — you pay one
|
|
25
|
+
* browser session per hour and run hundreds of API calls in between.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Parse the Akamai sensor validation state from a `_abck` cookie value.
|
|
29
|
+
* Akamai encodes the state in the second `~`-delimited field:
|
|
30
|
+
* - `-1` means "sensor data not yet submitted/validated" (the value
|
|
31
|
+
* Akamai sets on the very first response).
|
|
32
|
+
* - `0` means "sensor data submitted and validated as human."
|
|
33
|
+
* - `1`+ means "sensor data submitted but flagged as bot."
|
|
34
|
+
*
|
|
35
|
+
* The validated state is what unlocks the rest of the protected paths.
|
|
36
|
+
*/
|
|
37
|
+
export function parseAbckState(abckValue) {
|
|
38
|
+
if (!abckValue)
|
|
39
|
+
return null;
|
|
40
|
+
const parts = abckValue.split('~');
|
|
41
|
+
if (parts.length < 2)
|
|
42
|
+
return null;
|
|
43
|
+
const stateStr = parts[1] ?? '';
|
|
44
|
+
const n = parseInt(stateStr, 10);
|
|
45
|
+
if (n === -1)
|
|
46
|
+
return -1;
|
|
47
|
+
if (n === 0)
|
|
48
|
+
return 0;
|
|
49
|
+
if (n >= 1)
|
|
50
|
+
return 1;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/** Cookie names we treat as "Akamai session state" for the result bundle. */
|
|
54
|
+
const AKAMAI_COOKIE_NAMES = new Set([
|
|
55
|
+
'_abck',
|
|
56
|
+
'bm_sz',
|
|
57
|
+
'bm_sv',
|
|
58
|
+
'ak_bmsc',
|
|
59
|
+
'bm_mi',
|
|
60
|
+
'bm_so',
|
|
61
|
+
'bm_s',
|
|
62
|
+
'bm_ss',
|
|
63
|
+
]);
|
|
64
|
+
/**
|
|
65
|
+
* Drive a BlackTip session through Akamai's sensor challenge for `url`,
|
|
66
|
+
* waiting until `_abck` reaches a validated state.
|
|
67
|
+
*
|
|
68
|
+
* The caller passes a launched BlackTip instance — the function does NOT
|
|
69
|
+
* launch its own. This is so the caller controls the surrounding context
|
|
70
|
+
* (TLS rewriting, IdentityPool identity, persistent profile, etc.).
|
|
71
|
+
*
|
|
72
|
+
* After this returns with `validated: true`, you can:
|
|
73
|
+
* - Inject `result.cookies` into another BlackTip session via `bt.setCookies()`
|
|
74
|
+
* - Inject them into a TLS daemon flow by setting them on the `Cookie:`
|
|
75
|
+
* header of subsequent `bt.fetchWithTls()` calls
|
|
76
|
+
* - Persist them in an IdentityPool snapshot
|
|
77
|
+
*
|
|
78
|
+
* Polls every 250ms with a default 15-second wait window. Most Akamai
|
|
79
|
+
* targets validate within 2-5 seconds; the wait is generous enough for
|
|
80
|
+
* slow targets but bounded so we don't hang on a permanently-blocked URL.
|
|
81
|
+
*/
|
|
82
|
+
export async function solveAkamaiChallenge(bt, url, options = {}) {
|
|
83
|
+
const start = Date.now();
|
|
84
|
+
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
85
|
+
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
86
|
+
const dwellMsBeforePolling = options.dwellMsBeforePolling ?? 1500;
|
|
87
|
+
const notes = [];
|
|
88
|
+
// 1. Navigate to the target. Akamai sets `_abck` on the response and
|
|
89
|
+
// schedules its bm.js to run.
|
|
90
|
+
await bt.navigate(url);
|
|
91
|
+
// 2. Brief dwell so Chrome can run bm.js and POST the sensor data.
|
|
92
|
+
// On most sites, the sensor POST happens within 100-500ms of DOM ready.
|
|
93
|
+
if (dwellMsBeforePolling > 0) {
|
|
94
|
+
await new Promise((r) => setTimeout(r, dwellMsBeforePolling));
|
|
95
|
+
}
|
|
96
|
+
// 3. Poll the cookie jar until `_abck` reaches a definitive state
|
|
97
|
+
// (validated or flagged), or the timeout window expires.
|
|
98
|
+
let lastAbck = null;
|
|
99
|
+
let lastState = null;
|
|
100
|
+
const deadline = start + timeoutMs;
|
|
101
|
+
while (Date.now() < deadline) {
|
|
102
|
+
const allCookies = await bt.cookies();
|
|
103
|
+
const abck = allCookies.find((c) => c.name === '_abck');
|
|
104
|
+
lastAbck = abck?.value ?? null;
|
|
105
|
+
lastState = parseAbckState(lastAbck);
|
|
106
|
+
if (lastState === 0) {
|
|
107
|
+
notes.push('_abck reached validated state (0) — sensor POST accepted');
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
if (lastState === 1) {
|
|
111
|
+
notes.push('_abck reached flagged state (1) — sensor POST was rejected as bot');
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
115
|
+
}
|
|
116
|
+
// 4. Collect the full Akamai cookie set for the caller.
|
|
117
|
+
const allCookies = await bt.cookies();
|
|
118
|
+
const akamaiCookies = allCookies
|
|
119
|
+
.filter((c) => AKAMAI_COOKIE_NAMES.has(c.name))
|
|
120
|
+
.map((c) => ({ name: c.name, value: c.value, domain: c.domain, path: c.path }));
|
|
121
|
+
// 5. Capture the final page state for diagnosis. We check for the
|
|
122
|
+
// Akamai Access Denied page format here so we can distinguish
|
|
123
|
+
// "page rendered fine, sensor not enforced" from "page blocked".
|
|
124
|
+
const pageState = (await bt.executeJS(`(() => ({
|
|
125
|
+
url: location.href,
|
|
126
|
+
title: document.title,
|
|
127
|
+
bodyPreview: (document.body ? document.body.innerText : '').slice(0, 600),
|
|
128
|
+
}))()`));
|
|
129
|
+
const blocked = pageState.title === 'Access Denied' ||
|
|
130
|
+
/You don't have permission to access/i.test(pageState.bodyPreview) ||
|
|
131
|
+
/errors\.edgesuite\.net/i.test(pageState.bodyPreview);
|
|
132
|
+
if (blocked) {
|
|
133
|
+
notes.push('Akamai served the Access Denied block page');
|
|
134
|
+
}
|
|
135
|
+
else if (lastState === null) {
|
|
136
|
+
notes.push('_abck cookie was never set — target may not be Akamai-protected');
|
|
137
|
+
}
|
|
138
|
+
else if (lastState === -1) {
|
|
139
|
+
notes.push('_abck stayed at -1 (sensor not enforced). Page rendered successfully — Akamai admitted the request based on TLS/IP/behavior signals without requiring JS sensor validation. Cookies are still usable for the session window.');
|
|
140
|
+
}
|
|
141
|
+
// The session is usable when the page rendered without a block,
|
|
142
|
+
// regardless of whether _abck reached the validated state. Akamai
|
|
143
|
+
// only enforces sensor validation when other signals look bad.
|
|
144
|
+
const validated = !blocked && (lastState === 0 || lastState === -1);
|
|
145
|
+
// 6. Build the recommended replay headers. These are the headers that
|
|
146
|
+
// Akamai validates alongside the cookie jar — without them, replays
|
|
147
|
+
// via the TLS daemon will 403 even with valid cookies. We include
|
|
148
|
+
// every cookie from the session in the Cookie header (not just the
|
|
149
|
+
// Akamai ones), since some sites bind to non-Akamai cookies too.
|
|
150
|
+
const cookieHeader = allCookies
|
|
151
|
+
.filter((c) => {
|
|
152
|
+
const cd = c.domain.replace(/^\./, '');
|
|
153
|
+
try {
|
|
154
|
+
const host = new URL(pageState.url).hostname;
|
|
155
|
+
return host === cd || host.endsWith('.' + cd);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
.map((c) => `${c.name}=${c.value}`)
|
|
162
|
+
.join('; ');
|
|
163
|
+
const recommendedHeaders = {
|
|
164
|
+
'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',
|
|
165
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
166
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
167
|
+
'Sec-Ch-Ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
|
168
|
+
'Sec-Ch-Ua-Mobile': '?0',
|
|
169
|
+
'Sec-Ch-Ua-Platform': '"Windows"',
|
|
170
|
+
'Sec-Fetch-Dest': 'document',
|
|
171
|
+
'Sec-Fetch-Mode': 'navigate',
|
|
172
|
+
'Sec-Fetch-Site': 'none',
|
|
173
|
+
'Sec-Fetch-User': '?1',
|
|
174
|
+
'Upgrade-Insecure-Requests': '1',
|
|
175
|
+
'Cookie': cookieHeader,
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
validated,
|
|
179
|
+
abckState: lastState,
|
|
180
|
+
abckValue: lastAbck,
|
|
181
|
+
blocked,
|
|
182
|
+
cookies: akamaiCookies,
|
|
183
|
+
finalUrl: pageState.url,
|
|
184
|
+
title: pageState.title,
|
|
185
|
+
durationMs: Date.now() - start,
|
|
186
|
+
notes,
|
|
187
|
+
recommendedHeaders,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=akamai-sensor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"akamai-sensor.js","sourceRoot":"","sources":["../src/akamai-sensor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AA0DH;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAC,SAAwB;IACrD,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAChC,MAAM,CAAC,GAAG,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACjC,IAAI,CAAC,KAAK,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC;IACxB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACrB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,6EAA6E;AAC7E,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IAClC,OAAO;IACP,OAAO;IACP,OAAO;IACP,SAAS;IACT,OAAO;IACP,OAAO;IACP,MAAM;IACN,OAAO;CACR,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,EAAY,EACZ,GAAW,EACX,UAYI,EAAE;IAEN,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC;IAC9C,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,GAAG,CAAC;IACrD,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,IAAI,IAAI,CAAC;IAElE,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,qEAAqE;IACrE,iCAAiC;IACjC,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAEvB,mEAAmE;IACnE,2EAA2E;IAC3E,IAAI,oBAAoB,GAAG,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAChE,CAAC;IAED,kEAAkE;IAClE,4DAA4D;IAC5D,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,IAAI,SAAS,GAAsC,IAAI,CAAC;IACxD,MAAM,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAC;IAEnC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC7B,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;QACxD,QAAQ,GAAG,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC;QAC/B,SAAS,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;QAErC,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;YACpB,KAAK,CAAC,IAAI,CAAC,0DAA0D,CAAC,CAAC;YACvE,MAAM;QACR,CAAC;QACD,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;YACpB,KAAK,CAAC,IAAI,CAAC,mEAAmE,CAAC,CAAC;YAChF,MAAM;QACR,CAAC;QAED,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,wDAAwD;IACxD,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC;IACtC,MAAM,aAAa,GAAG,UAAU;SAC7B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;SAC9C,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAElF,kEAAkE;IAClE,iEAAiE;IACjE,oEAAoE;IACpE,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,CAAC,SAAS,CAAC;;;;QAIhC,CAAC,CAAwD,CAAC;IAEhE,MAAM,OAAO,GACX,SAAS,CAAC,KAAK,KAAK,eAAe;QACnC,sCAAsC,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC;QAClE,yBAAyB,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IAExD,IAAI,OAAO,EAAE,CAAC;QACZ,KAAK,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAC;IAC3D,CAAC;SAAM,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,iEAAiE,CAAC,CAAC;IAChF,CAAC;SAAM,IAAI,SAAS,KAAK,CAAC,CAAC,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CACR,8NAA8N,CAC/N,CAAC;IACJ,CAAC;IAED,gEAAgE;IAChE,kEAAkE;IAClE,+DAA+D;IAC/D,MAAM,SAAS,GAAG,CAAC,OAAO,IAAI,CAAC,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC;IAEpE,sEAAsE;IACtE,uEAAuE;IACvE,qEAAqE;IACrE,sEAAsE;IACtE,oEAAoE;IACpE,MAAM,YAAY,GAAG,UAAU;SAC5B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACZ,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACvC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;YAC7C,OAAO,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC,CAAC;SACD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;SAClC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,MAAM,kBAAkB,GAA2B;QACjD,YAAY,EAAE,iHAAiH;QAC/H,QAAQ,EAAE,8HAA8H;QACxI,iBAAiB,EAAE,gBAAgB;QACnC,WAAW,EAAE,mEAAmE;QAChF,kBAAkB,EAAE,IAAI;QACxB,oBAAoB,EAAE,WAAW;QACjC,gBAAgB,EAAE,UAAU;QAC5B,gBAAgB,EAAE,UAAU;QAC5B,gBAAgB,EAAE,MAAM;QACxB,gBAAgB,EAAE,IAAI;QACtB,2BAA2B,EAAE,GAAG;QAChC,QAAQ,EAAE,YAAY;KACvB,CAAC;IAEF,OAAO;QACL,SAAS;QACT,SAAS,EAAE,SAAS;QACpB,SAAS,EAAE,QAAQ;QACnB,OAAO;QACP,OAAO,EAAE,aAAa;QACtB,QAAQ,EAAE,SAAS,CAAC,GAAG;QACvB,KAAK,EAAE,SAAS,CAAC,KAAK;QACtB,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;QAC9B,KAAK;QACL,kBAAkB;KACnB,CAAC;AACJ,CAAC"}
|