@webhands/core 0.2.0 → 0.3.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,71 @@ 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
+ Programmatic equivalent (the `--stealth` / `--use-system-browser` flags map onto
135
+ these transport options):
136
+
137
+ ```ts
138
+ import {PlaywrightLaunchTransport} from '@webhands/core';
139
+
140
+ const transport = new PlaywrightLaunchTransport(
141
+ {}, // profile location (omit for ~/.webhands)
142
+ [], // extra hands
143
+ {stealth: true, systemBrowser: 'chrome'},
144
+ );
145
+ // Stealth + headed + a real logged-in profile is the strongest recipe:
146
+ const session = await transport.open({
147
+ mode: 'launch',
148
+ profile: 'default',
149
+ headed: true,
150
+ });
151
+ ```
152
+
153
+ If stealth is enabled but `patchright` is not installed, the open throws a typed
154
+ `MissingStealthDependencyError` (the CLI prints `pnpm add patchright` as the fix).
155
+ It **never silently falls back** to vanilla Playwright, because that would put
156
+ the tell back without telling you.
157
+
158
+ **Honest caveat.** Stealth addresses ONLY the CDP `Runtime.enable` automation
159
+ tell. It is **necessary-but-not-sufficient**: IP reputation and session/profile
160
+ reputation still matter. The realistic recipe is stealth +
161
+ `systemBrowser: 'chrome'` + headed + a warmed, logged-in profile + a residential
162
+ IP (see
163
+ [`docs/adr/0002`](docs/adr/0002-real-session-over-fingerprint-spoofing.md)).
164
+
100
165
  ## Security note (the `serve` endpoint runs arbitrary code)
101
166
 
102
167
  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":"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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';
@@ -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,68 @@
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
+ * INTERNAL test seam: override how the stealth chromium is imported. Omit in
61
+ * production (defaults to `import('patchright')`). See
62
+ * {@link StealthChromiumImporter}.
63
+ */
64
+ readonly importStealthChromium?: StealthChromiumImporter;
65
+ }
4
66
  /**
5
67
  * The v1 concrete transport: a Playwright browser the controller LAUNCHES
6
68
  * against a dedicated, persistent profile directory it owns (PRD "Solution,
@@ -17,6 +79,13 @@ import type { OpenTarget, Session, Transport } from './seam.js';
17
79
  * `WEBHANDS_HOME` env var, or `~/.webhands`). See
18
80
  * {@link resolveProfileLocation}. Because that is a SHARED location, tests pass
19
81
  * a temp `root` (or set the env var) and assert the real home is untouched.
82
+ *
83
+ * STEALTH (opt-in, default OFF): the third constructor arg can enable a
84
+ * Patchright-backed launch ({@link PlaywrightLaunchTransportOptions}). Patchright
85
+ * is an OPTIONAL dependency imported lazily only when stealth is enabled; if it
86
+ * is absent the transport throws {@link MissingStealthDependencyError} rather
87
+ * than falling back to vanilla. This addresses ONLY the CDP `Runtime.enable`
88
+ * automation tell; a real profile/IP/session reputation still matter (ADR-0002).
20
89
  */
21
90
  export declare class PlaywrightLaunchTransport implements Transport {
22
91
  #private;
@@ -28,8 +97,13 @@ export declare class PlaywrightLaunchTransport implements Transport {
28
97
  * built-ins (Phase 2, ADR-0007). These come from {@link loadHands} against
29
98
  * the operator's explicit config; the transport does NOT discover them. Omit
30
99
  * for the built-ins-only surface.
100
+ * @param options transport-construction policy, notably the opt-in `stealth`
101
+ * toggle and optional `systemBrowser` (see
102
+ * {@link PlaywrightLaunchTransportOptions}). Defaults to vanilla Playwright,
103
+ * bundled Chromium, stealth OFF.
31
104
  */
32
- constructor(location?: ProfileLocationOptions, hands?: readonly Hand[]);
105
+ constructor(location?: ProfileLocationOptions, hands?: readonly Hand[], options?: PlaywrightLaunchTransportOptions);
33
106
  open(target: OpenTarget): Promise<Session>;
34
107
  }
108
+ export {};
35
109
  //# 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;;;;OAIG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,uBAAuB,CAAC;CACzD;AAmBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,yBAA0B,YAAW,SAAS;;IAO1D;;;;;;;;;;;;OAYG;gBAEF,QAAQ,GAAE,sBAA2B,EACrC,KAAK,GAAE,SAAS,IAAI,EAAO,EAC3B,OAAO,GAAE,gCAAqC;IAUzC,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC;CA8FhD"}
@@ -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,20 @@ 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
+ #importStealthChromium;
26
51
  /**
27
52
  * @param location overrides for where profiles live (a `root` dir and/or an
28
53
  * `env`). Omit in production to use `~/.webhands`; pass a temp
@@ -31,10 +56,18 @@ export class PlaywrightLaunchTransport {
31
56
  * built-ins (Phase 2, ADR-0007). These come from {@link loadHands} against
32
57
  * the operator's explicit config; the transport does NOT discover them. Omit
33
58
  * for the built-ins-only surface.
59
+ * @param options transport-construction policy, notably the opt-in `stealth`
60
+ * toggle and optional `systemBrowser` (see
61
+ * {@link PlaywrightLaunchTransportOptions}). Defaults to vanilla Playwright,
62
+ * bundled Chromium, stealth OFF.
34
63
  */
35
- constructor(location = {}, hands = []) {
64
+ constructor(location = {}, hands = [], options = {}) {
36
65
  this.#location = location;
37
66
  this.#hands = hands;
67
+ this.#stealth = options.stealth === true;
68
+ this.#systemBrowser = options.systemBrowser;
69
+ this.#importStealthChromium =
70
+ options.importStealthChromium ?? defaultStealthImporter;
38
71
  }
39
72
  async open(target) {
40
73
  if (target.mode !== 'launch') {
@@ -52,15 +85,35 @@ export class PlaywrightLaunchTransport {
52
85
  throw new MissingProfileError(loc.profile, loc.profileDir);
53
86
  }
54
87
  const headless = target.headed !== true;
88
+ // Pick the engine: the lazily-imported stealth (Patchright) chromium when
89
+ // opted in, else vanilla Playwright's. Resolving the stealth module is where
90
+ // an absent optional dependency surfaces as the typed
91
+ // MissingStealthDependencyError (we never fall back to vanilla silently).
92
+ const launcher = this.#stealth
93
+ ? await this.#resolveStealthLauncher()
94
+ : chromium;
95
+ // Launch options: forward headless, the optional systemBrowser (Playwright's
96
+ // `channel`, e.g. 'chrome' to drive system Chrome, Patchright's recommended
97
+ // setup), and for stealth drop Playwright's automation-flavoured default
98
+ // args so they cannot re-add the fingerprint Patchright just removed.
99
+ const launchOptions = { headless };
100
+ if (this.#systemBrowser !== undefined) {
101
+ launchOptions.channel = this.#systemBrowser;
102
+ }
103
+ if (this.#stealth) {
104
+ launchOptions.ignoreDefaultArgs = ['--enable-automation'];
105
+ }
55
106
  let context;
56
107
  try {
57
- context = await chromium.launchPersistentContext(loc.profileDir, {
58
- headless,
59
- });
108
+ context = await launcher.launchPersistentContext(loc.profileDir, launchOptions);
60
109
  }
61
110
  catch (cause) {
62
111
  if (isMissingBrowserBinary(cause)) {
63
- throw new MissingBrowserBinaryError('chromium', undefined, { cause });
112
+ // With systemBrowser set (e.g. 'chrome') the "binary missing" failure
113
+ // means the SYSTEM browser is absent, not the bundled Chromium; name
114
+ // what is actually missing so the CLI's fix message is accurate.
115
+ const browser = this.#systemBrowser ?? 'chromium';
116
+ throw new MissingBrowserBinaryError(browser, undefined, { cause });
64
117
  }
65
118
  throw cause;
66
119
  }
@@ -70,6 +123,31 @@ export class PlaywrightLaunchTransport {
70
123
  const pwPage = context.pages()[0] ?? (await context.newPage());
71
124
  return makeSession(context, pwPage, this.#hands);
72
125
  }
126
+ /**
127
+ * Resolve the stealth (`patchright`) chromium via the injected lazy importer.
128
+ *
129
+ * Confines the brittle "optional dependency absent" detection to ONE spot
130
+ * (mirroring {@link isMissingBrowserBinary}): any failure to import the
131
+ * optional package becomes the typed {@link MissingStealthDependencyError}, so
132
+ * the caller never silently degrades to vanilla Playwright.
133
+ */
134
+ async #resolveStealthLauncher() {
135
+ let mod;
136
+ try {
137
+ mod = await this.#importStealthChromium();
138
+ }
139
+ catch (cause) {
140
+ throw new MissingStealthDependencyError('patchright', undefined, {
141
+ cause,
142
+ });
143
+ }
144
+ if (mod === null ||
145
+ typeof mod !== 'object' ||
146
+ typeof mod.chromium?.launchPersistentContext !== 'function') {
147
+ throw new MissingStealthDependencyError('patchright');
148
+ }
149
+ return mod.chromium;
150
+ }
73
151
  }
74
152
  /** True iff `path` exists and is a directory. */
75
153
  async function isExistingDirectory(path) {
@@ -86,12 +164,20 @@ async function isExistingDirectory(path) {
86
164
  * does not export a typed error for this, so we detect on the message (it
87
165
  * instructs the user to run `playwright install`). We confine that brittle
88
166
  * string match to this one spot and re-raise as a stable typed error.
167
+ *
168
+ * This also covers the `channel: 'chrome'` case, where the missing binary is the
169
+ * SYSTEM Chrome, not the bundled Chromium. Playwright phrases that as the
170
+ * channel/distribution not being found; we match those variants too so the
171
+ * stealth+system-Chrome path still yields the typed MissingBrowserBinaryError.
89
172
  */
90
173
  function isMissingBrowserBinary(cause) {
91
174
  const message = cause instanceof Error ? cause.message : String(cause ?? '');
92
175
  return (/Executable doesn't exist/i.test(message) ||
93
176
  /please run the following command to download new browsers/i.test(message) ||
94
- /playwright install/i.test(message));
177
+ /playwright install/i.test(message) ||
178
+ // channel: 'chrome' (or other system channels) not installed on the host.
179
+ /Chromium distribution '.*' is not found/i.test(message) ||
180
+ /No "?(chrome|msedge|chromium)"? .* found/i.test(message));
95
181
  }
96
182
  /**
97
183
  * 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.3.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,88 @@ 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
+ * INTERNAL test seam: override how the stealth chromium is imported. Omit in
74
+ * production (defaults to `import('patchright')`). See
75
+ * {@link StealthChromiumImporter}.
76
+ */
77
+ readonly importStealthChromium?: StealthChromiumImporter;
78
+ }
79
+
80
+ /**
81
+ * The package name of the optional stealth dependency. Kept as a runtime value
82
+ * (not an `import('patchright')` literal) so TypeScript does NOT try to resolve
83
+ * its types at build time, since it is an OPTIONAL dependency that is legitimately
84
+ * absent when stealth is never enabled.
85
+ */
86
+ const STEALTH_PACKAGE = 'patchright';
87
+
88
+ /** The default lazy import of the OPTIONAL `patchright` dependency. */
89
+ const defaultStealthImporter: StealthChromiumImporter = async () => {
90
+ // Indirect (non-literal specifier) so tsc/bundlers do not resolve the
91
+ // optional dep eagerly, and the module load never fails when it is absent;
92
+ // the import only runs when stealth is opted in.
93
+ const specifier = STEALTH_PACKAGE;
94
+ return (await import(specifier)) as unknown as StealthModule;
95
+ };
96
+
11
97
  /**
12
98
  * The v1 concrete transport: a Playwright browser the controller LAUNCHES
13
99
  * against a dedicated, persistent profile directory it owns (PRD "Solution,
@@ -24,10 +110,20 @@ import type {OpenTarget, Session, Transport} from './seam.js';
24
110
  * `WEBHANDS_HOME` env var, or `~/.webhands`). See
25
111
  * {@link resolveProfileLocation}. Because that is a SHARED location, tests pass
26
112
  * a temp `root` (or set the env var) and assert the real home is untouched.
113
+ *
114
+ * STEALTH (opt-in, default OFF): the third constructor arg can enable a
115
+ * Patchright-backed launch ({@link PlaywrightLaunchTransportOptions}). Patchright
116
+ * is an OPTIONAL dependency imported lazily only when stealth is enabled; if it
117
+ * is absent the transport throws {@link MissingStealthDependencyError} rather
118
+ * than falling back to vanilla. This addresses ONLY the CDP `Runtime.enable`
119
+ * automation tell; a real profile/IP/session reputation still matter (ADR-0002).
27
120
  */
28
121
  export class PlaywrightLaunchTransport implements Transport {
29
122
  readonly #location: ProfileLocationOptions;
30
123
  readonly #hands: readonly Hand[];
124
+ readonly #stealth: boolean;
125
+ readonly #systemBrowser: string | undefined;
126
+ readonly #importStealthChromium: StealthChromiumImporter;
31
127
 
32
128
  /**
33
129
  * @param location overrides for where profiles live (a `root` dir and/or an
@@ -37,13 +133,22 @@ export class PlaywrightLaunchTransport implements Transport {
37
133
  * built-ins (Phase 2, ADR-0007). These come from {@link loadHands} against
38
134
  * the operator's explicit config; the transport does NOT discover them. Omit
39
135
  * for the built-ins-only surface.
136
+ * @param options transport-construction policy, notably the opt-in `stealth`
137
+ * toggle and optional `systemBrowser` (see
138
+ * {@link PlaywrightLaunchTransportOptions}). Defaults to vanilla Playwright,
139
+ * bundled Chromium, stealth OFF.
40
140
  */
41
141
  constructor(
42
142
  location: ProfileLocationOptions = {},
43
143
  hands: readonly Hand[] = [],
144
+ options: PlaywrightLaunchTransportOptions = {},
44
145
  ) {
45
146
  this.#location = location;
46
147
  this.#hands = hands;
148
+ this.#stealth = options.stealth === true;
149
+ this.#systemBrowser = options.systemBrowser;
150
+ this.#importStealthChromium =
151
+ options.importStealthChromium ?? defaultStealthImporter;
47
152
  }
48
153
 
49
154
  async open(target: OpenTarget): Promise<Session> {
@@ -68,14 +173,41 @@ export class PlaywrightLaunchTransport implements Transport {
68
173
 
69
174
  const headless = target.headed !== true;
70
175
 
176
+ // Pick the engine: the lazily-imported stealth (Patchright) chromium when
177
+ // opted in, else vanilla Playwright's. Resolving the stealth module is where
178
+ // an absent optional dependency surfaces as the typed
179
+ // MissingStealthDependencyError (we never fall back to vanilla silently).
180
+ const launcher = this.#stealth
181
+ ? await this.#resolveStealthLauncher()
182
+ : chromium;
183
+
184
+ // Launch options: forward headless, the optional systemBrowser (Playwright's
185
+ // `channel`, e.g. 'chrome' to drive system Chrome, Patchright's recommended
186
+ // setup), and for stealth drop Playwright's automation-flavoured default
187
+ // args so they cannot re-add the fingerprint Patchright just removed.
188
+ const launchOptions: Parameters<
189
+ typeof chromium.launchPersistentContext
190
+ >[1] = {headless};
191
+ if (this.#systemBrowser !== undefined) {
192
+ launchOptions.channel = this.#systemBrowser;
193
+ }
194
+ if (this.#stealth) {
195
+ launchOptions.ignoreDefaultArgs = ['--enable-automation'];
196
+ }
197
+
71
198
  let context: BrowserContext;
72
199
  try {
73
- context = await chromium.launchPersistentContext(loc.profileDir, {
74
- headless,
75
- });
200
+ context = await launcher.launchPersistentContext(
201
+ loc.profileDir,
202
+ launchOptions,
203
+ );
76
204
  } catch (cause) {
77
205
  if (isMissingBrowserBinary(cause)) {
78
- throw new MissingBrowserBinaryError('chromium', undefined, {cause});
206
+ // With systemBrowser set (e.g. 'chrome') the "binary missing" failure
207
+ // means the SYSTEM browser is absent, not the bundled Chromium; name
208
+ // what is actually missing so the CLI's fix message is accurate.
209
+ const browser = this.#systemBrowser ?? 'chromium';
210
+ throw new MissingBrowserBinaryError(browser, undefined, {cause});
79
211
  }
80
212
  throw cause;
81
213
  }
@@ -86,6 +218,33 @@ export class PlaywrightLaunchTransport implements Transport {
86
218
  const pwPage = context.pages()[0] ?? (await context.newPage());
87
219
  return makeSession(context, pwPage, this.#hands);
88
220
  }
221
+
222
+ /**
223
+ * Resolve the stealth (`patchright`) chromium via the injected lazy importer.
224
+ *
225
+ * Confines the brittle "optional dependency absent" detection to ONE spot
226
+ * (mirroring {@link isMissingBrowserBinary}): any failure to import the
227
+ * optional package becomes the typed {@link MissingStealthDependencyError}, so
228
+ * the caller never silently degrades to vanilla Playwright.
229
+ */
230
+ async #resolveStealthLauncher(): Promise<ChromiumLauncher> {
231
+ let mod: StealthModule;
232
+ try {
233
+ mod = await this.#importStealthChromium();
234
+ } catch (cause) {
235
+ throw new MissingStealthDependencyError('patchright', undefined, {
236
+ cause,
237
+ });
238
+ }
239
+ if (
240
+ mod === null ||
241
+ typeof mod !== 'object' ||
242
+ typeof mod.chromium?.launchPersistentContext !== 'function'
243
+ ) {
244
+ throw new MissingStealthDependencyError('patchright');
245
+ }
246
+ return mod.chromium;
247
+ }
89
248
  }
90
249
 
91
250
  /** True iff `path` exists and is a directory. */
@@ -103,6 +262,11 @@ async function isExistingDirectory(path: string): Promise<boolean> {
103
262
  * does not export a typed error for this, so we detect on the message (it
104
263
  * instructs the user to run `playwright install`). We confine that brittle
105
264
  * string match to this one spot and re-raise as a stable typed error.
265
+ *
266
+ * This also covers the `channel: 'chrome'` case, where the missing binary is the
267
+ * SYSTEM Chrome, not the bundled Chromium. Playwright phrases that as the
268
+ * channel/distribution not being found; we match those variants too so the
269
+ * stealth+system-Chrome path still yields the typed MissingBrowserBinaryError.
106
270
  */
107
271
  function isMissingBrowserBinary(cause: unknown): boolean {
108
272
  const message = cause instanceof Error ? cause.message : String(cause ?? '');
@@ -111,7 +275,10 @@ function isMissingBrowserBinary(cause: unknown): boolean {
111
275
  /please run the following command to download new browsers/i.test(
112
276
  message,
113
277
  ) ||
114
- /playwright install/i.test(message)
278
+ /playwright install/i.test(message) ||
279
+ // channel: 'chrome' (or other system channels) not installed on the host.
280
+ /Chromium distribution '.*' is not found/i.test(message) ||
281
+ /No "?(chrome|msedge|chromium)"? .* found/i.test(message)
115
282
  );
116
283
  }
117
284