@webhands/core 0.2.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/README.md +77 -0
- package/dist/errors.d.ts +24 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/playwright-launch-transport.d.ts +124 -1
- package/dist/playwright-launch-transport.d.ts.map +1 -1
- package/dist/playwright-launch-transport.js +127 -7
- package/dist/playwright-launch-transport.js.map +1 -1
- package/package.json +4 -1
- package/src/errors.ts +31 -0
- package/src/index.ts +6 -1
- package/src/playwright-launch-transport.ts +255 -6
package/README.md
CHANGED
|
@@ -97,6 +97,83 @@ deliberately local and single-session by design.
|
|
|
97
97
|
In short: this is for reading and acting on web apps **you already have an account
|
|
98
98
|
on**, from **your own browser**, the way you could by hand.
|
|
99
99
|
|
|
100
|
+
## Optional: stealth launch (opt-in, default OFF)
|
|
101
|
+
|
|
102
|
+
Standard Playwright drives Chromium over CDP and calls `Runtime.enable` at
|
|
103
|
+
startup. That emits a side-effect a few lines of page JS can detect, and some
|
|
104
|
+
anti-bot WAFs (Imperva/Cloudflare/DataDome) use it to serve an "Access Denied"
|
|
105
|
+
block page *before the page even renders* — even on a real residential IP, even
|
|
106
|
+
headed. `@webhands/core` can optionally launch via
|
|
107
|
+
[Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright) (an
|
|
108
|
+
API-compatible Playwright fork that patches exactly these CDP leaks) to remove
|
|
109
|
+
that one tell.
|
|
110
|
+
|
|
111
|
+
This is **off by default** — vanilla Playwright stays the default. To enable it:
|
|
112
|
+
|
|
113
|
+
1. Install the optional dependency (it is NOT pulled in unless you ask for it):
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
pnpm add patchright
|
|
117
|
+
# if you do NOT pass --use-system-browser chrome, also fetch its browser:
|
|
118
|
+
# pnpm exec patchright install chromium
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
2. Bring the session up with `--stealth`. The realistic recipe also drives your
|
|
122
|
+
installed system browser (`--use-system-browser chrome`), headed, against a
|
|
123
|
+
**warmed, logged-in profile**:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
# serve consumes these (it is where the browser is launched, ADR-0005):
|
|
127
|
+
npx webhands serve --headed --stealth --use-system-browser chrome
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`--use-system-browser` is independent of `--stealth`: you can drive real
|
|
131
|
+
Chrome with or without the Patchright path, and stealth with or without a
|
|
132
|
+
system browser. Other channel names work too (e.g. `msedge`).
|
|
133
|
+
|
|
134
|
+
3. Optional extra hardening. `--no-viewport` lets the real browser window drive
|
|
135
|
+
its own size instead of Playwright's fixed 1280x720 emulated viewport (a
|
|
136
|
+
known headless tell). It is **defaulted ON under `--stealth`** (Patchright's
|
|
137
|
+
recommended recipe) and is overridable; pass `--viewport` to keep the fixed
|
|
138
|
+
viewport even under stealth. webhands deliberately does **not** override
|
|
139
|
+
`user-agent`, `locale`, `timezone`, or `headers`: a wrong UA is a bigger tell
|
|
140
|
+
than none.
|
|
141
|
+
|
|
142
|
+
Programmatic equivalent (the `--stealth` / `--use-system-browser` /
|
|
143
|
+
`--no-viewport` flags map onto these transport options; the constructor also
|
|
144
|
+
takes `extraLaunchArgs` and `ignoreDefaultArgs` escape hatches for additional
|
|
145
|
+
hardening flags, none of which touch the `OpenTarget` seam):
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import {PlaywrightLaunchTransport} from '@webhands/core';
|
|
149
|
+
|
|
150
|
+
const transport = new PlaywrightLaunchTransport(
|
|
151
|
+
{}, // profile location (omit for ~/.webhands)
|
|
152
|
+
[], // extra hands
|
|
153
|
+
{stealth: true, systemBrowser: 'chrome'}, // noViewport defaults to true here
|
|
154
|
+
);
|
|
155
|
+
// Stealth + headed + a real logged-in profile is the strongest recipe:
|
|
156
|
+
const session = await transport.open({
|
|
157
|
+
mode: 'launch',
|
|
158
|
+
profile: 'default',
|
|
159
|
+
headed: true,
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
If stealth is enabled but `patchright` is not installed, the open throws a typed
|
|
164
|
+
`MissingStealthDependencyError` (the CLI prints `pnpm add patchright` as the fix).
|
|
165
|
+
It **never silently falls back** to vanilla Playwright, because that would put
|
|
166
|
+
the tell back without telling you.
|
|
167
|
+
|
|
168
|
+
**Honest caveat.** Stealth addresses ONLY the CDP `Runtime.enable` automation
|
|
169
|
+
tell, and the launch-hardening knobs (`--no-viewport`, `extraLaunchArgs`,
|
|
170
|
+
`ignoreDefaultArgs`) reduce but do **not** eliminate detection. They are
|
|
171
|
+
**necessary-but-not-sufficient**: IP reputation and session/profile
|
|
172
|
+
reputation still matter. The realistic recipe is stealth +
|
|
173
|
+
`systemBrowser: 'chrome'` + headed + a warmed, logged-in profile + a residential
|
|
174
|
+
IP (see
|
|
175
|
+
[`docs/adr/0002`](docs/adr/0002-real-session-over-fingerprint-spoofing.md)).
|
|
176
|
+
|
|
100
177
|
## Security note (the `serve` endpoint runs arbitrary code)
|
|
101
178
|
|
|
102
179
|
The page verbs execute caller-supplied expressions: `eval` runs a JS expression
|
package/dist/errors.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* has to re-derive paths.
|
|
16
16
|
*/
|
|
17
17
|
/** The closed set of identifiable `core` error conditions. */
|
|
18
|
-
export type ControllerErrorCode = 'missing-browser-binary' | 'missing-profile' | 'attach-not-chromium' | 'attach-no-context' | 'no-live-server' | 'session-already-active';
|
|
18
|
+
export type ControllerErrorCode = 'missing-browser-binary' | 'missing-stealth-dependency' | 'missing-profile' | 'attach-not-chromium' | 'attach-no-context' | 'no-live-server' | 'session-already-active';
|
|
19
19
|
/**
|
|
20
20
|
* Base class for every identifiable `core` error. Branch on {@link code}.
|
|
21
21
|
*
|
|
@@ -45,6 +45,29 @@ export declare class MissingBrowserBinaryError extends ControllerError {
|
|
|
45
45
|
cause?: unknown;
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Stealth launch was REQUESTED (the opt-in is on) but the optional `patchright`
|
|
50
|
+
* dependency is not installed/importable. Patchright is an OPTIONAL dependency
|
|
51
|
+
* of `@webhands/core` imported lazily only when stealth is enabled, so a user
|
|
52
|
+
* who never opts in is not forced to install it (ADR-0002: stealth is one extra
|
|
53
|
+
* layer, not the default). When it IS opted into and missing, we refuse LOUDLY
|
|
54
|
+
* with this typed condition rather than silently falling back to vanilla
|
|
55
|
+
* Playwright, because a silent fallback would re-introduce the exact CDP
|
|
56
|
+
* automation tell the user asked us to remove WITHOUT telling them.
|
|
57
|
+
*
|
|
58
|
+
* Mirrors {@link MissingBrowserBinaryError}: a stable typed error whose brittle
|
|
59
|
+
* detection (the dynamic-import failure) is confined to one spot in the launch
|
|
60
|
+
* transport. The CLI can render the exact `pnpm add patchright` fix command by
|
|
61
|
+
* branching on {@link code}.
|
|
62
|
+
*/
|
|
63
|
+
export declare class MissingStealthDependencyError extends ControllerError {
|
|
64
|
+
readonly code = "missing-stealth-dependency";
|
|
65
|
+
/** The optional package that must be installed to use stealth. */
|
|
66
|
+
readonly dependency: string;
|
|
67
|
+
constructor(dependency?: string, message?: string, options?: {
|
|
68
|
+
cause?: unknown;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
48
71
|
/**
|
|
49
72
|
* The named profile has not been set up yet: its dedicated profile directory
|
|
50
73
|
* does not exist on disk. A profile is created by the headed `setup-profile`
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,8DAA8D;AAC9D,MAAM,MAAM,mBAAmB,GAC5B,wBAAwB,GACxB,iBAAiB,GACjB,qBAAqB,GACrB,mBAAmB,GACnB,gBAAgB,GAChB,wBAAwB,CAAC;AAE5B;;;;;;GAMG;AACH,8BAAsB,eAAgB,SAAQ,KAAK;IAClD,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAC5C,8EAA8E;IAC9E,QAAQ,CAAC,iBAAiB,EAAG,IAAI,CAAU;gBAE/B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAMxD;AAED;;;;GAIG;AACH,qBAAa,yBAA0B,SAAQ,eAAe;IAC7D,QAAQ,CAAC,IAAI,4BAA4B;IACzC,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAGxB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,MAA0D,EACnE,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAK5B;AAED;;;;;GAKG;AACH,qBAAa,mBAAoB,SAAQ,eAAe;IACvD,QAAQ,CAAC,IAAI,qBAAqB;IAClC,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAG3B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,MAA0F,EACnG,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAM5B;AAED;;;;;;GAMG;AACH,qBAAa,sBAAuB,SAAQ,eAAe;IAC1D,QAAQ,CAAC,IAAI,yBAAyB;IACtC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAGxB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,MAAuJ,EAChK,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAK5B;AAED;;;;;GAKG;AACH,qBAAa,oBAAqB,SAAQ,eAAe;IACxD,QAAQ,CAAC,IAAI,uBAAuB;IACpC,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAGzB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,MAA+H,EACxI,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAK5B;AAED;;;;;;;GAOG;AACH,qBAAa,iBAAkB,SAAQ,eAAe;IACrD,QAAQ,CAAC,IAAI,oBAAoB;gBAGhC,OAAO,GAAE,MAAoF,EAC7F,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAI5B;AAED;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,eAAe;IAC7D,QAAQ,CAAC,IAAI,4BAA4B;gBAGxC,OAAO,GAAE,MAAmE,EAC5E,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAI5B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAO1E"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,8DAA8D;AAC9D,MAAM,MAAM,mBAAmB,GAC5B,wBAAwB,GACxB,4BAA4B,GAC5B,iBAAiB,GACjB,qBAAqB,GACrB,mBAAmB,GACnB,gBAAgB,GAChB,wBAAwB,CAAC;AAE5B;;;;;;GAMG;AACH,8BAAsB,eAAgB,SAAQ,KAAK;IAClD,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAC5C,8EAA8E;IAC9E,QAAQ,CAAC,iBAAiB,EAAG,IAAI,CAAU;gBAE/B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAMxD;AAED;;;;GAIG;AACH,qBAAa,yBAA0B,SAAQ,eAAe;IAC7D,QAAQ,CAAC,IAAI,4BAA4B;IACzC,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAGxB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,MAA0D,EACnE,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAK5B;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,6BAA8B,SAAQ,eAAe;IACjE,QAAQ,CAAC,IAAI,gCAAgC;IAC7C,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAG3B,UAAU,SAAe,EACzB,OAAO,GAAE,MAA+Q,EACxR,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAK5B;AAED;;;;;GAKG;AACH,qBAAa,mBAAoB,SAAQ,eAAe;IACvD,QAAQ,CAAC,IAAI,qBAAqB;IAClC,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAG3B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,MAA0F,EACnG,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAM5B;AAED;;;;;;GAMG;AACH,qBAAa,sBAAuB,SAAQ,eAAe;IAC1D,QAAQ,CAAC,IAAI,yBAAyB;IACtC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAGxB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,MAAuJ,EAChK,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAK5B;AAED;;;;;GAKG;AACH,qBAAa,oBAAqB,SAAQ,eAAe;IACxD,QAAQ,CAAC,IAAI,uBAAuB;IACpC,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAGzB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,MAA+H,EACxI,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAK5B;AAED;;;;;;;GAOG;AACH,qBAAa,iBAAkB,SAAQ,eAAe;IACrD,QAAQ,CAAC,IAAI,oBAAoB;gBAGhC,OAAO,GAAE,MAAoF,EAC7F,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAI5B;AAED;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,eAAe;IAC7D,QAAQ,CAAC,IAAI,4BAA4B;gBAGxC,OAAO,GAAE,MAAmE,EAC5E,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAC;CAI5B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAO1E"}
|
package/dist/errors.js
CHANGED
|
@@ -45,6 +45,30 @@ export class MissingBrowserBinaryError extends ControllerError {
|
|
|
45
45
|
this.browser = browser;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Stealth launch was REQUESTED (the opt-in is on) but the optional `patchright`
|
|
50
|
+
* dependency is not installed/importable. Patchright is an OPTIONAL dependency
|
|
51
|
+
* of `@webhands/core` imported lazily only when stealth is enabled, so a user
|
|
52
|
+
* who never opts in is not forced to install it (ADR-0002: stealth is one extra
|
|
53
|
+
* layer, not the default). When it IS opted into and missing, we refuse LOUDLY
|
|
54
|
+
* with this typed condition rather than silently falling back to vanilla
|
|
55
|
+
* Playwright, because a silent fallback would re-introduce the exact CDP
|
|
56
|
+
* automation tell the user asked us to remove WITHOUT telling them.
|
|
57
|
+
*
|
|
58
|
+
* Mirrors {@link MissingBrowserBinaryError}: a stable typed error whose brittle
|
|
59
|
+
* detection (the dynamic-import failure) is confined to one spot in the launch
|
|
60
|
+
* transport. The CLI can render the exact `pnpm add patchright` fix command by
|
|
61
|
+
* branching on {@link code}.
|
|
62
|
+
*/
|
|
63
|
+
export class MissingStealthDependencyError extends ControllerError {
|
|
64
|
+
code = 'missing-stealth-dependency';
|
|
65
|
+
/** The optional package that must be installed to use stealth. */
|
|
66
|
+
dependency;
|
|
67
|
+
constructor(dependency = 'patchright', message = `Stealth launch is enabled but the optional "${dependency}" dependency is not installed. Install it with \`pnpm add ${dependency}\` (and \`${dependency} install chromium\` if you do not use channel: 'chrome'), or construct the transport without {stealth: true}.`, options) {
|
|
68
|
+
super(message, options);
|
|
69
|
+
this.dependency = dependency;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
48
72
|
/**
|
|
49
73
|
* The named profile has not been set up yet: its dedicated profile directory
|
|
50
74
|
* does not exist on disk. A profile is created by the headed `setup-profile`
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,10 +4,10 @@ export { serializeCookies, deserializeCookies, COOKIES_EXPORT_VERSION, type Cook
|
|
|
4
4
|
export { StubTransport, type StubCall } from './stub-transport.js';
|
|
5
5
|
export type { Hand, HandContext, HandContribution } from './hand-host.js';
|
|
6
6
|
export { readHandsConfig, normalizeConfig, loadHands, HandLoadError, HANDS_CONFIG_FILENAME, type HandEntry, type HandsConfig, type LoadedHand, type LoadHandsOptions, } from './hand-loading.js';
|
|
7
|
-
export { PlaywrightLaunchTransport } from './playwright-launch-transport.js';
|
|
7
|
+
export { PlaywrightLaunchTransport, type PlaywrightLaunchTransportOptions, type StealthChromiumImporter, } from './playwright-launch-transport.js';
|
|
8
8
|
export { PlaywrightAttachTransport } from './playwright-attach-transport.js';
|
|
9
9
|
export { setupProfile, buildPrompt, type PromptSink, type SetupProfileOptions, type SetupProfileResult, } from './setup-profile.js';
|
|
10
|
-
export { ControllerError, MissingBrowserBinaryError, MissingProfileError, AttachNotChromiumError, AttachNoContextError, NoLiveServerError, SessionAlreadyActiveError, isControllerError, type ControllerErrorCode, } from './errors.js';
|
|
10
|
+
export { ControllerError, MissingBrowserBinaryError, MissingStealthDependencyError, MissingProfileError, AttachNotChromiumError, AttachNoContextError, NoLiveServerError, SessionAlreadyActiveError, isControllerError, type ControllerErrorCode, } from './errors.js';
|
|
11
11
|
export { resolveSessionEndpointPath, writeSessionEndpoint, readSessionEndpoint, clearSessionEndpoint, SESSION_ENDPOINT_FILENAME, type SessionEndpoint, } from './session-endpoint.js';
|
|
12
12
|
export { startSessionServer, sessionAlreadyActive, type SessionServerOptions, type RunningSessionServer, } from './session-server.js';
|
|
13
13
|
export { connectRemoteSession } from './remote-session.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,MAAM,EACN,MAAM,EACN,aAAa,EACb,UAAU,EACV,YAAY,EACZ,OAAO,EACP,QAAQ,EACR,eAAe,EACf,YAAY,EACZ,SAAS,EACT,aAAa,GACb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AAElC,OAAO,EACN,gBAAgB,EAChB,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,aAAa,GAClB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAC,aAAa,EAAE,KAAK,QAAQ,EAAC,MAAM,qBAAqB,CAAC;AAEjE,YAAY,EAAC,IAAI,EAAE,WAAW,EAAE,gBAAgB,EAAC,MAAM,gBAAgB,CAAC;AAExE,OAAO,EACN,eAAe,EACf,eAAe,EACf,SAAS,EACT,aAAa,EACb,qBAAqB,EACrB,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,gBAAgB,GACrB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,MAAM,EACN,MAAM,EACN,aAAa,EACb,UAAU,EACV,YAAY,EACZ,OAAO,EACP,QAAQ,EACR,eAAe,EACf,YAAY,EACZ,SAAS,EACT,aAAa,GACb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AAElC,OAAO,EACN,gBAAgB,EAChB,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,aAAa,GAClB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAC,aAAa,EAAE,KAAK,QAAQ,EAAC,MAAM,qBAAqB,CAAC;AAEjE,YAAY,EAAC,IAAI,EAAE,WAAW,EAAE,gBAAgB,EAAC,MAAM,gBAAgB,CAAC;AAExE,OAAO,EACN,eAAe,EACf,eAAe,EACf,SAAS,EACT,aAAa,EACb,qBAAqB,EACrB,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,gBAAgB,GACrB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACN,yBAAyB,EACzB,KAAK,gCAAgC,EACrC,KAAK,uBAAuB,GAC5B,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EAAC,yBAAyB,EAAC,MAAM,kCAAkC,CAAC;AAE3E,OAAO,EACN,YAAY,EACZ,WAAW,EACX,KAAK,UAAU,EACf,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,GACvB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACN,eAAe,EACf,yBAAyB,EACzB,6BAA6B,EAC7B,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,EACzB,iBAAiB,EACjB,KAAK,mBAAmB,GACxB,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,KAAK,eAAe,GACpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACN,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GACzB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAC,oBAAoB,EAAC,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EACN,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,GACvB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACN,eAAe,EACf,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,sBAAsB,GAC3B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACN,kBAAkB,EAClB,KAAK,aAAa,GAClB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAC,aAAa,EAAC,MAAM,kCAAkC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@ export { locator } from './seam.js';
|
|
|
2
2
|
export { serializeCookies, deserializeCookies, COOKIES_EXPORT_VERSION, } from './cookies-export.js';
|
|
3
3
|
export { StubTransport } from './stub-transport.js';
|
|
4
4
|
export { readHandsConfig, normalizeConfig, loadHands, HandLoadError, HANDS_CONFIG_FILENAME, } from './hand-loading.js';
|
|
5
|
-
export { PlaywrightLaunchTransport } from './playwright-launch-transport.js';
|
|
5
|
+
export { PlaywrightLaunchTransport, } from './playwright-launch-transport.js';
|
|
6
6
|
export { PlaywrightAttachTransport } from './playwright-attach-transport.js';
|
|
7
7
|
export { setupProfile, buildPrompt, } from './setup-profile.js';
|
|
8
|
-
export { ControllerError, MissingBrowserBinaryError, MissingProfileError, AttachNotChromiumError, AttachNoContextError, NoLiveServerError, SessionAlreadyActiveError, isControllerError, } from './errors.js';
|
|
8
|
+
export { ControllerError, MissingBrowserBinaryError, MissingStealthDependencyError, MissingProfileError, AttachNotChromiumError, AttachNoContextError, NoLiveServerError, SessionAlreadyActiveError, isControllerError, } from './errors.js';
|
|
9
9
|
export { resolveSessionEndpointPath, writeSessionEndpoint, readSessionEndpoint, clearSessionEndpoint, SESSION_ENDPOINT_FILENAME, } from './session-endpoint.js';
|
|
10
10
|
export { startSessionServer, sessionAlreadyActive, } from './session-server.js';
|
|
11
11
|
export { connectRemoteSession } from './remote-session.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AAElC,OAAO,EACN,gBAAgB,EAChB,kBAAkB,EAClB,sBAAsB,GAEtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAC,aAAa,EAAgB,MAAM,qBAAqB,CAAC;AAIjE,OAAO,EACN,eAAe,EACf,eAAe,EACf,SAAS,EACT,aAAa,EACb,qBAAqB,GAKrB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AAElC,OAAO,EACN,gBAAgB,EAChB,kBAAkB,EAClB,sBAAsB,GAEtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAC,aAAa,EAAgB,MAAM,qBAAqB,CAAC;AAIjE,OAAO,EACN,eAAe,EACf,eAAe,EACf,SAAS,EACT,aAAa,EACb,qBAAqB,GAKrB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACN,yBAAyB,GAGzB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EAAC,yBAAyB,EAAC,MAAM,kCAAkC,CAAC;AAE3E,OAAO,EACN,YAAY,EACZ,WAAW,GAIX,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACN,eAAe,EACf,yBAAyB,EACzB,6BAA6B,EAC7B,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,EACzB,iBAAiB,GAEjB,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,GAEzB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACN,kBAAkB,EAClB,oBAAoB,GAGpB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAC,oBAAoB,EAAC,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EACN,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,YAAY,GAKZ,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACN,eAAe,EACf,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,GAGhB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACN,kBAAkB,GAElB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAC,aAAa,EAAC,MAAM,kCAAkC,CAAC"}
|
|
@@ -1,6 +1,115 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
1
2
|
import { type Hand } from './hand-host.js';
|
|
2
3
|
import { type ProfileLocationOptions } from './profile-location.js';
|
|
3
4
|
import type { OpenTarget, Session, Transport } from './seam.js';
|
|
5
|
+
/**
|
|
6
|
+
* The subset of Playwright's `chromium` browser type the launch transport uses.
|
|
7
|
+
*
|
|
8
|
+
* Patchright is an API-compatible Playwright fork, so its `chromium` has the
|
|
9
|
+
* SAME shape (ADR-0003 stays intact: this structural type, like Playwright's
|
|
10
|
+
* own types, is confined to this module and never crosses the seam). We type the
|
|
11
|
+
* lazily-imported stealth chromium against THIS rather than importing any
|
|
12
|
+
* Patchright type, so the dependency stays optional at the type level too.
|
|
13
|
+
*/
|
|
14
|
+
type ChromiumLauncher = Pick<typeof chromium, 'launchPersistentContext'>;
|
|
15
|
+
/** The shape `await import('patchright')` is expected to expose. */
|
|
16
|
+
interface StealthModule {
|
|
17
|
+
readonly chromium: ChromiumLauncher;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* How the transport obtains the stealth (`patchright`) chromium. This is an
|
|
21
|
+
* INTERNAL test seam, not a public API: tests inject a fake module (or a
|
|
22
|
+
* rejecting importer) here so no real browser/Patchright is needed, exactly as
|
|
23
|
+
* production uses the default lazy `import('patchright')`. It is deliberately
|
|
24
|
+
* NOT on {@link OpenTarget} (ADR-0003: the seam stays free of Playwright/CDP/
|
|
25
|
+
* Patchright concerns).
|
|
26
|
+
*/
|
|
27
|
+
export type StealthChromiumImporter = () => Promise<StealthModule>;
|
|
28
|
+
/**
|
|
29
|
+
* Construction-time policy for {@link PlaywrightLaunchTransport}.
|
|
30
|
+
*
|
|
31
|
+
* Stealth is a TRANSPORT-CONSTRUCTION policy (which browser engine + launch
|
|
32
|
+
* flags to use), not a per-open target detail, so it lives here and NOT on
|
|
33
|
+
* {@link OpenTarget} (which stays Playwright/CDP-free per ADR-0003).
|
|
34
|
+
*/
|
|
35
|
+
export interface PlaywrightLaunchTransportOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Opt-in Patchright-backed stealth launch. Default `false` (vanilla
|
|
38
|
+
* Playwright). When `true`, the transport launches via the lazily-imported
|
|
39
|
+
* optional `patchright` package, which patches the CDP `Runtime.enable`
|
|
40
|
+
* automation tell that anti-bot WAFs detect (ADR-0002 keeps this as one extra
|
|
41
|
+
* layer, not a replacement for a real profile/IP). If `patchright` is not
|
|
42
|
+
* installed it throws {@link MissingStealthDependencyError}; it NEVER silently
|
|
43
|
+
* falls back to vanilla.
|
|
44
|
+
*/
|
|
45
|
+
readonly stealth?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Drive a browser ALREADY INSTALLED ON THE SYSTEM instead of the bundled
|
|
48
|
+
* Chromium, named by its install identity (e.g. `'chrome'` to drive the system
|
|
49
|
+
* Google Chrome, Patchright's recommended setup; also `'msedge'`,
|
|
50
|
+
* `'chrome-beta'`, ...). Applies to BOTH stealth and vanilla launches when set.
|
|
51
|
+
* When omitted, Playwright/Patchright's bundled Chromium is used.
|
|
52
|
+
*
|
|
53
|
+
* Maps to Playwright's `channel` launch option internally; we name it
|
|
54
|
+
* `systemBrowser` so the public surface speaks domain language ("use a browser
|
|
55
|
+
* I already have installed") rather than the Playwright term (ADR-0003 keeps
|
|
56
|
+
* Playwright vocabulary out of the public surface).
|
|
57
|
+
*/
|
|
58
|
+
readonly systemBrowser?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Don't impose a fixed emulated viewport: let the browser window drive its own
|
|
61
|
+
* size, exactly as a real user's browser does. Maps to Playwright's
|
|
62
|
+
* `viewport: null` on the persistent context.
|
|
63
|
+
*
|
|
64
|
+
* Why this matters for hardening: Playwright's DEFAULT is a fixed 1280x720
|
|
65
|
+
* emulated viewport that does NOT match the real OS window, a discrepancy
|
|
66
|
+
* (e.g. `window.outerWidth`/`innerWidth`/`screen` mismatches, no real resize
|
|
67
|
+
* behaviour) that fingerprinting scripts read as a headless/automation tell.
|
|
68
|
+
* Patchright's official recommended recipe sets `no_viewport=True` for this
|
|
69
|
+
* reason.
|
|
70
|
+
*
|
|
71
|
+
* Default: `undefined` leaves Playwright's behaviour as-is, EXCEPT that when
|
|
72
|
+
* {@link stealth} is enabled it defaults to `true` (the Patchright recipe).
|
|
73
|
+
* Pass an explicit `false` to keep the fixed emulated viewport even under
|
|
74
|
+
* stealth (e.g. when a caller deliberately wants a deterministic size). We pick
|
|
75
|
+
* the stealth-on default because shipping the stealth engine while leaving the
|
|
76
|
+
* tell it is meant to hide in place would be self-defeating; making it an
|
|
77
|
+
* explicit, overridable default keeps that honest and discoverable.
|
|
78
|
+
*/
|
|
79
|
+
readonly noViewport?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Extra command-line args appended to the browser launch (Playwright's
|
|
82
|
+
* `args`). An escape hatch for well-known hardening flags Patchright/Chromium
|
|
83
|
+
* users pass (e.g. `--disable-blink-features=AutomationControlled`) WITHOUT
|
|
84
|
+
* leaking a Playwright type across the seam: this is a plain `string[]`, kept
|
|
85
|
+
* confined to this transport-construction policy and deliberately NOT on
|
|
86
|
+
* {@link OpenTarget} (ADR-0003). Default: none.
|
|
87
|
+
*
|
|
88
|
+
* Caveat: args are passed THROUGH verbatim; a wrong or contradictory flag can
|
|
89
|
+
* itself become a tell or break the launch. Opt-in only.
|
|
90
|
+
*/
|
|
91
|
+
readonly extraLaunchArgs?: readonly string[];
|
|
92
|
+
/**
|
|
93
|
+
* Passthrough for Playwright's `ignoreDefaultArgs`: either `true` to drop ALL
|
|
94
|
+
* of Playwright's default launch args, or a list of specific default args to
|
|
95
|
+
* drop, so a caller can strip more automation-flavoured defaults than the
|
|
96
|
+
* built-in stealth subset.
|
|
97
|
+
*
|
|
98
|
+
* When omitted, the stealth path still drops `--enable-automation` on its own
|
|
99
|
+
* (unchanged behaviour). When provided, this value REPLACES that built-in
|
|
100
|
+
* choice, so a caller opting in owns the full list (pass
|
|
101
|
+
* `['--enable-automation', ...]` to keep it). Like {@link extraLaunchArgs}
|
|
102
|
+
* this is a plain value confined to this module, never on {@link OpenTarget}.
|
|
103
|
+
* Default: none.
|
|
104
|
+
*/
|
|
105
|
+
readonly ignoreDefaultArgs?: boolean | readonly string[];
|
|
106
|
+
/**
|
|
107
|
+
* INTERNAL test seam: override how the stealth chromium is imported. Omit in
|
|
108
|
+
* production (defaults to `import('patchright')`). See
|
|
109
|
+
* {@link StealthChromiumImporter}.
|
|
110
|
+
*/
|
|
111
|
+
readonly importStealthChromium?: StealthChromiumImporter;
|
|
112
|
+
}
|
|
4
113
|
/**
|
|
5
114
|
* The v1 concrete transport: a Playwright browser the controller LAUNCHES
|
|
6
115
|
* against a dedicated, persistent profile directory it owns (PRD "Solution,
|
|
@@ -17,6 +126,13 @@ import type { OpenTarget, Session, Transport } from './seam.js';
|
|
|
17
126
|
* `WEBHANDS_HOME` env var, or `~/.webhands`). See
|
|
18
127
|
* {@link resolveProfileLocation}. Because that is a SHARED location, tests pass
|
|
19
128
|
* a temp `root` (or set the env var) and assert the real home is untouched.
|
|
129
|
+
*
|
|
130
|
+
* STEALTH (opt-in, default OFF): the third constructor arg can enable a
|
|
131
|
+
* Patchright-backed launch ({@link PlaywrightLaunchTransportOptions}). Patchright
|
|
132
|
+
* is an OPTIONAL dependency imported lazily only when stealth is enabled; if it
|
|
133
|
+
* is absent the transport throws {@link MissingStealthDependencyError} rather
|
|
134
|
+
* than falling back to vanilla. This addresses ONLY the CDP `Runtime.enable`
|
|
135
|
+
* automation tell; a real profile/IP/session reputation still matter (ADR-0002).
|
|
20
136
|
*/
|
|
21
137
|
export declare class PlaywrightLaunchTransport implements Transport {
|
|
22
138
|
#private;
|
|
@@ -28,8 +144,15 @@ export declare class PlaywrightLaunchTransport implements Transport {
|
|
|
28
144
|
* built-ins (Phase 2, ADR-0007). These come from {@link loadHands} against
|
|
29
145
|
* the operator's explicit config; the transport does NOT discover them. Omit
|
|
30
146
|
* for the built-ins-only surface.
|
|
147
|
+
* @param options transport-construction policy, notably the opt-in `stealth`
|
|
148
|
+
* toggle, optional `systemBrowser`, and the launch-hardening knobs
|
|
149
|
+
* (`noViewport`, `extraLaunchArgs`, `ignoreDefaultArgs`; see
|
|
150
|
+
* {@link PlaywrightLaunchTransportOptions}). Defaults to vanilla Playwright,
|
|
151
|
+
* bundled Chromium, stealth OFF. The hardening knobs are confined to this
|
|
152
|
+
* module and never reach {@link OpenTarget} (ADR-0003).
|
|
31
153
|
*/
|
|
32
|
-
constructor(location?: ProfileLocationOptions, hands?: readonly Hand[]);
|
|
154
|
+
constructor(location?: ProfileLocationOptions, hands?: readonly Hand[], options?: PlaywrightLaunchTransportOptions);
|
|
33
155
|
open(target: OpenTarget): Promise<Session>;
|
|
34
156
|
}
|
|
157
|
+
export {};
|
|
35
158
|
//# sourceMappingURL=playwright-launch-transport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playwright-launch-transport.d.ts","sourceRoot":"","sources":["../src/playwright-launch-transport.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"playwright-launch-transport.d.ts","sourceRoot":"","sources":["../src/playwright-launch-transport.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,QAAQ,EAAiC,MAAM,YAAY,CAAC;AAMpE,OAAO,EAAmB,KAAK,IAAI,EAAmB,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EAEN,KAAK,sBAAsB,EAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAC,UAAU,EAAE,OAAO,EAAE,SAAS,EAAC,MAAM,WAAW,CAAC;AAE9D;;;;;;;;GAQG;AACH,KAAK,gBAAgB,GAAG,IAAI,CAAC,OAAO,QAAQ,EAAE,yBAAyB,CAAC,CAAC;AAEzE,oEAAoE;AACpE,UAAU,aAAa;IACtB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,uBAAuB,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;AAEnE;;;;;;GAMG;AACH,MAAM,WAAW,gCAAgC;IAChD;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CAAC;IACzD;;;;OAIG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,uBAAuB,CAAC;CACzD;AAmBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,yBAA0B,YAAW,SAAS;;IAU1D;;;;;;;;;;;;;;OAcG;gBAEF,QAAQ,GAAE,sBAA2B,EACrC,KAAK,GAAE,SAAS,IAAI,EAAO,EAC3B,OAAO,GAAE,gCAAqC;IAazC,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC;CAyHhD"}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
2
|
import { chromium } from 'playwright';
|
|
3
|
-
import { MissingBrowserBinaryError, MissingProfileError } from './errors.js';
|
|
3
|
+
import { MissingBrowserBinaryError, MissingProfileError, MissingStealthDependencyError, } from './errors.js';
|
|
4
4
|
import { composeWithHands } from './hand-host.js';
|
|
5
5
|
import { resolveProfileLocation, } from './profile-location.js';
|
|
6
|
+
/**
|
|
7
|
+
* The package name of the optional stealth dependency. Kept as a runtime value
|
|
8
|
+
* (not an `import('patchright')` literal) so TypeScript does NOT try to resolve
|
|
9
|
+
* its types at build time, since it is an OPTIONAL dependency that is legitimately
|
|
10
|
+
* absent when stealth is never enabled.
|
|
11
|
+
*/
|
|
12
|
+
const STEALTH_PACKAGE = 'patchright';
|
|
13
|
+
/** The default lazy import of the OPTIONAL `patchright` dependency. */
|
|
14
|
+
const defaultStealthImporter = async () => {
|
|
15
|
+
// Indirect (non-literal specifier) so tsc/bundlers do not resolve the
|
|
16
|
+
// optional dep eagerly, and the module load never fails when it is absent;
|
|
17
|
+
// the import only runs when stealth is opted in.
|
|
18
|
+
const specifier = STEALTH_PACKAGE;
|
|
19
|
+
return (await import(specifier));
|
|
20
|
+
};
|
|
6
21
|
/**
|
|
7
22
|
* The v1 concrete transport: a Playwright browser the controller LAUNCHES
|
|
8
23
|
* against a dedicated, persistent profile directory it owns (PRD "Solution,
|
|
@@ -19,10 +34,23 @@ import { resolveProfileLocation, } from './profile-location.js';
|
|
|
19
34
|
* `WEBHANDS_HOME` env var, or `~/.webhands`). See
|
|
20
35
|
* {@link resolveProfileLocation}. Because that is a SHARED location, tests pass
|
|
21
36
|
* a temp `root` (or set the env var) and assert the real home is untouched.
|
|
37
|
+
*
|
|
38
|
+
* STEALTH (opt-in, default OFF): the third constructor arg can enable a
|
|
39
|
+
* Patchright-backed launch ({@link PlaywrightLaunchTransportOptions}). Patchright
|
|
40
|
+
* is an OPTIONAL dependency imported lazily only when stealth is enabled; if it
|
|
41
|
+
* is absent the transport throws {@link MissingStealthDependencyError} rather
|
|
42
|
+
* than falling back to vanilla. This addresses ONLY the CDP `Runtime.enable`
|
|
43
|
+
* automation tell; a real profile/IP/session reputation still matter (ADR-0002).
|
|
22
44
|
*/
|
|
23
45
|
export class PlaywrightLaunchTransport {
|
|
24
46
|
#location;
|
|
25
47
|
#hands;
|
|
48
|
+
#stealth;
|
|
49
|
+
#systemBrowser;
|
|
50
|
+
#noViewport;
|
|
51
|
+
#extraLaunchArgs;
|
|
52
|
+
#ignoreDefaultArgs;
|
|
53
|
+
#importStealthChromium;
|
|
26
54
|
/**
|
|
27
55
|
* @param location overrides for where profiles live (a `root` dir and/or an
|
|
28
56
|
* `env`). Omit in production to use `~/.webhands`; pass a temp
|
|
@@ -31,10 +59,23 @@ export class PlaywrightLaunchTransport {
|
|
|
31
59
|
* built-ins (Phase 2, ADR-0007). These come from {@link loadHands} against
|
|
32
60
|
* the operator's explicit config; the transport does NOT discover them. Omit
|
|
33
61
|
* for the built-ins-only surface.
|
|
62
|
+
* @param options transport-construction policy, notably the opt-in `stealth`
|
|
63
|
+
* toggle, optional `systemBrowser`, and the launch-hardening knobs
|
|
64
|
+
* (`noViewport`, `extraLaunchArgs`, `ignoreDefaultArgs`; see
|
|
65
|
+
* {@link PlaywrightLaunchTransportOptions}). Defaults to vanilla Playwright,
|
|
66
|
+
* bundled Chromium, stealth OFF. The hardening knobs are confined to this
|
|
67
|
+
* module and never reach {@link OpenTarget} (ADR-0003).
|
|
34
68
|
*/
|
|
35
|
-
constructor(location = {}, hands = []) {
|
|
69
|
+
constructor(location = {}, hands = [], options = {}) {
|
|
36
70
|
this.#location = location;
|
|
37
71
|
this.#hands = hands;
|
|
72
|
+
this.#stealth = options.stealth === true;
|
|
73
|
+
this.#systemBrowser = options.systemBrowser;
|
|
74
|
+
this.#noViewport = options.noViewport;
|
|
75
|
+
this.#extraLaunchArgs = options.extraLaunchArgs;
|
|
76
|
+
this.#ignoreDefaultArgs = options.ignoreDefaultArgs;
|
|
77
|
+
this.#importStealthChromium =
|
|
78
|
+
options.importStealthChromium ?? defaultStealthImporter;
|
|
38
79
|
}
|
|
39
80
|
async open(target) {
|
|
40
81
|
if (target.mode !== 'launch') {
|
|
@@ -52,15 +93,61 @@ export class PlaywrightLaunchTransport {
|
|
|
52
93
|
throw new MissingProfileError(loc.profile, loc.profileDir);
|
|
53
94
|
}
|
|
54
95
|
const headless = target.headed !== true;
|
|
96
|
+
// Pick the engine: the lazily-imported stealth (Patchright) chromium when
|
|
97
|
+
// opted in, else vanilla Playwright's. Resolving the stealth module is where
|
|
98
|
+
// an absent optional dependency surfaces as the typed
|
|
99
|
+
// MissingStealthDependencyError (we never fall back to vanilla silently).
|
|
100
|
+
const launcher = this.#stealth
|
|
101
|
+
? await this.#resolveStealthLauncher()
|
|
102
|
+
: chromium;
|
|
103
|
+
// Launch options: forward headless, the optional systemBrowser (Playwright's
|
|
104
|
+
// `channel`, e.g. 'chrome' to drive system Chrome, Patchright's recommended
|
|
105
|
+
// setup), and for stealth drop Playwright's automation-flavoured default
|
|
106
|
+
// args so they cannot re-add the fingerprint Patchright just removed.
|
|
107
|
+
const launchOptions = { headless };
|
|
108
|
+
if (this.#systemBrowser !== undefined) {
|
|
109
|
+
launchOptions.channel = this.#systemBrowser;
|
|
110
|
+
}
|
|
111
|
+
// no_viewport: explicit caller choice wins; otherwise default to TRUE under
|
|
112
|
+
// stealth (Patchright's recommended recipe), and leave Playwright's default
|
|
113
|
+
// fixed viewport in place when stealth is off. `viewport: null` is how
|
|
114
|
+
// Playwright expresses "let the real window drive the size".
|
|
115
|
+
const noViewport = this.#noViewport ?? this.#stealth;
|
|
116
|
+
if (noViewport) {
|
|
117
|
+
launchOptions.viewport = null;
|
|
118
|
+
}
|
|
119
|
+
// ignoreDefaultArgs: an explicit passthrough REPLACES the built-in stealth
|
|
120
|
+
// choice (the caller then owns the full list). With no passthrough, the
|
|
121
|
+
// stealth path keeps dropping just `--enable-automation` so it cannot re-add
|
|
122
|
+
// the fingerprint Patchright just removed.
|
|
123
|
+
if (this.#ignoreDefaultArgs !== undefined) {
|
|
124
|
+
launchOptions.ignoreDefaultArgs =
|
|
125
|
+
typeof this.#ignoreDefaultArgs === 'boolean'
|
|
126
|
+
? this.#ignoreDefaultArgs
|
|
127
|
+
: [...this.#ignoreDefaultArgs];
|
|
128
|
+
}
|
|
129
|
+
else if (this.#stealth) {
|
|
130
|
+
launchOptions.ignoreDefaultArgs = ['--enable-automation'];
|
|
131
|
+
}
|
|
132
|
+
// Extra launch args (the hardening escape hatch) are appended verbatim. We do
|
|
133
|
+
// NOT set user-agent/locale/timezone/headers here: a wrong UA is a bigger
|
|
134
|
+
// tell than none (Patchright warns against overriding them), so those stay
|
|
135
|
+
// untouched by default.
|
|
136
|
+
if (this.#extraLaunchArgs !== undefined &&
|
|
137
|
+
this.#extraLaunchArgs.length > 0) {
|
|
138
|
+
launchOptions.args = [...this.#extraLaunchArgs];
|
|
139
|
+
}
|
|
55
140
|
let context;
|
|
56
141
|
try {
|
|
57
|
-
context = await
|
|
58
|
-
headless,
|
|
59
|
-
});
|
|
142
|
+
context = await launcher.launchPersistentContext(loc.profileDir, launchOptions);
|
|
60
143
|
}
|
|
61
144
|
catch (cause) {
|
|
62
145
|
if (isMissingBrowserBinary(cause)) {
|
|
63
|
-
|
|
146
|
+
// With systemBrowser set (e.g. 'chrome') the "binary missing" failure
|
|
147
|
+
// means the SYSTEM browser is absent, not the bundled Chromium; name
|
|
148
|
+
// what is actually missing so the CLI's fix message is accurate.
|
|
149
|
+
const browser = this.#systemBrowser ?? 'chromium';
|
|
150
|
+
throw new MissingBrowserBinaryError(browser, undefined, { cause });
|
|
64
151
|
}
|
|
65
152
|
throw cause;
|
|
66
153
|
}
|
|
@@ -70,6 +157,31 @@ export class PlaywrightLaunchTransport {
|
|
|
70
157
|
const pwPage = context.pages()[0] ?? (await context.newPage());
|
|
71
158
|
return makeSession(context, pwPage, this.#hands);
|
|
72
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Resolve the stealth (`patchright`) chromium via the injected lazy importer.
|
|
162
|
+
*
|
|
163
|
+
* Confines the brittle "optional dependency absent" detection to ONE spot
|
|
164
|
+
* (mirroring {@link isMissingBrowserBinary}): any failure to import the
|
|
165
|
+
* optional package becomes the typed {@link MissingStealthDependencyError}, so
|
|
166
|
+
* the caller never silently degrades to vanilla Playwright.
|
|
167
|
+
*/
|
|
168
|
+
async #resolveStealthLauncher() {
|
|
169
|
+
let mod;
|
|
170
|
+
try {
|
|
171
|
+
mod = await this.#importStealthChromium();
|
|
172
|
+
}
|
|
173
|
+
catch (cause) {
|
|
174
|
+
throw new MissingStealthDependencyError('patchright', undefined, {
|
|
175
|
+
cause,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (mod === null ||
|
|
179
|
+
typeof mod !== 'object' ||
|
|
180
|
+
typeof mod.chromium?.launchPersistentContext !== 'function') {
|
|
181
|
+
throw new MissingStealthDependencyError('patchright');
|
|
182
|
+
}
|
|
183
|
+
return mod.chromium;
|
|
184
|
+
}
|
|
73
185
|
}
|
|
74
186
|
/** True iff `path` exists and is a directory. */
|
|
75
187
|
async function isExistingDirectory(path) {
|
|
@@ -86,12 +198,20 @@ async function isExistingDirectory(path) {
|
|
|
86
198
|
* does not export a typed error for this, so we detect on the message (it
|
|
87
199
|
* instructs the user to run `playwright install`). We confine that brittle
|
|
88
200
|
* string match to this one spot and re-raise as a stable typed error.
|
|
201
|
+
*
|
|
202
|
+
* This also covers the `channel: 'chrome'` case, where the missing binary is the
|
|
203
|
+
* SYSTEM Chrome, not the bundled Chromium. Playwright phrases that as the
|
|
204
|
+
* channel/distribution not being found; we match those variants too so the
|
|
205
|
+
* stealth+system-Chrome path still yields the typed MissingBrowserBinaryError.
|
|
89
206
|
*/
|
|
90
207
|
function isMissingBrowserBinary(cause) {
|
|
91
208
|
const message = cause instanceof Error ? cause.message : String(cause ?? '');
|
|
92
209
|
return (/Executable doesn't exist/i.test(message) ||
|
|
93
210
|
/please run the following command to download new browsers/i.test(message) ||
|
|
94
|
-
/playwright install/i.test(message)
|
|
211
|
+
/playwright install/i.test(message) ||
|
|
212
|
+
// channel: 'chrome' (or other system channels) not installed on the host.
|
|
213
|
+
/Chromium distribution '.*' is not found/i.test(message) ||
|
|
214
|
+
/No "?(chrome|msedge|chromium)"? .* found/i.test(message));
|
|
95
215
|
}
|
|
96
216
|
/**
|
|
97
217
|
* Wrap a live Playwright persistent context into the seam's {@link Session}.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playwright-launch-transport.js","sourceRoot":"","sources":["../src/playwright-launch-transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,kBAAkB,CAAC;AACtC,OAAO,EAAC,QAAQ,EAAiC,MAAM,YAAY,CAAC;AACpE,OAAO,
|
|
1
|
+
{"version":3,"file":"playwright-launch-transport.js","sourceRoot":"","sources":["../src/playwright-launch-transport.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webhands/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Core library for webhands: drives a real, persistent browser via Playwright.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"browser",
|
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"playwright": "1.61.1"
|
|
39
39
|
},
|
|
40
|
+
"optionalDependencies": {
|
|
41
|
+
"patchright": "1.61.1"
|
|
42
|
+
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"@types/node": "^25.2.0",
|
|
42
45
|
"as-soon": "^0.1.5",
|
package/src/errors.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
/** The closed set of identifiable `core` error conditions. */
|
|
19
19
|
export type ControllerErrorCode =
|
|
20
20
|
| 'missing-browser-binary'
|
|
21
|
+
| 'missing-stealth-dependency'
|
|
21
22
|
| 'missing-profile'
|
|
22
23
|
| 'attach-not-chromium'
|
|
23
24
|
| 'attach-no-context'
|
|
@@ -65,6 +66,36 @@ export class MissingBrowserBinaryError extends ControllerError {
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Stealth launch was REQUESTED (the opt-in is on) but the optional `patchright`
|
|
71
|
+
* dependency is not installed/importable. Patchright is an OPTIONAL dependency
|
|
72
|
+
* of `@webhands/core` imported lazily only when stealth is enabled, so a user
|
|
73
|
+
* who never opts in is not forced to install it (ADR-0002: stealth is one extra
|
|
74
|
+
* layer, not the default). When it IS opted into and missing, we refuse LOUDLY
|
|
75
|
+
* with this typed condition rather than silently falling back to vanilla
|
|
76
|
+
* Playwright, because a silent fallback would re-introduce the exact CDP
|
|
77
|
+
* automation tell the user asked us to remove WITHOUT telling them.
|
|
78
|
+
*
|
|
79
|
+
* Mirrors {@link MissingBrowserBinaryError}: a stable typed error whose brittle
|
|
80
|
+
* detection (the dynamic-import failure) is confined to one spot in the launch
|
|
81
|
+
* transport. The CLI can render the exact `pnpm add patchright` fix command by
|
|
82
|
+
* branching on {@link code}.
|
|
83
|
+
*/
|
|
84
|
+
export class MissingStealthDependencyError extends ControllerError {
|
|
85
|
+
readonly code = 'missing-stealth-dependency';
|
|
86
|
+
/** The optional package that must be installed to use stealth. */
|
|
87
|
+
readonly dependency: string;
|
|
88
|
+
|
|
89
|
+
constructor(
|
|
90
|
+
dependency = 'patchright',
|
|
91
|
+
message: string = `Stealth launch is enabled but the optional "${dependency}" dependency is not installed. Install it with \`pnpm add ${dependency}\` (and \`${dependency} install chromium\` if you do not use channel: 'chrome'), or construct the transport without {stealth: true}.`,
|
|
92
|
+
options?: {cause?: unknown},
|
|
93
|
+
) {
|
|
94
|
+
super(message, options);
|
|
95
|
+
this.dependency = dependency;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
68
99
|
/**
|
|
69
100
|
* The named profile has not been set up yet: its dedicated profile directory
|
|
70
101
|
* does not exist on disk. A profile is created by the headed `setup-profile`
|
package/src/index.ts
CHANGED
|
@@ -36,7 +36,11 @@ export {
|
|
|
36
36
|
type LoadHandsOptions,
|
|
37
37
|
} from './hand-loading.js';
|
|
38
38
|
|
|
39
|
-
export {
|
|
39
|
+
export {
|
|
40
|
+
PlaywrightLaunchTransport,
|
|
41
|
+
type PlaywrightLaunchTransportOptions,
|
|
42
|
+
type StealthChromiumImporter,
|
|
43
|
+
} from './playwright-launch-transport.js';
|
|
40
44
|
|
|
41
45
|
export {PlaywrightAttachTransport} from './playwright-attach-transport.js';
|
|
42
46
|
|
|
@@ -51,6 +55,7 @@ export {
|
|
|
51
55
|
export {
|
|
52
56
|
ControllerError,
|
|
53
57
|
MissingBrowserBinaryError,
|
|
58
|
+
MissingStealthDependencyError,
|
|
54
59
|
MissingProfileError,
|
|
55
60
|
AttachNotChromiumError,
|
|
56
61
|
AttachNoContextError,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {stat} from 'node:fs/promises';
|
|
2
2
|
import {chromium, type BrowserContext, type Page} from 'playwright';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
MissingBrowserBinaryError,
|
|
5
|
+
MissingProfileError,
|
|
6
|
+
MissingStealthDependencyError,
|
|
7
|
+
} from './errors.js';
|
|
4
8
|
import {composeWithHands, type Hand, type HandContext} from './hand-host.js';
|
|
5
9
|
import {
|
|
6
10
|
resolveProfileLocation,
|
|
@@ -8,6 +12,135 @@ import {
|
|
|
8
12
|
} from './profile-location.js';
|
|
9
13
|
import type {OpenTarget, Session, Transport} from './seam.js';
|
|
10
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The subset of Playwright's `chromium` browser type the launch transport uses.
|
|
17
|
+
*
|
|
18
|
+
* Patchright is an API-compatible Playwright fork, so its `chromium` has the
|
|
19
|
+
* SAME shape (ADR-0003 stays intact: this structural type, like Playwright's
|
|
20
|
+
* own types, is confined to this module and never crosses the seam). We type the
|
|
21
|
+
* lazily-imported stealth chromium against THIS rather than importing any
|
|
22
|
+
* Patchright type, so the dependency stays optional at the type level too.
|
|
23
|
+
*/
|
|
24
|
+
type ChromiumLauncher = Pick<typeof chromium, 'launchPersistentContext'>;
|
|
25
|
+
|
|
26
|
+
/** The shape `await import('patchright')` is expected to expose. */
|
|
27
|
+
interface StealthModule {
|
|
28
|
+
readonly chromium: ChromiumLauncher;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* How the transport obtains the stealth (`patchright`) chromium. This is an
|
|
33
|
+
* INTERNAL test seam, not a public API: tests inject a fake module (or a
|
|
34
|
+
* rejecting importer) here so no real browser/Patchright is needed, exactly as
|
|
35
|
+
* production uses the default lazy `import('patchright')`. It is deliberately
|
|
36
|
+
* NOT on {@link OpenTarget} (ADR-0003: the seam stays free of Playwright/CDP/
|
|
37
|
+
* Patchright concerns).
|
|
38
|
+
*/
|
|
39
|
+
export type StealthChromiumImporter = () => Promise<StealthModule>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Construction-time policy for {@link PlaywrightLaunchTransport}.
|
|
43
|
+
*
|
|
44
|
+
* Stealth is a TRANSPORT-CONSTRUCTION policy (which browser engine + launch
|
|
45
|
+
* flags to use), not a per-open target detail, so it lives here and NOT on
|
|
46
|
+
* {@link OpenTarget} (which stays Playwright/CDP-free per ADR-0003).
|
|
47
|
+
*/
|
|
48
|
+
export interface PlaywrightLaunchTransportOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Opt-in Patchright-backed stealth launch. Default `false` (vanilla
|
|
51
|
+
* Playwright). When `true`, the transport launches via the lazily-imported
|
|
52
|
+
* optional `patchright` package, which patches the CDP `Runtime.enable`
|
|
53
|
+
* automation tell that anti-bot WAFs detect (ADR-0002 keeps this as one extra
|
|
54
|
+
* layer, not a replacement for a real profile/IP). If `patchright` is not
|
|
55
|
+
* installed it throws {@link MissingStealthDependencyError}; it NEVER silently
|
|
56
|
+
* falls back to vanilla.
|
|
57
|
+
*/
|
|
58
|
+
readonly stealth?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Drive a browser ALREADY INSTALLED ON THE SYSTEM instead of the bundled
|
|
61
|
+
* Chromium, named by its install identity (e.g. `'chrome'` to drive the system
|
|
62
|
+
* Google Chrome, Patchright's recommended setup; also `'msedge'`,
|
|
63
|
+
* `'chrome-beta'`, ...). Applies to BOTH stealth and vanilla launches when set.
|
|
64
|
+
* When omitted, Playwright/Patchright's bundled Chromium is used.
|
|
65
|
+
*
|
|
66
|
+
* Maps to Playwright's `channel` launch option internally; we name it
|
|
67
|
+
* `systemBrowser` so the public surface speaks domain language ("use a browser
|
|
68
|
+
* I already have installed") rather than the Playwright term (ADR-0003 keeps
|
|
69
|
+
* Playwright vocabulary out of the public surface).
|
|
70
|
+
*/
|
|
71
|
+
readonly systemBrowser?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Don't impose a fixed emulated viewport: let the browser window drive its own
|
|
74
|
+
* size, exactly as a real user's browser does. Maps to Playwright's
|
|
75
|
+
* `viewport: null` on the persistent context.
|
|
76
|
+
*
|
|
77
|
+
* Why this matters for hardening: Playwright's DEFAULT is a fixed 1280x720
|
|
78
|
+
* emulated viewport that does NOT match the real OS window, a discrepancy
|
|
79
|
+
* (e.g. `window.outerWidth`/`innerWidth`/`screen` mismatches, no real resize
|
|
80
|
+
* behaviour) that fingerprinting scripts read as a headless/automation tell.
|
|
81
|
+
* Patchright's official recommended recipe sets `no_viewport=True` for this
|
|
82
|
+
* reason.
|
|
83
|
+
*
|
|
84
|
+
* Default: `undefined` leaves Playwright's behaviour as-is, EXCEPT that when
|
|
85
|
+
* {@link stealth} is enabled it defaults to `true` (the Patchright recipe).
|
|
86
|
+
* Pass an explicit `false` to keep the fixed emulated viewport even under
|
|
87
|
+
* stealth (e.g. when a caller deliberately wants a deterministic size). We pick
|
|
88
|
+
* the stealth-on default because shipping the stealth engine while leaving the
|
|
89
|
+
* tell it is meant to hide in place would be self-defeating; making it an
|
|
90
|
+
* explicit, overridable default keeps that honest and discoverable.
|
|
91
|
+
*/
|
|
92
|
+
readonly noViewport?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Extra command-line args appended to the browser launch (Playwright's
|
|
95
|
+
* `args`). An escape hatch for well-known hardening flags Patchright/Chromium
|
|
96
|
+
* users pass (e.g. `--disable-blink-features=AutomationControlled`) WITHOUT
|
|
97
|
+
* leaking a Playwright type across the seam: this is a plain `string[]`, kept
|
|
98
|
+
* confined to this transport-construction policy and deliberately NOT on
|
|
99
|
+
* {@link OpenTarget} (ADR-0003). Default: none.
|
|
100
|
+
*
|
|
101
|
+
* Caveat: args are passed THROUGH verbatim; a wrong or contradictory flag can
|
|
102
|
+
* itself become a tell or break the launch. Opt-in only.
|
|
103
|
+
*/
|
|
104
|
+
readonly extraLaunchArgs?: readonly string[];
|
|
105
|
+
/**
|
|
106
|
+
* Passthrough for Playwright's `ignoreDefaultArgs`: either `true` to drop ALL
|
|
107
|
+
* of Playwright's default launch args, or a list of specific default args to
|
|
108
|
+
* drop, so a caller can strip more automation-flavoured defaults than the
|
|
109
|
+
* built-in stealth subset.
|
|
110
|
+
*
|
|
111
|
+
* When omitted, the stealth path still drops `--enable-automation` on its own
|
|
112
|
+
* (unchanged behaviour). When provided, this value REPLACES that built-in
|
|
113
|
+
* choice, so a caller opting in owns the full list (pass
|
|
114
|
+
* `['--enable-automation', ...]` to keep it). Like {@link extraLaunchArgs}
|
|
115
|
+
* this is a plain value confined to this module, never on {@link OpenTarget}.
|
|
116
|
+
* Default: none.
|
|
117
|
+
*/
|
|
118
|
+
readonly ignoreDefaultArgs?: boolean | readonly string[];
|
|
119
|
+
/**
|
|
120
|
+
* INTERNAL test seam: override how the stealth chromium is imported. Omit in
|
|
121
|
+
* production (defaults to `import('patchright')`). See
|
|
122
|
+
* {@link StealthChromiumImporter}.
|
|
123
|
+
*/
|
|
124
|
+
readonly importStealthChromium?: StealthChromiumImporter;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The package name of the optional stealth dependency. Kept as a runtime value
|
|
129
|
+
* (not an `import('patchright')` literal) so TypeScript does NOT try to resolve
|
|
130
|
+
* its types at build time, since it is an OPTIONAL dependency that is legitimately
|
|
131
|
+
* absent when stealth is never enabled.
|
|
132
|
+
*/
|
|
133
|
+
const STEALTH_PACKAGE = 'patchright';
|
|
134
|
+
|
|
135
|
+
/** The default lazy import of the OPTIONAL `patchright` dependency. */
|
|
136
|
+
const defaultStealthImporter: StealthChromiumImporter = async () => {
|
|
137
|
+
// Indirect (non-literal specifier) so tsc/bundlers do not resolve the
|
|
138
|
+
// optional dep eagerly, and the module load never fails when it is absent;
|
|
139
|
+
// the import only runs when stealth is opted in.
|
|
140
|
+
const specifier = STEALTH_PACKAGE;
|
|
141
|
+
return (await import(specifier)) as unknown as StealthModule;
|
|
142
|
+
};
|
|
143
|
+
|
|
11
144
|
/**
|
|
12
145
|
* The v1 concrete transport: a Playwright browser the controller LAUNCHES
|
|
13
146
|
* against a dedicated, persistent profile directory it owns (PRD "Solution,
|
|
@@ -24,10 +157,23 @@ import type {OpenTarget, Session, Transport} from './seam.js';
|
|
|
24
157
|
* `WEBHANDS_HOME` env var, or `~/.webhands`). See
|
|
25
158
|
* {@link resolveProfileLocation}. Because that is a SHARED location, tests pass
|
|
26
159
|
* a temp `root` (or set the env var) and assert the real home is untouched.
|
|
160
|
+
*
|
|
161
|
+
* STEALTH (opt-in, default OFF): the third constructor arg can enable a
|
|
162
|
+
* Patchright-backed launch ({@link PlaywrightLaunchTransportOptions}). Patchright
|
|
163
|
+
* is an OPTIONAL dependency imported lazily only when stealth is enabled; if it
|
|
164
|
+
* is absent the transport throws {@link MissingStealthDependencyError} rather
|
|
165
|
+
* than falling back to vanilla. This addresses ONLY the CDP `Runtime.enable`
|
|
166
|
+
* automation tell; a real profile/IP/session reputation still matter (ADR-0002).
|
|
27
167
|
*/
|
|
28
168
|
export class PlaywrightLaunchTransport implements Transport {
|
|
29
169
|
readonly #location: ProfileLocationOptions;
|
|
30
170
|
readonly #hands: readonly Hand[];
|
|
171
|
+
readonly #stealth: boolean;
|
|
172
|
+
readonly #systemBrowser: string | undefined;
|
|
173
|
+
readonly #noViewport: boolean | undefined;
|
|
174
|
+
readonly #extraLaunchArgs: readonly string[] | undefined;
|
|
175
|
+
readonly #ignoreDefaultArgs: boolean | readonly string[] | undefined;
|
|
176
|
+
readonly #importStealthChromium: StealthChromiumImporter;
|
|
31
177
|
|
|
32
178
|
/**
|
|
33
179
|
* @param location overrides for where profiles live (a `root` dir and/or an
|
|
@@ -37,13 +183,27 @@ export class PlaywrightLaunchTransport implements Transport {
|
|
|
37
183
|
* built-ins (Phase 2, ADR-0007). These come from {@link loadHands} against
|
|
38
184
|
* the operator's explicit config; the transport does NOT discover them. Omit
|
|
39
185
|
* for the built-ins-only surface.
|
|
186
|
+
* @param options transport-construction policy, notably the opt-in `stealth`
|
|
187
|
+
* toggle, optional `systemBrowser`, and the launch-hardening knobs
|
|
188
|
+
* (`noViewport`, `extraLaunchArgs`, `ignoreDefaultArgs`; see
|
|
189
|
+
* {@link PlaywrightLaunchTransportOptions}). Defaults to vanilla Playwright,
|
|
190
|
+
* bundled Chromium, stealth OFF. The hardening knobs are confined to this
|
|
191
|
+
* module and never reach {@link OpenTarget} (ADR-0003).
|
|
40
192
|
*/
|
|
41
193
|
constructor(
|
|
42
194
|
location: ProfileLocationOptions = {},
|
|
43
195
|
hands: readonly Hand[] = [],
|
|
196
|
+
options: PlaywrightLaunchTransportOptions = {},
|
|
44
197
|
) {
|
|
45
198
|
this.#location = location;
|
|
46
199
|
this.#hands = hands;
|
|
200
|
+
this.#stealth = options.stealth === true;
|
|
201
|
+
this.#systemBrowser = options.systemBrowser;
|
|
202
|
+
this.#noViewport = options.noViewport;
|
|
203
|
+
this.#extraLaunchArgs = options.extraLaunchArgs;
|
|
204
|
+
this.#ignoreDefaultArgs = options.ignoreDefaultArgs;
|
|
205
|
+
this.#importStealthChromium =
|
|
206
|
+
options.importStealthChromium ?? defaultStealthImporter;
|
|
47
207
|
}
|
|
48
208
|
|
|
49
209
|
async open(target: OpenTarget): Promise<Session> {
|
|
@@ -68,14 +228,68 @@ export class PlaywrightLaunchTransport implements Transport {
|
|
|
68
228
|
|
|
69
229
|
const headless = target.headed !== true;
|
|
70
230
|
|
|
231
|
+
// Pick the engine: the lazily-imported stealth (Patchright) chromium when
|
|
232
|
+
// opted in, else vanilla Playwright's. Resolving the stealth module is where
|
|
233
|
+
// an absent optional dependency surfaces as the typed
|
|
234
|
+
// MissingStealthDependencyError (we never fall back to vanilla silently).
|
|
235
|
+
const launcher = this.#stealth
|
|
236
|
+
? await this.#resolveStealthLauncher()
|
|
237
|
+
: chromium;
|
|
238
|
+
|
|
239
|
+
// Launch options: forward headless, the optional systemBrowser (Playwright's
|
|
240
|
+
// `channel`, e.g. 'chrome' to drive system Chrome, Patchright's recommended
|
|
241
|
+
// setup), and for stealth drop Playwright's automation-flavoured default
|
|
242
|
+
// args so they cannot re-add the fingerprint Patchright just removed.
|
|
243
|
+
const launchOptions: Parameters<
|
|
244
|
+
typeof chromium.launchPersistentContext
|
|
245
|
+
>[1] = {headless};
|
|
246
|
+
if (this.#systemBrowser !== undefined) {
|
|
247
|
+
launchOptions.channel = this.#systemBrowser;
|
|
248
|
+
}
|
|
249
|
+
// no_viewport: explicit caller choice wins; otherwise default to TRUE under
|
|
250
|
+
// stealth (Patchright's recommended recipe), and leave Playwright's default
|
|
251
|
+
// fixed viewport in place when stealth is off. `viewport: null` is how
|
|
252
|
+
// Playwright expresses "let the real window drive the size".
|
|
253
|
+
const noViewport = this.#noViewport ?? this.#stealth;
|
|
254
|
+
if (noViewport) {
|
|
255
|
+
launchOptions.viewport = null;
|
|
256
|
+
}
|
|
257
|
+
// ignoreDefaultArgs: an explicit passthrough REPLACES the built-in stealth
|
|
258
|
+
// choice (the caller then owns the full list). With no passthrough, the
|
|
259
|
+
// stealth path keeps dropping just `--enable-automation` so it cannot re-add
|
|
260
|
+
// the fingerprint Patchright just removed.
|
|
261
|
+
if (this.#ignoreDefaultArgs !== undefined) {
|
|
262
|
+
launchOptions.ignoreDefaultArgs =
|
|
263
|
+
typeof this.#ignoreDefaultArgs === 'boolean'
|
|
264
|
+
? this.#ignoreDefaultArgs
|
|
265
|
+
: [...this.#ignoreDefaultArgs];
|
|
266
|
+
} else if (this.#stealth) {
|
|
267
|
+
launchOptions.ignoreDefaultArgs = ['--enable-automation'];
|
|
268
|
+
}
|
|
269
|
+
// Extra launch args (the hardening escape hatch) are appended verbatim. We do
|
|
270
|
+
// NOT set user-agent/locale/timezone/headers here: a wrong UA is a bigger
|
|
271
|
+
// tell than none (Patchright warns against overriding them), so those stay
|
|
272
|
+
// untouched by default.
|
|
273
|
+
if (
|
|
274
|
+
this.#extraLaunchArgs !== undefined &&
|
|
275
|
+
this.#extraLaunchArgs.length > 0
|
|
276
|
+
) {
|
|
277
|
+
launchOptions.args = [...this.#extraLaunchArgs];
|
|
278
|
+
}
|
|
279
|
+
|
|
71
280
|
let context: BrowserContext;
|
|
72
281
|
try {
|
|
73
|
-
context = await
|
|
74
|
-
|
|
75
|
-
|
|
282
|
+
context = await launcher.launchPersistentContext(
|
|
283
|
+
loc.profileDir,
|
|
284
|
+
launchOptions,
|
|
285
|
+
);
|
|
76
286
|
} catch (cause) {
|
|
77
287
|
if (isMissingBrowserBinary(cause)) {
|
|
78
|
-
|
|
288
|
+
// With systemBrowser set (e.g. 'chrome') the "binary missing" failure
|
|
289
|
+
// means the SYSTEM browser is absent, not the bundled Chromium; name
|
|
290
|
+
// what is actually missing so the CLI's fix message is accurate.
|
|
291
|
+
const browser = this.#systemBrowser ?? 'chromium';
|
|
292
|
+
throw new MissingBrowserBinaryError(browser, undefined, {cause});
|
|
79
293
|
}
|
|
80
294
|
throw cause;
|
|
81
295
|
}
|
|
@@ -86,6 +300,33 @@ export class PlaywrightLaunchTransport implements Transport {
|
|
|
86
300
|
const pwPage = context.pages()[0] ?? (await context.newPage());
|
|
87
301
|
return makeSession(context, pwPage, this.#hands);
|
|
88
302
|
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Resolve the stealth (`patchright`) chromium via the injected lazy importer.
|
|
306
|
+
*
|
|
307
|
+
* Confines the brittle "optional dependency absent" detection to ONE spot
|
|
308
|
+
* (mirroring {@link isMissingBrowserBinary}): any failure to import the
|
|
309
|
+
* optional package becomes the typed {@link MissingStealthDependencyError}, so
|
|
310
|
+
* the caller never silently degrades to vanilla Playwright.
|
|
311
|
+
*/
|
|
312
|
+
async #resolveStealthLauncher(): Promise<ChromiumLauncher> {
|
|
313
|
+
let mod: StealthModule;
|
|
314
|
+
try {
|
|
315
|
+
mod = await this.#importStealthChromium();
|
|
316
|
+
} catch (cause) {
|
|
317
|
+
throw new MissingStealthDependencyError('patchright', undefined, {
|
|
318
|
+
cause,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (
|
|
322
|
+
mod === null ||
|
|
323
|
+
typeof mod !== 'object' ||
|
|
324
|
+
typeof mod.chromium?.launchPersistentContext !== 'function'
|
|
325
|
+
) {
|
|
326
|
+
throw new MissingStealthDependencyError('patchright');
|
|
327
|
+
}
|
|
328
|
+
return mod.chromium;
|
|
329
|
+
}
|
|
89
330
|
}
|
|
90
331
|
|
|
91
332
|
/** True iff `path` exists and is a directory. */
|
|
@@ -103,6 +344,11 @@ async function isExistingDirectory(path: string): Promise<boolean> {
|
|
|
103
344
|
* does not export a typed error for this, so we detect on the message (it
|
|
104
345
|
* instructs the user to run `playwright install`). We confine that brittle
|
|
105
346
|
* string match to this one spot and re-raise as a stable typed error.
|
|
347
|
+
*
|
|
348
|
+
* This also covers the `channel: 'chrome'` case, where the missing binary is the
|
|
349
|
+
* SYSTEM Chrome, not the bundled Chromium. Playwright phrases that as the
|
|
350
|
+
* channel/distribution not being found; we match those variants too so the
|
|
351
|
+
* stealth+system-Chrome path still yields the typed MissingBrowserBinaryError.
|
|
106
352
|
*/
|
|
107
353
|
function isMissingBrowserBinary(cause: unknown): boolean {
|
|
108
354
|
const message = cause instanceof Error ? cause.message : String(cause ?? '');
|
|
@@ -111,7 +357,10 @@ function isMissingBrowserBinary(cause: unknown): boolean {
|
|
|
111
357
|
/please run the following command to download new browsers/i.test(
|
|
112
358
|
message,
|
|
113
359
|
) ||
|
|
114
|
-
/playwright install/i.test(message)
|
|
360
|
+
/playwright install/i.test(message) ||
|
|
361
|
+
// channel: 'chrome' (or other system channels) not installed on the host.
|
|
362
|
+
/Chromium distribution '.*' is not found/i.test(message) ||
|
|
363
|
+
/No "?(chrome|msedge|chromium)"? .* found/i.test(message)
|
|
115
364
|
);
|
|
116
365
|
}
|
|
117
366
|
|