@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 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`
@@ -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`
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH;;;;;;GAMG;AACH,MAAM,OAAgB,eAAgB,SAAQ,KAAK;IAGlD,8EAA8E;IACrE,iBAAiB,GAAG,IAAa,CAAC;IAE3C,YAAY,OAAe,EAAE,OAA2B;QACvD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,sEAAsE;QACtE,yCAAyC;QACzC,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;IAC7B,CAAC;CACD;AAED;;;;GAIG;AACH,MAAM,OAAO,yBAA0B,SAAQ,eAAe;IACpD,IAAI,GAAG,wBAAwB,CAAC;IACzC,6DAA6D;IACpD,OAAO,CAAS;IAEzB,YACC,OAAe,EACf,UAAkB,OAAO,OAAO,mCAAmC,EACnE,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;CACD;AAED;;;;;GAKG;AACH,MAAM,OAAO,mBAAoB,SAAQ,eAAe;IAC9C,IAAI,GAAG,iBAAiB,CAAC;IAClC,kDAAkD;IACzC,OAAO,CAAS;IACzB,kEAAkE;IACzD,UAAU,CAAS;IAE5B,YACC,OAAe,EACf,UAAkB,EAClB,UAAkB,QAAQ,OAAO,oDAAoD,UAAU,IAAI,EACnG,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC9B,CAAC;CACD;AAED;;;;;;GAMG;AACH,MAAM,OAAO,sBAAuB,SAAQ,eAAe;IACjD,IAAI,GAAG,qBAAqB,CAAC;IACtC,4EAA4E;IACnE,OAAO,CAAS;IAEzB,YACC,OAAe,EACf,UAAkB,oDAAoD,OAAO,mFAAmF,EAChK,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;CACD;AAED;;;;;GAKG;AACH,MAAM,OAAO,oBAAqB,SAAQ,eAAe;IAC/C,IAAI,GAAG,mBAAmB,CAAC;IACpC,qDAAqD;IAC5C,QAAQ,CAAS;IAE1B,YACC,QAAgB,EAChB,UAAkB,+CAA+C,QAAQ,+DAA+D,EACxI,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC1B,CAAC;CACD;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,iBAAkB,SAAQ,eAAe;IAC5C,IAAI,GAAG,gBAAgB,CAAC;IAEjC,YACC,UAAkB,2EAA2E,EAC7F,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACzB,CAAC;CACD;AAED;;;;;GAKG;AACH,MAAM,OAAO,yBAA0B,SAAQ,eAAe;IACpD,IAAI,GAAG,wBAAwB,CAAC;IAEzC,YACC,UAAkB,0DAA0D,EAC5E,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACzB,CAAC;CACD;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC/C,OAAO,CACN,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAAuC,CAAC,iBAAiB,KAAK,IAAI;QACnE,OAAQ,KAA0B,CAAC,IAAI,KAAK,QAAQ,CACpD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAYH;;;;;;GAMG;AACH,MAAM,OAAgB,eAAgB,SAAQ,KAAK;IAGlD,8EAA8E;IACrE,iBAAiB,GAAG,IAAa,CAAC;IAE3C,YAAY,OAAe,EAAE,OAA2B;QACvD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,sEAAsE;QACtE,yCAAyC;QACzC,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;IAC7B,CAAC;CACD;AAED;;;;GAIG;AACH,MAAM,OAAO,yBAA0B,SAAQ,eAAe;IACpD,IAAI,GAAG,wBAAwB,CAAC;IACzC,6DAA6D;IACpD,OAAO,CAAS;IAEzB,YACC,OAAe,EACf,UAAkB,OAAO,OAAO,mCAAmC,EACnE,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;CACD;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,6BAA8B,SAAQ,eAAe;IACxD,IAAI,GAAG,4BAA4B,CAAC;IAC7C,kEAAkE;IACzD,UAAU,CAAS;IAE5B,YACC,UAAU,GAAG,YAAY,EACzB,UAAkB,+CAA+C,UAAU,6DAA6D,UAAU,aAAa,UAAU,+GAA+G,EACxR,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC9B,CAAC;CACD;AAED;;;;;GAKG;AACH,MAAM,OAAO,mBAAoB,SAAQ,eAAe;IAC9C,IAAI,GAAG,iBAAiB,CAAC;IAClC,kDAAkD;IACzC,OAAO,CAAS;IACzB,kEAAkE;IACzD,UAAU,CAAS;IAE5B,YACC,OAAe,EACf,UAAkB,EAClB,UAAkB,QAAQ,OAAO,oDAAoD,UAAU,IAAI,EACnG,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC9B,CAAC;CACD;AAED;;;;;;GAMG;AACH,MAAM,OAAO,sBAAuB,SAAQ,eAAe;IACjD,IAAI,GAAG,qBAAqB,CAAC;IACtC,4EAA4E;IACnE,OAAO,CAAS;IAEzB,YACC,OAAe,EACf,UAAkB,oDAAoD,OAAO,mFAAmF,EAChK,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;CACD;AAED;;;;;GAKG;AACH,MAAM,OAAO,oBAAqB,SAAQ,eAAe;IAC/C,IAAI,GAAG,mBAAmB,CAAC;IACpC,qDAAqD;IAC5C,QAAQ,CAAS;IAE1B,YACC,QAAgB,EAChB,UAAkB,+CAA+C,QAAQ,+DAA+D,EACxI,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC1B,CAAC;CACD;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,iBAAkB,SAAQ,eAAe;IAC5C,IAAI,GAAG,gBAAgB,CAAC;IAEjC,YACC,UAAkB,2EAA2E,EAC7F,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACzB,CAAC;CACD;AAED;;;;;GAKG;AACH,MAAM,OAAO,yBAA0B,SAAQ,eAAe;IACpD,IAAI,GAAG,wBAAwB,CAAC;IAEzC,YACC,UAAkB,0DAA0D,EAC5E,OAA2B;QAE3B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACzB,CAAC;CACD;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC/C,OAAO,CACN,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAAuC,CAAC,iBAAiB,KAAK,IAAI;QACnE,OAAQ,KAA0B,CAAC,IAAI,KAAK,QAAQ,CACpD,CAAC;AACH,CAAC"}
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';
@@ -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,EAAC,yBAAyB,EAAC,MAAM,kCAAkC,CAAC;AAE3E,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,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"}
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,EAAC,yBAAyB,EAAC,MAAM,kCAAkC,CAAC;AAE3E,OAAO,EAAC,yBAAyB,EAAC,MAAM,kCAAkC,CAAC;AAE3E,OAAO,EACN,YAAY,EACZ,WAAW,GAIX,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACN,eAAe,EACf,yBAAyB,EACzB,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
+ {"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":"AAGA,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;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,yBAA0B,YAAW,SAAS;;IAI1D;;;;;;;;OAQG;gBAEF,QAAQ,GAAE,sBAA2B,EACrC,KAAK,GAAE,SAAS,IAAI,EAAO;IAMtB,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC;CAwChD"}
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 chromium.launchPersistentContext(loc.profileDir, {
58
- headless,
59
- });
142
+ context = await launcher.launchPersistentContext(loc.profileDir, launchOptions);
60
143
  }
61
144
  catch (cause) {
62
145
  if (isMissingBrowserBinary(cause)) {
63
- throw new MissingBrowserBinaryError('chromium', undefined, { cause });
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":"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"}
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.2.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 {PlaywrightLaunchTransport} from './playwright-launch-transport.js';
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 {MissingBrowserBinaryError, MissingProfileError} from './errors.js';
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 chromium.launchPersistentContext(loc.profileDir, {
74
- headless,
75
- });
282
+ context = await launcher.launchPersistentContext(
283
+ loc.profileDir,
284
+ launchOptions,
285
+ );
76
286
  } catch (cause) {
77
287
  if (isMissingBrowserBinary(cause)) {
78
- throw new MissingBrowserBinaryError('chromium', undefined, {cause});
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