@rester159/blacktip 0.1.0 → 0.4.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 +190 -0
- package/README.md +95 -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 +86 -0
- package/dist/blacktip.d.ts.map +1 -1
- package/dist/blacktip.js +193 -0
- package/dist/blacktip.js.map +1 -1
- package/dist/browser-core.d.ts.map +1 -1
- package/dist/browser-core.js +125 -33
- package/dist/browser-core.js.map +1 -1
- package/dist/diagnostics.d.ts +150 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +389 -0
- package/dist/diagnostics.js.map +1 -0
- 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 +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/tls-side-channel.d.ts +82 -0
- package/dist/tls-side-channel.d.ts.map +1 -0
- package/dist/tls-side-channel.js +241 -0
- package/dist/tls-side-channel.js.map +1 -0
- package/dist/types.d.ts +26 -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/anti-bot-validation.md +84 -0
- package/docs/calibration-validation.md +93 -0
- package/docs/identity-pool.md +176 -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,190 @@
|
|
|
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.4.0] — 2026-04-10
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
### Added — IdentityPool for long-running session rotation
|
|
14
|
+
|
|
15
|
+
- **`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).
|
|
16
|
+
- **Rotation policy** — `maxUses` and `maxAgeMs` auto-burn thresholds, plus `preferLeastRecentlyUsed` for fair distribution across identities.
|
|
17
|
+
- **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.
|
|
18
|
+
- **`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.
|
|
19
|
+
- **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.
|
|
20
|
+
- **`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.
|
|
21
|
+
|
|
22
|
+
### Added — IP reputation gate
|
|
23
|
+
|
|
24
|
+
- **`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.
|
|
25
|
+
- 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.
|
|
26
|
+
|
|
27
|
+
### Added — Kasada validated pass
|
|
28
|
+
|
|
29
|
+
- **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.
|
|
30
|
+
- 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).
|
|
31
|
+
|
|
32
|
+
### Added — multi-vendor diagnostics
|
|
33
|
+
|
|
34
|
+
- **`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.
|
|
35
|
+
- **`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.
|
|
36
|
+
- **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.
|
|
37
|
+
|
|
38
|
+
### Added — Tier 2 calibration parsers (v0.3.0 prep)
|
|
39
|
+
|
|
40
|
+
- **`src/behavioral/parsers.ts`** — dataset parsers that turn the calibration scaffold from a skeleton into an end-to-end pipeline. Ships:
|
|
41
|
+
- **`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.
|
|
42
|
+
- **`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.
|
|
43
|
+
- **`parseGenericTelemetryJson()`** — bring-your-own-data path for users who export telemetry in the normalized `MouseMovement` / `TypingSession` shapes directly.
|
|
44
|
+
- **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).
|
|
45
|
+
- 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.
|
|
46
|
+
|
|
47
|
+
### Added — TLS side-channel via bogdanfinn/tls-client
|
|
48
|
+
|
|
49
|
+
- **`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.
|
|
50
|
+
- **`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.
|
|
51
|
+
- **`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.
|
|
52
|
+
- **`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.
|
|
53
|
+
- **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.
|
|
54
|
+
- **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.
|
|
55
|
+
|
|
56
|
+
### Added — calibration validated against real CMU dataset
|
|
57
|
+
|
|
58
|
+
- **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.
|
|
59
|
+
- **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.
|
|
60
|
+
- **`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 })`.
|
|
61
|
+
- **`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).
|
|
62
|
+
- **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.
|
|
63
|
+
|
|
64
|
+
### Added — diagnostics fixes
|
|
65
|
+
|
|
66
|
+
- **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).
|
|
67
|
+
- **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`.
|
|
68
|
+
|
|
69
|
+
### Test suite
|
|
70
|
+
|
|
71
|
+
- **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.
|
|
72
|
+
|
|
73
|
+
## [0.2.0] — 2026-04-10
|
|
74
|
+
|
|
75
|
+
### Defeating Akamai Bot Manager — the L016 fix
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
See `docs/akamai-bypass.md` for the full plan, methodology, and status against each Akamai detection layer.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- **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.
|
|
84
|
+
- 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.
|
|
85
|
+
- **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.
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
|
|
89
|
+
- **`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.
|
|
90
|
+
- **`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.
|
|
91
|
+
- **`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.
|
|
92
|
+
- **`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.
|
|
93
|
+
- **`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.
|
|
94
|
+
- **`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.
|
|
95
|
+
- **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.
|
|
96
|
+
|
|
97
|
+
### Validated against
|
|
98
|
+
|
|
99
|
+
- **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).
|
|
100
|
+
- All 0.1.0 detector targets continue to pass (bot.sannysoft, CreepJS, tls.peet.ws, browserleaks×4, fingerprint.com, pixelscan, browserscan, nowsecure.nl).
|
|
101
|
+
|
|
102
|
+
### Test suite
|
|
103
|
+
|
|
104
|
+
- **162 → 172 tests passing**, zero regressions.
|
|
105
|
+
|
|
106
|
+
### What's deferred to 0.3.0+
|
|
107
|
+
|
|
108
|
+
- TLS-rewriting proxy (`bogdanfinn/tls-client` integration) for cross-platform UA spoofing and Chrome version impersonation
|
|
109
|
+
- Akamai sensor data analysis and targeted JS-level patches for sites that probe deeper than headers
|
|
110
|
+
- Tier 2 behavioral calibration against real public datasets (Balabit, CMU Keystroke, GREYC)
|
|
111
|
+
|
|
112
|
+
## [0.1.0] — 2026-04-10
|
|
113
|
+
|
|
114
|
+
### First public release
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
### Added — core architecture
|
|
119
|
+
|
|
120
|
+
- **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.
|
|
121
|
+
- **`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.
|
|
122
|
+
- **Device profiles** — `desktop-windows`, `desktop-macos`, `desktop-linux` with matching user agents, hardware concurrency, plugins, fonts, and WebGL vendor/renderer strings.
|
|
123
|
+
- **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.
|
|
124
|
+
- **Retry engine** — six-strategy cascade (`standard`, `wait`, `reload`, `altSelector`, `scroll`, `clearOverlays`) with event emission on each retry.
|
|
125
|
+
- **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.
|
|
126
|
+
|
|
127
|
+
### Added — agent primitives
|
|
128
|
+
|
|
129
|
+
- **`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.
|
|
130
|
+
- **`bt.waitForText(text, {timeout})`** — polls body `innerText` for a target string. For server-rendered confirmations, OCR completion, and async content.
|
|
131
|
+
- **`bt.inspect(selector)`** — returns `{exists, visible, tagName, text, attributes, boundingBox}` in one call. Replaces multiple hand-written `executeJS` queries for diagnostics.
|
|
132
|
+
- **`bt.listOptions(selectorOrBaseId)`** — enumerates Angular/React-style custom dropdown options via the `{baseId}_option-{n}` pattern.
|
|
133
|
+
- **`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.
|
|
134
|
+
- **`bt.dismissOverlays()`** — proactively hides fixed/sticky overlays matching known patterns (Intercom, Drift, Zendesk, OneTrust, Medallia, cookie banners).
|
|
135
|
+
- **`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>`.
|
|
136
|
+
- **`bt.findInShadowDom(cssSelector, {timeout?})`** — recursive walker over open shadow roots. Supports modern component libraries (Lit, Stencil, Material Web Components).
|
|
137
|
+
- **`bt.download(selector, {saveTo})`** — click-to-download with returned `{path, size, suggestedFilename, url}` metadata.
|
|
138
|
+
|
|
139
|
+
### Added — click robustness
|
|
140
|
+
|
|
141
|
+
- **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.
|
|
142
|
+
- **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})`.
|
|
143
|
+
- **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`.
|
|
144
|
+
- **`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.
|
|
145
|
+
|
|
146
|
+
### Added — serve mode and CLI
|
|
147
|
+
|
|
148
|
+
- **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.
|
|
149
|
+
- **Batched commands** — `BATCH\n<json array>` runs multiple commands sequentially and returns an array of bundles, stopping on first failure.
|
|
150
|
+
- **CLI flags** — `--file <path>`, `--stdin`, `--pretty`, `--port` eliminate shell-escape hell for complex JavaScript commands.
|
|
151
|
+
- **`npx blacktip` subcommands** — `serve`, `send`, `batch`, `resume`, `pending`, `exec` with per-command help.
|
|
152
|
+
|
|
153
|
+
### Added — infrastructure
|
|
154
|
+
|
|
155
|
+
- **`ProxyPool`** — round-robin / random / least-used strategies, per-domain ban tracking with decay window, reputation snapshot for observability, URL formatters for BrightData / Oxylabs / Smartproxy.
|
|
156
|
+
- **`SnapshotManager`** — `capture()` and `restore()` for session migration across proxies or machines. Serializes cookies, localStorage, sessionStorage, and active URL into a JSON blob.
|
|
157
|
+
- **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.
|
|
158
|
+
- **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.
|
|
159
|
+
|
|
160
|
+
### Detector results
|
|
161
|
+
|
|
162
|
+
Captured against free public detectors as of the release:
|
|
163
|
+
|
|
164
|
+
- **bot.sannysoft.com** — 31 passed / 0 failed across 57 rows
|
|
165
|
+
- **tls.peet.ws** — JA4 `t13d1516h2_...`, first cipher rotates `TLS_GREASE (0x...)` matching real Chrome 122/124
|
|
166
|
+
- **CreepJS** — Grade A, 0% headless, 0% stealth, 31% like-headless (unavoidable desktop false-positive)
|
|
167
|
+
- **browserleaks.com/canvas** — signature shared with 100 of 298,939 browsers (blending in, not unique)
|
|
168
|
+
- **browserleaks.com/webgl** — real GPU via ANGLE, not SwiftShader
|
|
169
|
+
- **browserleaks.com/webrtc** — no RFC1918 local IP leak
|
|
170
|
+
- **browserleaks.com/javascript** — Chrome 122 on Win32, en-US, America/New_York, 1920×1080 all consistent
|
|
171
|
+
- **fingerprint.com/demo** — passes
|
|
172
|
+
- **pixelscan.net** — passes
|
|
173
|
+
- **browserscan.net** — identified as "Chrome on Windows 10", not flagged
|
|
174
|
+
|
|
175
|
+
Real-target validation:
|
|
176
|
+
|
|
177
|
+
- **nowsecure.nl** (Cloudflare bot-fight test, nodriver author's public benchmark) — passes without challenge
|
|
178
|
+
- **antoinevastel.com/bots** (ex-DataDome VP of Research) — loads without block
|
|
179
|
+
- **Anthem.com** (Okta MFA, Angular SPA, real medical claim submission) — full end-to-end flow successful
|
|
180
|
+
|
|
181
|
+
### Notes
|
|
182
|
+
|
|
183
|
+
- Always runs headful. `headless: true` in config is silently ignored — there is no real-headless path that passes serious detectors.
|
|
184
|
+
- Real Chrome must be installed on the host for the preferred `channel: 'chrome'` path. patchright's bundled Chromium is the fallback.
|
|
185
|
+
- 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.
|
|
186
|
+
|
|
187
|
+
[Unreleased]: https://github.com/rester159/blacktip/compare/v0.4.0...HEAD
|
|
188
|
+
[0.4.0]: https://github.com/rester159/blacktip/compare/v0.2.0...v0.4.0
|
|
189
|
+
[0.2.0]: https://github.com/rester159/blacktip/compare/v0.1.0...v0.2.0
|
|
190
|
+
[0.1.0]: https://github.com/rester159/blacktip/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo.svg" alt="BlackTip" width="360">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# BlackTip
|
|
2
6
|
|
|
7
|
+
[](https://www.npmjs.com/package/@rester159/blacktip)
|
|
8
|
+
[](https://www.npmjs.com/package/@rester159/blacktip)
|
|
9
|
+
[](https://github.com/rester159/blacktip/actions/workflows/ci.yml)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://github.com/rester159/blacktip/stargazers)
|
|
12
|
+
[](https://www.typescriptlang.org/)
|
|
13
|
+
[](https://nodejs.org/)
|
|
14
|
+
|
|
3
15
|
**Stealth browser instrument for AI agents.** Real Chrome + patchright CDP stealth + human-calibrated behavioral simulation. BlackTip is the hands, you (or your agent) are the brain.
|
|
4
16
|
|
|
5
17
|
BlackTip is not an agent. It does not parse natural language, does not plan, and does not decide what to click. It exposes primitives — `navigate`, `click`, `type`, `scroll`, `screenshot`, `waitForStable` — and wraps every action in human behavior that defeats bot detection.
|
|
@@ -37,6 +49,22 @@ BlackTip's architecture:
|
|
|
37
49
|
- nowsecure.nl (Cloudflare bot-fight test, nodriver author's public benchmark) — passes without challenge
|
|
38
50
|
- antoinevastel.com/bots (ex-DataDome VP of Research) — loads without block
|
|
39
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).
|
|
40
68
|
|
|
41
69
|
---
|
|
42
70
|
|
|
@@ -147,10 +175,32 @@ See `AGENTS.md` for the full agent-facing reference, including the decision tree
|
|
|
147
175
|
| `bt.cookies()` / `bt.setCookies()` / `bt.clearCookies()` | Cookie jar |
|
|
148
176
|
| `bt.executeJS(script)` | Raw JS evaluation |
|
|
149
177
|
| `bt.pauseForInput({prompt, validate?, timeoutMs?})` | User-in-the-loop (MFA) |
|
|
178
|
+
| `bt.captureFingerprint()` | TLS / HTTP2 / header snapshot via tls.peet.ws + httpbin (v0.2.0) |
|
|
179
|
+
| `bt.checkIpReputation()` | Egress IP, ASN, datacenter/residential heuristic (v0.2.0) |
|
|
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
|
+
|
|
185
|
+
Plus `IdentityPool` (v0.4.0) for long-running session and identity rotation across many flows. See **[docs/identity-pool.md](docs/identity-pool.md)**.
|
|
186
|
+
| `bt.warmSession({sites?, dwellMsRange?})` | Pre-target warm-up — visit normal sites first (v0.2.0) |
|
|
150
187
|
| `bt.serve(port?)` | Start TCP command server |
|
|
151
188
|
|
|
152
189
|
Plus `SnapshotManager`, `ProxyPool`, `attachObservability`, and the calibration module — see the TypeScript types for details.
|
|
153
190
|
|
|
191
|
+
### Akamai Bot Manager
|
|
192
|
+
|
|
193
|
+
As of v0.2.0, BlackTip passes Akamai Bot Manager on validated targets including OpenTable. The headline fix (L016) was a User-Agent / Sec-Ch-Ua client hint consistency bug that had been silently undermining stealth against top-tier commercial detectors since v0.1.0. See **[docs/akamai-bypass.md](docs/akamai-bypass.md)** for the full plan, methodology, current status by detection layer, and a troubleshooting checklist for users who hit blocks.
|
|
194
|
+
|
|
195
|
+
Quick verification that you're on a fixed build:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
const fp = await bt.captureFingerprint();
|
|
199
|
+
if (!fp.headers.uaConsistent) {
|
|
200
|
+
throw new Error('UA / Sec-Ch-Ua mismatch — upgrade BlackTip to v0.2.0+');
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
154
204
|
---
|
|
155
205
|
|
|
156
206
|
## Configuration
|
|
@@ -168,6 +218,10 @@ new BlackTip({
|
|
|
168
218
|
screenResolution: { width: 1920, height: 1080 },
|
|
169
219
|
proxy: 'http://user:pass@host:port',
|
|
170
220
|
chromiumPath: '/custom/chrome',
|
|
221
|
+
// v0.2.0 — persistent Chrome profile for cookies/history continuity
|
|
222
|
+
// across BlackTip runs. Critical for sites with "first request from
|
|
223
|
+
// unknown session" challenges (Akamai, DataDome, PerimeterX).
|
|
224
|
+
userDataDir: './.bt-profile',
|
|
171
225
|
});
|
|
172
226
|
```
|
|
173
227
|
|
|
@@ -196,6 +250,47 @@ Changes to BlackTip's `.ts` files recompile to `dist/` on save, and the consumin
|
|
|
196
250
|
|
|
197
251
|
---
|
|
198
252
|
|
|
253
|
+
## Server deployment
|
|
254
|
+
|
|
255
|
+
BlackTip runs real Chrome in headful mode — on servers you use **Xvfb** (virtual X framebuffer) so Chrome renders to a virtual display instead of a physical monitor. This is the standard approach used by every serious stealth tool; running Chrome with `--headless` is NOT supported because headless mode is detectable at many fingerprint levels.
|
|
256
|
+
|
|
257
|
+
The repo ships production-ready deployment artifacts:
|
|
258
|
+
|
|
259
|
+
- **`Dockerfile`** — production image with Chrome Stable, Xvfb, Node 20, and all runtime dependencies. Build with `docker build -t blacktip:latest .`.
|
|
260
|
+
- **`docker-compose.yml`** — one-command local / dev environment. `docker compose up --build`.
|
|
261
|
+
- **`deploy/systemd/xvfb.service`** and **`deploy/systemd/blacktip.service`** — systemd unit files for bare-metal VPS deployments.
|
|
262
|
+
- **`deploy/README.md`** — full deployment guide covering Docker, systemd, AWS / GCP / DigitalOcean / Hetzner / Fly.io, sizing, cost, monitoring, and troubleshooting.
|
|
263
|
+
|
|
264
|
+
**Quick Docker start:**
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
docker build -t blacktip:latest .
|
|
268
|
+
docker run --rm -it \
|
|
269
|
+
--shm-size=2gb \
|
|
270
|
+
--cap-add=SYS_ADMIN \
|
|
271
|
+
-p 9779:9779 \
|
|
272
|
+
blacktip:latest
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Quick bare-metal start (Ubuntu 22.04):**
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# System deps + Chrome + Node
|
|
279
|
+
sudo apt-get update && sudo apt-get install -y xvfb libnss3 libatk-bridge2.0-0 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libxss1 libasound2 fonts-liberation libvulkan1
|
|
280
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
|
281
|
+
sudo apt-get install -y nodejs
|
|
282
|
+
wget -qO /tmp/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && sudo apt install -y /tmp/chrome.deb
|
|
283
|
+
|
|
284
|
+
# Run under Xvfb
|
|
285
|
+
export DISPLAY=:99
|
|
286
|
+
Xvfb :99 -screen 0 1920x1080x24 &
|
|
287
|
+
node your-app.js
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
See **[deploy/README.md](deploy/README.md)** for the full guide including systemd setup, cloud provider specifics, resource sizing, and troubleshooting.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
199
294
|
## What BlackTip is NOT
|
|
200
295
|
|
|
201
296
|
- **Not an agent.** It doesn't plan or decide. A human or an LLM drives it.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dataset parsers for behavioral calibration.
|
|
3
|
+
*
|
|
4
|
+
* The calibration scaffold (`./calibration.ts`) defines normalized
|
|
5
|
+
* `MouseMovement` and `TypingSession` shapes plus distribution fitters.
|
|
6
|
+
* What was missing through v0.2.0 was the bridge from real public datasets
|
|
7
|
+
* into those shapes — without parsers, the scaffold required every user
|
|
8
|
+
* to write their own ETL, which kept "Tier 2 behavioral calibration" on
|
|
9
|
+
* the deferred list.
|
|
10
|
+
*
|
|
11
|
+
* This module ships parsers for the two datasets we point users at
|
|
12
|
+
* most often:
|
|
13
|
+
*
|
|
14
|
+
* 1. **CMU Keystroke Dynamics** (Killourhy & Maxion, 2009).
|
|
15
|
+
* Free for research. CSV format with columns `subject`, `sessionIndex`,
|
|
16
|
+
* `rep`, then for the fixed phrase `.tie5Roanl` a tuple of
|
|
17
|
+
* `H.<key>` (hold), `DD.<k1>.<k2>` (down-down latency), and
|
|
18
|
+
* `UD.<k1>.<k2>` (up-down = flight time). 51 subjects × 8 sessions
|
|
19
|
+
* × 50 repetitions = 20,400 phrases.
|
|
20
|
+
*
|
|
21
|
+
* 2. **Balabit Mouse Dynamics Challenge** (Antal & Egyed-Zsigmond, 2014).
|
|
22
|
+
* Free for research. Per-session CSV with columns
|
|
23
|
+
* `record_timestamp`, `client_timestamp`, `button`, `state`, `x`, `y`.
|
|
24
|
+
* Each row is a single mouse event; consecutive `Mouse` rows
|
|
25
|
+
* followed by a `Pressed` row form one movement.
|
|
26
|
+
*
|
|
27
|
+
* Plus a generic JSON loader for users with their own telemetry exported
|
|
28
|
+
* in the `MouseMovement` / `TypingSession` shapes directly.
|
|
29
|
+
*
|
|
30
|
+
* The parsers do NOT download the datasets — both have ToS that say "do
|
|
31
|
+
* not redistribute". Users acquire the data themselves and feed the file
|
|
32
|
+
* contents in as a string. We just turn raw text into normalized samples.
|
|
33
|
+
*/
|
|
34
|
+
import type { MouseMovement, TypingSession } from './calibration.js';
|
|
35
|
+
/** The fixed phrase typed by every CMU subject. Used to map column index → key. */
|
|
36
|
+
export declare const CMU_PHRASE = ".tie5Roanl";
|
|
37
|
+
/**
|
|
38
|
+
* Parse the CMU Keystroke Dynamics CSV (DSL-StrongPasswordData.csv).
|
|
39
|
+
*
|
|
40
|
+
* Each row is one repetition of the phrase `.tie5Roanl` plus Return,
|
|
41
|
+
* yielding 11 keys, 11 hold-times, 10 down-down and 10 up-down latencies.
|
|
42
|
+
* We normalize each row into one `TypingSession` of 11 keystrokes.
|
|
43
|
+
*
|
|
44
|
+
* The CSV looks like:
|
|
45
|
+
* subject,sessionIndex,rep,H.period,DD.period.t,UD.period.t,H.t,DD.t.i,UD.t.i,H.i,...
|
|
46
|
+
* s002,1,1,0.1491,0.3979,0.2488,0.1069,0.1674,0.0605,...
|
|
47
|
+
*
|
|
48
|
+
* All time values are in **seconds** in the source file; we convert to
|
|
49
|
+
* milliseconds.
|
|
50
|
+
*
|
|
51
|
+
* Returns one `TypingSession` per CSV row. Pass these straight into
|
|
52
|
+
* `fitTypingDynamics()`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseCmuKeystrokeCsv(csvText: string): TypingSession[];
|
|
55
|
+
/**
|
|
56
|
+
* Parse a single Balabit Mouse Dynamics session CSV.
|
|
57
|
+
*
|
|
58
|
+
* Each row is one mouse event:
|
|
59
|
+
* record_timestamp,client_timestamp,button,state,x,y
|
|
60
|
+
* 1424866316.93,0.000,NoButton,Move,1192,529
|
|
61
|
+
* 1424866316.99,0.064,NoButton,Move,1183,532
|
|
62
|
+
* ...
|
|
63
|
+
* 1424866317.84,0.911,Left,Pressed,820,440
|
|
64
|
+
*
|
|
65
|
+
* We segment into movements: each contiguous run of `Move`/`Drag` rows
|
|
66
|
+
* followed by a `Pressed` row becomes one `MouseMovement`. The press row
|
|
67
|
+
* is the click target. Movements that don't end in a press are also
|
|
68
|
+
* recorded but with `endedWithClick: false` — these are navigation moves.
|
|
69
|
+
*
|
|
70
|
+
* Time is normalized to milliseconds since the first sample of the
|
|
71
|
+
* movement.
|
|
72
|
+
*/
|
|
73
|
+
export declare function parseBalabitMouseCsv(csvText: string): MouseMovement[];
|
|
74
|
+
/**
|
|
75
|
+
* Generic loader for users who already export their telemetry in the
|
|
76
|
+
* normalized shapes. Accepts JSON of the form:
|
|
77
|
+
*
|
|
78
|
+
* { "movements": MouseMovement[], "sessions": TypingSession[] }
|
|
79
|
+
*
|
|
80
|
+
* Either field may be absent. Returns the parsed object with empty
|
|
81
|
+
* defaults filled in. This is the "bring your own data" path — most
|
|
82
|
+
* production users will write a tiny exporter on their own telemetry
|
|
83
|
+
* pipeline that emits this shape, then feed it through `fitFromSamples()`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function parseGenericTelemetryJson(jsonText: string): {
|
|
86
|
+
movements: MouseMovement[];
|
|
87
|
+
sessions: TypingSession[];
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=parsers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parsers.d.ts","sourceRoot":"","sources":["../../src/behavioral/parsers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAmB,aAAa,EAAe,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAInG,mFAAmF;AACnF,eAAO,MAAM,UAAU,eAAe,CAAC;AAEvC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,EAAE,CAoDrE;AAoBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,EAAE,CA0DrE;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC3D,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,CASA"}
|