onionwright 0.0.1__tar.gz
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.
- onionwright-0.0.1/.gitignore +7 -0
- onionwright-0.0.1/LICENSE +75 -0
- onionwright-0.0.1/PKG-INFO +108 -0
- onionwright-0.0.1/README.md +94 -0
- onionwright-0.0.1/harness/measure.py +167 -0
- onionwright-0.0.1/harness/page.html +70 -0
- onionwright-0.0.1/packaging/README-pypi.md +16 -0
- onionwright-0.0.1/pyproject.toml +35 -0
- onionwright-0.0.1/src/onionwright/__init__.py +7 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
onionwright Commercial Licence
|
|
2
|
+
Copyright (c) 2026 OpenOnion. All rights reserved.
|
|
3
|
+
|
|
4
|
+
NOT OPEN SOURCE. This software is proprietary and is licensed, not sold.
|
|
5
|
+
|
|
6
|
+
NOTE: This text has not been reviewed by a lawyer. It states the intent agreed for
|
|
7
|
+
the product and is a drafting starting point, not final terms. Review before the
|
|
8
|
+
first paid release.
|
|
9
|
+
|
|
10
|
+
1. GRANT
|
|
11
|
+
|
|
12
|
+
Subject to an active subscription at the tier designated by OpenOnion, you are
|
|
13
|
+
granted a non-exclusive, non-transferable, revocable licence to install and use
|
|
14
|
+
this software for your own purposes, personal or commercial, on systems you
|
|
15
|
+
control.
|
|
16
|
+
|
|
17
|
+
2. RESTRICTIONS
|
|
18
|
+
|
|
19
|
+
You may not:
|
|
20
|
+
|
|
21
|
+
a. Redistribute this software, in whole or in part, modified or unmodified.
|
|
22
|
+
b. Resell, sublicense, rent, lease, or repackage it.
|
|
23
|
+
c. Reverse engineer, decompile, or disassemble it, or attempt to derive its
|
|
24
|
+
source, except to the extent applicable law expressly permits despite this
|
|
25
|
+
restriction.
|
|
26
|
+
d. Remove or alter any copyright, licence, or attribution notice.
|
|
27
|
+
e. Share licence credentials, or use one licence across more concurrent
|
|
28
|
+
sessions than your tier permits.
|
|
29
|
+
|
|
30
|
+
3. OEM / SAAS
|
|
31
|
+
|
|
32
|
+
A separate written agreement is required where this software is bundled,
|
|
33
|
+
embedded, exposed through an API, or otherwise used to provide functionality to
|
|
34
|
+
third-party customers. No subscription tier grants redistribution, resale,
|
|
35
|
+
sublicensing, bundling, embedding, hosted-service, white-label, OEM, or SaaS
|
|
36
|
+
rights unless expressly stated in a separate agreement.
|
|
37
|
+
|
|
38
|
+
4. VERSIONS AND CONTINUITY
|
|
39
|
+
|
|
40
|
+
The latest major version requires an active subscription. Versions already
|
|
41
|
+
obtained lawfully under an earlier active subscription remain usable
|
|
42
|
+
indefinitely; lapsing only stops access to new releases, it does not disable
|
|
43
|
+
what you already have.
|
|
44
|
+
|
|
45
|
+
5. ACCEPTABLE USE
|
|
46
|
+
|
|
47
|
+
This software is licensed for lawful automation of systems you own or are
|
|
48
|
+
authorised to access — testing, monitoring, authorised data collection, and
|
|
49
|
+
management of accounts you control under terms you are entitled to accept. It
|
|
50
|
+
is not licensed for unauthorised access, fraud, or circumventing controls you
|
|
51
|
+
have no right to circumvent. OpenOnion may terminate a licence for use in
|
|
52
|
+
breach of this section.
|
|
53
|
+
|
|
54
|
+
6. NO WARRANTY
|
|
55
|
+
|
|
56
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
57
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
58
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. Detection and
|
|
59
|
+
anti-detection are adversarial and change continuously; no representation is
|
|
60
|
+
made that any particular site will accept traffic driven by this software.
|
|
61
|
+
|
|
62
|
+
7. LIMITATION OF LIABILITY
|
|
63
|
+
|
|
64
|
+
IN NO EVENT SHALL OPENONION BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
|
65
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM
|
|
66
|
+
OR IN CONNECTION WITH THE SOFTWARE OR ITS USE.
|
|
67
|
+
|
|
68
|
+
8. THIRD-PARTY COMPONENTS
|
|
69
|
+
|
|
70
|
+
This software depends on Playwright (Apache-2.0, Copyright (c) Microsoft
|
|
71
|
+
Corporation), which it uses through its public API without modification.
|
|
72
|
+
Playwright is not distributed as part of this software and remains governed by
|
|
73
|
+
its own licence.
|
|
74
|
+
|
|
75
|
+
Contact: info@openonion.ai
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: onionwright
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Humanized input for browser automation
|
|
5
|
+
Project-URL: Homepage, https://openonion.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.connectonion.com
|
|
7
|
+
Author-email: OpenOnion <info@openonion.ai>
|
|
8
|
+
License: onionwright Commercial Licence
|
|
9
|
+
Copyright (c) 2026 OpenOnion. All rights reserved.
|
|
10
|
+
|
|
11
|
+
NOT OPEN SOURCE. This software is proprietary and is licensed, not sold.
|
|
12
|
+
|
|
13
|
+
NOTE: This text has not been reviewed by a lawyer. It states the intent agreed for
|
|
14
|
+
the product and is a drafting starting point, not final terms. Review before the
|
|
15
|
+
first paid release.
|
|
16
|
+
|
|
17
|
+
1. GRANT
|
|
18
|
+
|
|
19
|
+
Subject to an active subscription at the tier designated by OpenOnion, you are
|
|
20
|
+
granted a non-exclusive, non-transferable, revocable licence to install and use
|
|
21
|
+
this software for your own purposes, personal or commercial, on systems you
|
|
22
|
+
control.
|
|
23
|
+
|
|
24
|
+
2. RESTRICTIONS
|
|
25
|
+
|
|
26
|
+
You may not:
|
|
27
|
+
|
|
28
|
+
a. Redistribute this software, in whole or in part, modified or unmodified.
|
|
29
|
+
b. Resell, sublicense, rent, lease, or repackage it.
|
|
30
|
+
c. Reverse engineer, decompile, or disassemble it, or attempt to derive its
|
|
31
|
+
source, except to the extent applicable law expressly permits despite this
|
|
32
|
+
restriction.
|
|
33
|
+
d. Remove or alter any copyright, licence, or attribution notice.
|
|
34
|
+
e. Share licence credentials, or use one licence across more concurrent
|
|
35
|
+
sessions than your tier permits.
|
|
36
|
+
|
|
37
|
+
3. OEM / SAAS
|
|
38
|
+
|
|
39
|
+
A separate written agreement is required where this software is bundled,
|
|
40
|
+
embedded, exposed through an API, or otherwise used to provide functionality to
|
|
41
|
+
third-party customers. No subscription tier grants redistribution, resale,
|
|
42
|
+
sublicensing, bundling, embedding, hosted-service, white-label, OEM, or SaaS
|
|
43
|
+
rights unless expressly stated in a separate agreement.
|
|
44
|
+
|
|
45
|
+
4. VERSIONS AND CONTINUITY
|
|
46
|
+
|
|
47
|
+
The latest major version requires an active subscription. Versions already
|
|
48
|
+
obtained lawfully under an earlier active subscription remain usable
|
|
49
|
+
indefinitely; lapsing only stops access to new releases, it does not disable
|
|
50
|
+
what you already have.
|
|
51
|
+
|
|
52
|
+
5. ACCEPTABLE USE
|
|
53
|
+
|
|
54
|
+
This software is licensed for lawful automation of systems you own or are
|
|
55
|
+
authorised to access — testing, monitoring, authorised data collection, and
|
|
56
|
+
management of accounts you control under terms you are entitled to accept. It
|
|
57
|
+
is not licensed for unauthorised access, fraud, or circumventing controls you
|
|
58
|
+
have no right to circumvent. OpenOnion may terminate a licence for use in
|
|
59
|
+
breach of this section.
|
|
60
|
+
|
|
61
|
+
6. NO WARRANTY
|
|
62
|
+
|
|
63
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
64
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
65
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. Detection and
|
|
66
|
+
anti-detection are adversarial and change continuously; no representation is
|
|
67
|
+
made that any particular site will accept traffic driven by this software.
|
|
68
|
+
|
|
69
|
+
7. LIMITATION OF LIABILITY
|
|
70
|
+
|
|
71
|
+
IN NO EVENT SHALL OPENONION BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
|
72
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM
|
|
73
|
+
OR IN CONNECTION WITH THE SOFTWARE OR ITS USE.
|
|
74
|
+
|
|
75
|
+
8. THIRD-PARTY COMPONENTS
|
|
76
|
+
|
|
77
|
+
This software depends on Playwright (Apache-2.0, Copyright (c) Microsoft
|
|
78
|
+
Corporation), which it uses through its public API without modification.
|
|
79
|
+
Playwright is not distributed as part of this software and remains governed by
|
|
80
|
+
its own licence.
|
|
81
|
+
|
|
82
|
+
Contact: info@openonion.ai
|
|
83
|
+
License-File: LICENSE
|
|
84
|
+
Keywords: automation,browser,human,input,playwright
|
|
85
|
+
Classifier: Intended Audience :: Developers
|
|
86
|
+
Classifier: License :: Other/Proprietary License
|
|
87
|
+
Classifier: Programming Language :: Python :: 3
|
|
88
|
+
Classifier: Topic :: Software Development :: Testing
|
|
89
|
+
Requires-Python: >=3.9
|
|
90
|
+
Requires-Dist: playwright>=1.55
|
|
91
|
+
Description-Content-Type: text/markdown
|
|
92
|
+
|
|
93
|
+
# onionwright
|
|
94
|
+
|
|
95
|
+
Humanized input for browser automation — a commercial layer over Playwright that
|
|
96
|
+
makes automated interaction behave like a person using the browser.
|
|
97
|
+
|
|
98
|
+
**This is a placeholder release. There is no implementation in this version.**
|
|
99
|
+
|
|
100
|
+
onionwright is proprietary software distributed under a commercial licence, not
|
|
101
|
+
from this index. This entry reserves the name and points at the product.
|
|
102
|
+
|
|
103
|
+
- Product: https://openonion.ai
|
|
104
|
+
- Docs: https://docs.connectonion.com
|
|
105
|
+
- Licensing enquiries: info@openonion.ai
|
|
106
|
+
|
|
107
|
+
Licensed, not sold. Redistribution, resale, and reverse engineering are not
|
|
108
|
+
permitted. See the LICENSE included in the distribution.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# onionwright
|
|
2
|
+
|
|
3
|
+
Humanized input for browser automation. A drop-in layer over Playwright's public
|
|
4
|
+
API that makes automated interaction statistically indistinguishable from a person
|
|
5
|
+
using the browser.
|
|
6
|
+
|
|
7
|
+
**Status: scaffold.** No implementation yet — the repo exists so the name, the
|
|
8
|
+
licence, and the release path are settled before code lands. Work is tracked in
|
|
9
|
+
issues.
|
|
10
|
+
|
|
11
|
+
## Positioning
|
|
12
|
+
|
|
13
|
+
> Patchright makes you look like a real browser.
|
|
14
|
+
> onionwright makes you *act* like a real person.
|
|
15
|
+
|
|
16
|
+
[Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright) (Apache-2.0, free,
|
|
17
|
+
and what OpenOnion's free tier uses) fixes **static** automation tells: the
|
|
18
|
+
`Runtime.enable` CDP leak, detectable launch flags, the `__playwright__binding__`
|
|
19
|
+
global. It does not touch timing or behaviour at all.
|
|
20
|
+
|
|
21
|
+
Measured on stock Playwright 1.55 driving Chrome 150 (`harness/measure.py`):
|
|
22
|
+
|
|
23
|
+
| signal | stock Playwright | human |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| keystroke flight (keyup → next keydown) | **2.8 ms** | 200–400 ms |
|
|
26
|
+
| keystroke hold, standard deviation | **0.7 ms** | tens of ms |
|
|
27
|
+
| click hold (mousedown → mouseup) | **0.4 ms** | 60–150 ms |
|
|
28
|
+
| hover → press dwell | **0.45 ms** | hundreds of ms |
|
|
29
|
+
| mouse path | exact linear interpolation — every step `movementX` 11–12, zero acceleration | jerk-limited, with corrective submovements |
|
|
30
|
+
|
|
31
|
+
`page.keyboard.type(delay=80)` sets key **hold** time, not flight time. That is
|
|
32
|
+
why flights come out at 2.8 ms: nothing in Playwright's API controls the interval
|
|
33
|
+
a detector actually measures.
|
|
34
|
+
|
|
35
|
+
These are different layers, not competing products. The free tier keeps Patchright.
|
|
36
|
+
|
|
37
|
+
## Where this sits
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
free playwright + stock Chrome
|
|
41
|
+
paid onionwright + onion browser (patched Chromium)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
onionwright ships first: it works against stock Chrome, so it needs no build
|
|
45
|
+
infrastructure and is useful before a single Chromium patch exists. The patched
|
|
46
|
+
browser ([openonion/browser](https://github.com/openonion/browser)) covers what no
|
|
47
|
+
driver-level layer can reach — TLS/JA3, HTTP/2 framing, and fingerprint surfaces
|
|
48
|
+
that live below the script layer.
|
|
49
|
+
|
|
50
|
+
## Design commitments
|
|
51
|
+
|
|
52
|
+
**On top of Playwright, never inside it.** onionwright calls `page.mouse.move()`
|
|
53
|
+
and friends through the public API. It does not patch Playwright's internals, so
|
|
54
|
+
there is no AST surgery, no per-release rebase treadmill, and no inherited test
|
|
55
|
+
failures. Playwright upgrades do not break us. (Patchright takes the other route —
|
|
56
|
+
~4,900 lines of ts-morph rewriting against a fresh upstream tree — and pays ~70
|
|
57
|
+
failing Playwright tests for it, including Console being entirely dead.)
|
|
58
|
+
|
|
59
|
+
**Distributions, not paths.** Detectors compare moments of empirical distributions,
|
|
60
|
+
not screenshots. DataDome's payload ships sigma/mu and angle medians; Unbotify's
|
|
61
|
+
patent (US 2017/0185758 A1) describes histogram comparison against references. A
|
|
62
|
+
trajectory that looks human and has the wrong variance is not an improvement, so
|
|
63
|
+
`harness/` asserts on distributions.
|
|
64
|
+
|
|
65
|
+
**Bézier is a signature, not a defence.** Cloudflare's Precursor launch names the
|
|
66
|
+
tell directly: bots produce "linear interpolations or mathematically ideal Bézier
|
|
67
|
+
curves". BeCAPTCHA-Mouse (*Pattern Recognition*, 2022) trained a GAN reproducing
|
|
68
|
+
human traits and a plain random forest still detected it. Every open-source
|
|
69
|
+
humanizer is Bézier-plus-easing, which is roughly 2020 state of the art. Our core
|
|
70
|
+
is a biomechanical model instead — minimum-jerk (Flash & Hogan 1985) with
|
|
71
|
+
optimized submovements (Meyer et al. 1988), so overshoot-and-correct emerges from
|
|
72
|
+
the model rather than being bolted on as a random branch.
|
|
73
|
+
|
|
74
|
+
## harness/
|
|
75
|
+
|
|
76
|
+
The measurement instrument, not the product. Serves an instrumented page over
|
|
77
|
+
`http://127.0.0.1` (a secure context, so `pointerrawupdate` and
|
|
78
|
+
`getCoalescedEvents` behave as they do in production — on `about:blank` they
|
|
79
|
+
silently disappear, which is the likely origin of several widely repeated false
|
|
80
|
+
claims about CDP).
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
python3 harness/measure.py # system Chrome via channel="chrome"
|
|
84
|
+
python3 harness/measure.py --bundled # Playwright's bundled Chromium
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Run it before writing any humanization code: the numbers above are the baseline
|
|
88
|
+
every change is measured against.
|
|
89
|
+
|
|
90
|
+
## Licence
|
|
91
|
+
|
|
92
|
+
Proprietary and commercial — see [`LICENSE`](LICENSE). This is not open source.
|
|
93
|
+
Distribution is gated on an active licence
|
|
94
|
+
([openonion/browser#6](https://github.com/openonion/browser/issues/6)).
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Measure the input-timing distributions a driver actually produces.
|
|
2
|
+
|
|
3
|
+
Run against stock Playwright to get the baseline every humanization change is
|
|
4
|
+
measured against. The point is distributions, not paths: detectors compare
|
|
5
|
+
moments of empirical distributions (DataDome ships sigma/mu/angle medians;
|
|
6
|
+
Unbotify's patent describes histogram comparison), so a trajectory that looks
|
|
7
|
+
human in a screenshot but has the wrong variance is not an improvement.
|
|
8
|
+
|
|
9
|
+
python3 harness/measure.py # stock playwright, system Chrome
|
|
10
|
+
python3 harness/measure.py --bundled # playwright's bundled Chromium
|
|
11
|
+
|
|
12
|
+
Served over http://127.0.0.1 rather than set_content(): localhost is a secure
|
|
13
|
+
context, so pointerrawupdate and getCoalescedEvents behave as they would in
|
|
14
|
+
production. On about:blank they silently disappear.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import functools
|
|
19
|
+
import http.server
|
|
20
|
+
import json
|
|
21
|
+
import math
|
|
22
|
+
import statistics
|
|
23
|
+
import threading
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from playwright.sync_api import sync_playwright
|
|
27
|
+
|
|
28
|
+
HERE = Path(__file__).parent
|
|
29
|
+
PORT = 8931
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def serve():
|
|
33
|
+
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(HERE))
|
|
34
|
+
handler.log_message = lambda *a: None
|
|
35
|
+
server = http.server.ThreadingHTTPServer(("127.0.0.1", PORT), handler)
|
|
36
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
37
|
+
return server
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def drive(page):
|
|
41
|
+
"""One scenario: move to the field, type, then move to the button and click."""
|
|
42
|
+
page.mouse.move(60, 60)
|
|
43
|
+
page.click("#field")
|
|
44
|
+
page.keyboard.type("hello world", delay=80)
|
|
45
|
+
page.mouse.move(480, 344, steps=25)
|
|
46
|
+
page.click("#target")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def spans(events, down, up, key=lambda e: None):
|
|
50
|
+
"""Durations from each `down` to the matching `up`, in ms."""
|
|
51
|
+
open_at = {}
|
|
52
|
+
out = []
|
|
53
|
+
for e in events:
|
|
54
|
+
if e["type"] == down:
|
|
55
|
+
open_at[key(e)] = e["t"]
|
|
56
|
+
elif e["type"] == up and key(e) in open_at:
|
|
57
|
+
out.append(e["t"] - open_at.pop(key(e)))
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def flights(events):
|
|
62
|
+
"""keyup -> next keydown, in ms. The interval Playwright's `delay` does NOT set."""
|
|
63
|
+
out = []
|
|
64
|
+
last_up = None
|
|
65
|
+
for e in events:
|
|
66
|
+
if e["type"] == "keyup":
|
|
67
|
+
last_up = e["t"]
|
|
68
|
+
elif e["type"] == "keydown" and last_up is not None:
|
|
69
|
+
out.append(e["t"] - last_up)
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def describe(name, values, human):
|
|
74
|
+
"""Median leads: one setup gap (the click before typing) skews the mean badly
|
|
75
|
+
at these sample sizes, and it is the steady-state interval we care about."""
|
|
76
|
+
if not values:
|
|
77
|
+
print(f" {name:<26} (none observed)")
|
|
78
|
+
return
|
|
79
|
+
print(f" {name:<26} n={len(values):<3} median={statistics.median(values):8.2f}ms "
|
|
80
|
+
f"mean={statistics.mean(values):8.2f} sd={statistics.pstdev(values):7.2f} human≈{human}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def longest_run(events):
|
|
84
|
+
"""The longest unbroken stretch of mousemove events.
|
|
85
|
+
|
|
86
|
+
A scenario contains both teleports (page.click jumps straight to the target)
|
|
87
|
+
and travelled paths. Mixing them makes a teleport look like curvature, which
|
|
88
|
+
flatters the straightness index. Measure one journey at a time.
|
|
89
|
+
"""
|
|
90
|
+
# pointermove/pointerrawupdate interleave with every mousemove and must stay
|
|
91
|
+
# transparent here, or every run is length 1.
|
|
92
|
+
transparent = {"pointermove", "pointerrawupdate", "mouseover"}
|
|
93
|
+
runs, current = [], []
|
|
94
|
+
for e in events:
|
|
95
|
+
if e["type"] in transparent:
|
|
96
|
+
continue
|
|
97
|
+
if e["type"] == "mousemove":
|
|
98
|
+
current.append(e)
|
|
99
|
+
elif current:
|
|
100
|
+
runs.append(current)
|
|
101
|
+
current = []
|
|
102
|
+
runs.append(current)
|
|
103
|
+
return max(runs, key=len)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def path_metrics(moves):
|
|
107
|
+
"""Straightness and velocity variability over a mouse path."""
|
|
108
|
+
pts = [(m["clientX"], m["clientY"], m["t"]) for m in moves]
|
|
109
|
+
if len(pts) < 3:
|
|
110
|
+
print(f" path only {len(pts)} move events — no path to measure")
|
|
111
|
+
return
|
|
112
|
+
step = [math.dist(pts[i][:2], pts[i + 1][:2]) for i in range(len(pts) - 1)]
|
|
113
|
+
gap = [pts[i + 1][2] - pts[i][2] for i in range(len(pts) - 1)]
|
|
114
|
+
direct = math.dist(pts[0][:2], pts[-1][:2]) or 1
|
|
115
|
+
speeds = [d / g for d, g in zip(step, gap) if g > 0]
|
|
116
|
+
cov = statistics.pstdev(speeds) / statistics.mean(speeds) if speeds else 0
|
|
117
|
+
print(f" path n={len(pts)} moves")
|
|
118
|
+
print(f" straightness index {sum(step) / direct:.4f} human≈1.02–1.5, bot→1.0000")
|
|
119
|
+
print(f" velocity CoV {cov:.4f} human>0.4")
|
|
120
|
+
print(f" step length sd {statistics.pstdev(step):.4f} 0 means linear interpolation")
|
|
121
|
+
print(f" inter-event gap mean={statistics.mean(gap):.2f}ms sd={statistics.pstdev(gap):.2f}"
|
|
122
|
+
f" real mice quantize to 8ms@125Hz")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main():
|
|
126
|
+
ap = argparse.ArgumentParser()
|
|
127
|
+
ap.add_argument("--bundled", action="store_true", help="use Playwright's bundled Chromium")
|
|
128
|
+
args = ap.parse_args()
|
|
129
|
+
|
|
130
|
+
serve()
|
|
131
|
+
with sync_playwright() as p:
|
|
132
|
+
launch = {"headless": False} if args.bundled else {"headless": False, "channel": "chrome"}
|
|
133
|
+
browser = p.chromium.launch(**launch)
|
|
134
|
+
page = browser.new_page()
|
|
135
|
+
page.goto(f"http://127.0.0.1:{PORT}/page.html")
|
|
136
|
+
drive(page)
|
|
137
|
+
events = page.evaluate("window.__events")
|
|
138
|
+
ctx = page.evaluate("window.__ctx")
|
|
139
|
+
version = browser.version
|
|
140
|
+
browser.close()
|
|
141
|
+
|
|
142
|
+
print(f"\nengine {'bundled chromium' if args.bundled else 'system chrome'} {version}")
|
|
143
|
+
print(f"context secure={ctx['isSecureContext']} coalescedAPI={ctx['hasCoalesced']} "
|
|
144
|
+
f"webdriver={ctx['webdriver']}")
|
|
145
|
+
|
|
146
|
+
mouse = [e for e in events if e["type"] == "mousemove"]
|
|
147
|
+
leak = sum(1 for e in mouse if e["clientX"] == e["screenX"] and e["clientY"] == e["screenY"])
|
|
148
|
+
print(f"screen/client coords {leak}/{len(mouse)} identical "
|
|
149
|
+
f"(brotector Input.cordinatesLeak; fixed upstream ~Chrome 142)")
|
|
150
|
+
print(f"isTrusted all={all(e['trusted'] for e in events)}")
|
|
151
|
+
|
|
152
|
+
print("\nkeyboard")
|
|
153
|
+
describe("hold (keydown->keyup)", spans(events, "keydown", "keyup", lambda e: e["code"]), "60–150ms, sd tens")
|
|
154
|
+
describe("flight (keyup->keydown)", flights(events), "200–400ms")
|
|
155
|
+
|
|
156
|
+
print("\nmouse")
|
|
157
|
+
describe("click hold (down->up)", spans(events, "mousedown", "mouseup"), "60–150ms")
|
|
158
|
+
dwell = spans(events, "mouseover", "mousedown", lambda e: e.get("target"))
|
|
159
|
+
describe("hover->press dwell", dwell, "hundreds of ms")
|
|
160
|
+
path_metrics(longest_run(events))
|
|
161
|
+
|
|
162
|
+
Path(HERE / "last-run.json").write_text(json.dumps(events, indent=1))
|
|
163
|
+
print(f"\nraw events -> {HERE / 'last-run.json'}\n")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<meta charset="utf-8">
|
|
3
|
+
<title>input measurement harness</title>
|
|
4
|
+
<style>
|
|
5
|
+
body { font: 14px system-ui; margin: 40px; }
|
|
6
|
+
#target { width: 160px; height: 48px; margin-top: 300px; margin-left: 400px; }
|
|
7
|
+
#field { width: 300px; padding: 8px; }
|
|
8
|
+
</style>
|
|
9
|
+
|
|
10
|
+
<h1>input measurement harness</h1>
|
|
11
|
+
<p>Records raw input events for distribution analysis. Nothing here is stealth
|
|
12
|
+
logic — this is the instrument, not the thing being measured.</p>
|
|
13
|
+
|
|
14
|
+
<input id="field" placeholder="typing target">
|
|
15
|
+
<button id="target">click target</button>
|
|
16
|
+
|
|
17
|
+
<script>
|
|
18
|
+
// One flat event log. Analysis happens in Python, where the distribution work
|
|
19
|
+
// belongs — this file only needs to observe without perturbing timing.
|
|
20
|
+
window.__events = [];
|
|
21
|
+
|
|
22
|
+
function record(e) {
|
|
23
|
+
const r = {
|
|
24
|
+
type: e.type,
|
|
25
|
+
t: e.timeStamp,
|
|
26
|
+
trusted: e.isTrusted,
|
|
27
|
+
};
|
|
28
|
+
if (e instanceof MouseEvent) {
|
|
29
|
+
Object.assign(r, {
|
|
30
|
+
clientX: e.clientX, clientY: e.clientY,
|
|
31
|
+
screenX: e.screenX, screenY: e.screenY,
|
|
32
|
+
movementX: e.movementX, movementY: e.movementY,
|
|
33
|
+
buttons: e.buttons, detail: e.detail,
|
|
34
|
+
target: e.target.id || null,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (e instanceof PointerEvent) {
|
|
38
|
+
Object.assign(r, {
|
|
39
|
+
pointerId: e.pointerId, pointerType: e.pointerType,
|
|
40
|
+
pressure: e.pressure, tiltX: e.tiltX, tiltY: e.tiltY,
|
|
41
|
+
// Coalescing is how real pointer streams arrive; a driver that emits one
|
|
42
|
+
// discrete event per move never produces a group here.
|
|
43
|
+
coalesced: e.getCoalescedEvents ? e.getCoalescedEvents().length : -1,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (e instanceof KeyboardEvent) {
|
|
47
|
+
Object.assign(r, { key: e.key, code: e.code, repeat: e.repeat });
|
|
48
|
+
}
|
|
49
|
+
window.__events.push(r);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const type of [
|
|
53
|
+
'mousemove', 'mousedown', 'mouseup', 'click', 'mouseover',
|
|
54
|
+
'pointermove', 'pointerrawupdate', 'pointerdown', 'pointerup',
|
|
55
|
+
'keydown', 'keyup', 'wheel',
|
|
56
|
+
]) {
|
|
57
|
+
window.addEventListener(type, record, { capture: true, passive: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Secure-context gate check. pointerrawupdate and getCoalescedEvents are only
|
|
61
|
+
// available in a secure context; testing on about:blank or a data: URL makes
|
|
62
|
+
// them look absent and produces wrong conclusions.
|
|
63
|
+
window.__ctx = {
|
|
64
|
+
isSecureContext: window.isSecureContext,
|
|
65
|
+
hasCoalesced: typeof PointerEvent.prototype.getCoalescedEvents === 'function',
|
|
66
|
+
webdriver: navigator.webdriver,
|
|
67
|
+
outerInnerDelta: window.outerHeight - window.innerHeight,
|
|
68
|
+
screen: { x: window.screenX, y: window.screenY },
|
|
69
|
+
};
|
|
70
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# onionwright
|
|
2
|
+
|
|
3
|
+
Humanized input for browser automation — a commercial layer over Playwright that
|
|
4
|
+
makes automated interaction behave like a person using the browser.
|
|
5
|
+
|
|
6
|
+
**This is a placeholder release. There is no implementation in this version.**
|
|
7
|
+
|
|
8
|
+
onionwright is proprietary software distributed under a commercial licence, not
|
|
9
|
+
from this index. This entry reserves the name and points at the product.
|
|
10
|
+
|
|
11
|
+
- Product: https://openonion.ai
|
|
12
|
+
- Docs: https://docs.connectonion.com
|
|
13
|
+
- Licensing enquiries: info@openonion.ai
|
|
14
|
+
|
|
15
|
+
Licensed, not sold. Redistribution, resale, and reverse engineering are not
|
|
16
|
+
permitted. See the LICENSE included in the distribution.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "onionwright"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Humanized input for browser automation"
|
|
9
|
+
# NOT the repo README. That one is internal: it carries the positioning, the
|
|
10
|
+
# measured competitive baseline, and links to private repos that would 404 for
|
|
11
|
+
# everyone reading PyPI. What ships to a public index is the minimal one.
|
|
12
|
+
readme = "packaging/README-pypi.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
license = { file = "LICENSE" }
|
|
15
|
+
authors = [{ name = "OpenOnion", email = "info@openonion.ai" }]
|
|
16
|
+
keywords = ["playwright", "automation", "browser", "human", "input"]
|
|
17
|
+
|
|
18
|
+
# Proprietary. The trove classifier matters: PyPI shows it on the project page,
|
|
19
|
+
# and it is the difference between a developer assuming they can vendor this and
|
|
20
|
+
# knowing they cannot.
|
|
21
|
+
classifiers = [
|
|
22
|
+
"License :: Other/Proprietary License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Topic :: Software Development :: Testing",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
dependencies = ["playwright>=1.55"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://openonion.ai"
|
|
32
|
+
Documentation = "https://docs.connectonion.com"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/onionwright"]
|