@webhands/core 0.4.0 → 0.6.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 +69 -6
- package/dist/errors.d.ts +112 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +121 -0
- package/dist/errors.js.map +1 -1
- package/dist/hand-host.d.ts +198 -5
- package/dist/hand-host.d.ts.map +1 -1
- package/dist/hand-host.js +664 -21
- package/dist/hand-host.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/playwright-attach-transport.d.ts +8 -1
- package/dist/playwright-attach-transport.d.ts.map +1 -1
- package/dist/playwright-attach-transport.js +19 -4
- package/dist/playwright-attach-transport.js.map +1 -1
- package/dist/playwright-launch-transport.d.ts +23 -0
- package/dist/playwright-launch-transport.d.ts.map +1 -1
- package/dist/playwright-launch-transport.js +40 -6
- package/dist/playwright-launch-transport.js.map +1 -1
- package/dist/profile-location.d.ts +19 -0
- package/dist/profile-location.d.ts.map +1 -1
- package/dist/profile-location.js +21 -0
- package/dist/profile-location.js.map +1 -1
- package/dist/seam.d.ts +501 -7
- package/dist/seam.d.ts.map +1 -1
- package/dist/seam.js +31 -0
- package/dist/seam.js.map +1 -1
- package/dist/session-rpc.d.ts +63 -1
- package/dist/session-rpc.d.ts.map +1 -1
- package/dist/session-rpc.js +174 -11
- package/dist/session-rpc.js.map +1 -1
- package/dist/socks-proxy.d.ts +61 -0
- package/dist/socks-proxy.d.ts.map +1 -0
- package/dist/socks-proxy.js +84 -0
- package/dist/socks-proxy.js.map +1 -0
- package/dist/stub-transport.d.ts.map +1 -1
- package/dist/stub-transport.js +74 -6
- package/dist/stub-transport.js.map +1 -1
- package/dist/test-fixtures/fixture-pages.d.ts.map +1 -1
- package/dist/test-fixtures/fixture-pages.js +994 -0
- package/dist/test-fixtures/fixture-pages.js.map +1 -1
- package/dist/test-fixtures/fixture-server.d.ts.map +1 -1
- package/dist/test-fixtures/fixture-server.js +33 -3
- package/dist/test-fixtures/fixture-server.js.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +164 -1
- package/src/hand-host.ts +797 -21
- package/src/index.ts +27 -1
- package/src/playwright-attach-transport.ts +25 -3
- package/src/playwright-launch-transport.ts +63 -4
- package/src/profile-location.ts +25 -0
- package/src/seam.ts +535 -7
- package/src/session-rpc.ts +276 -14
- package/src/socks-proxy.ts +127 -0
- package/src/stub-transport.ts +83 -6
- package/src/test-fixtures/fixture-pages.ts +1010 -0
- package/src/test-fixtures/fixture-server.ts +32 -3
package/src/index.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
export type {
|
|
2
|
+
ActionOptions,
|
|
3
|
+
BoundingBox,
|
|
2
4
|
Cookie,
|
|
3
5
|
Driver,
|
|
6
|
+
EvalOptions,
|
|
4
7
|
LocatorString,
|
|
8
|
+
MouseAction,
|
|
9
|
+
MouseButton,
|
|
10
|
+
MouseInput,
|
|
5
11
|
OpenTarget,
|
|
12
|
+
PwExtra,
|
|
13
|
+
QueryOptions,
|
|
14
|
+
QueryRow,
|
|
15
|
+
Screenshot,
|
|
16
|
+
ScreenshotOptions,
|
|
17
|
+
ScreenshotScope,
|
|
18
|
+
ScrollTarget,
|
|
19
|
+
SelectChoice,
|
|
6
20
|
WebHandsPage,
|
|
7
21
|
Session,
|
|
8
22
|
Snapshot,
|
|
@@ -11,7 +25,7 @@ export type {
|
|
|
11
25
|
Transport,
|
|
12
26
|
WaitCondition,
|
|
13
27
|
} from './seam.js';
|
|
14
|
-
export {locator} from './seam.js';
|
|
28
|
+
export {locator, validateSnapshotOptions} from './seam.js';
|
|
15
29
|
|
|
16
30
|
export {
|
|
17
31
|
serializeCookies,
|
|
@@ -42,6 +56,12 @@ export {
|
|
|
42
56
|
type StealthChromiumImporter,
|
|
43
57
|
} from './playwright-launch-transport.js';
|
|
44
58
|
|
|
59
|
+
export {
|
|
60
|
+
parseSocksProxy,
|
|
61
|
+
hostResolverRulesArg,
|
|
62
|
+
type ParsedSocksProxy,
|
|
63
|
+
} from './socks-proxy.js';
|
|
64
|
+
|
|
45
65
|
export {PlaywrightAttachTransport} from './playwright-attach-transport.js';
|
|
46
66
|
|
|
47
67
|
export {
|
|
@@ -56,11 +76,15 @@ export {
|
|
|
56
76
|
ControllerError,
|
|
57
77
|
MissingBrowserBinaryError,
|
|
58
78
|
MissingStealthDependencyError,
|
|
79
|
+
InvalidProxyError,
|
|
59
80
|
MissingProfileError,
|
|
60
81
|
AttachNotChromiumError,
|
|
61
82
|
AttachNoContextError,
|
|
62
83
|
NoLiveServerError,
|
|
63
84
|
SessionAlreadyActiveError,
|
|
85
|
+
CrossOriginFrameError,
|
|
86
|
+
ScreenshotPathError,
|
|
87
|
+
StaleRefError,
|
|
64
88
|
isControllerError,
|
|
65
89
|
type ControllerErrorCode,
|
|
66
90
|
} from './errors.js';
|
|
@@ -97,9 +121,11 @@ export {
|
|
|
97
121
|
export {
|
|
98
122
|
resolveHomeRoot,
|
|
99
123
|
resolveProfileLocation,
|
|
124
|
+
resolveScreenshotsDir,
|
|
100
125
|
CONTROLLER_HOME_ENV,
|
|
101
126
|
DEFAULT_HOME_DIRNAME,
|
|
102
127
|
PROFILES_DIRNAME,
|
|
128
|
+
SCREENSHOTS_DIRNAME,
|
|
103
129
|
type ProfileLocation,
|
|
104
130
|
type ProfileLocationOptions,
|
|
105
131
|
} from './profile-location.js';
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
} from 'playwright';
|
|
7
7
|
import {AttachNoContextError, AttachNotChromiumError} from './errors.js';
|
|
8
8
|
import {composeWithHands, type Hand, type HandContext} from './hand-host.js';
|
|
9
|
+
import {
|
|
10
|
+
resolveScreenshotsDir,
|
|
11
|
+
type ProfileLocationOptions,
|
|
12
|
+
} from './profile-location.js';
|
|
9
13
|
import type {OpenTarget, Session, Transport} from './seam.js';
|
|
10
14
|
|
|
11
15
|
/**
|
|
@@ -33,15 +37,26 @@ import type {OpenTarget, Session, Transport} from './seam.js';
|
|
|
33
37
|
*/
|
|
34
38
|
export class PlaywrightAttachTransport implements Transport {
|
|
35
39
|
readonly #hands: readonly Hand[];
|
|
40
|
+
readonly #location: ProfileLocationOptions;
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
43
|
* @param hands explicitly-loaded third-party hands to compose alongside the
|
|
39
44
|
* built-ins (Phase 2, ADR-0007). These come from {@link loadHands} against
|
|
40
45
|
* the operator's explicit config; the transport does NOT discover them. Omit
|
|
41
46
|
* for the built-ins-only surface.
|
|
47
|
+
* @param location overrides for the controller home root, used ONLY to resolve
|
|
48
|
+
* the managed SCREENSHOTS dir (`<homeRoot>/screenshots`) the Tier-4
|
|
49
|
+
* `screenshot` verb mints under — attach reuses the user's own browser, so it
|
|
50
|
+
* owns no profile dir, but the screenshot output location still honours the
|
|
51
|
+
* same `root`/`WEBHANDS_HOME` override so a test can isolate it. Omit in
|
|
52
|
+
* production to use `~/.webhands/screenshots`.
|
|
42
53
|
*/
|
|
43
|
-
constructor(
|
|
54
|
+
constructor(
|
|
55
|
+
hands: readonly Hand[] = [],
|
|
56
|
+
location: ProfileLocationOptions = {},
|
|
57
|
+
) {
|
|
44
58
|
this.#hands = hands;
|
|
59
|
+
this.#location = location;
|
|
45
60
|
}
|
|
46
61
|
|
|
47
62
|
async open(target: OpenTarget): Promise<Session> {
|
|
@@ -77,7 +92,8 @@ export class PlaywrightAttachTransport implements Transport {
|
|
|
77
92
|
// browser exposes a context with no page yet (single active session in
|
|
78
93
|
// v1, PRD Out of Scope).
|
|
79
94
|
const pwPage = context.pages()[0] ?? (await context.newPage());
|
|
80
|
-
|
|
95
|
+
const screenshotsDir = resolveScreenshotsDir(this.#location);
|
|
96
|
+
return makeAttachedSession(browser, pwPage, this.#hands, screenshotsDir);
|
|
81
97
|
} catch (cause) {
|
|
82
98
|
// On any open-time refusal, disconnect from the user's browser without
|
|
83
99
|
// closing it (a CDP connection close detaches; it does not kill the
|
|
@@ -107,6 +123,7 @@ function makeAttachedSession(
|
|
|
107
123
|
browser: Browser,
|
|
108
124
|
pwPage: Page,
|
|
109
125
|
extraHands: readonly Hand[],
|
|
126
|
+
screenshotsDir: string,
|
|
110
127
|
): Session {
|
|
111
128
|
const context: BrowserContext = pwPage.context();
|
|
112
129
|
let closed = false;
|
|
@@ -134,7 +151,12 @@ function makeAttachedSession(
|
|
|
134
151
|
// the same shared host the launch transport uses, so the verbs behave
|
|
135
152
|
// identically across both transports. The live `pwPage`/`context` stay
|
|
136
153
|
// in-process and never cross the seam (ADR-0003).
|
|
137
|
-
const handContext: HandContext = {
|
|
154
|
+
const handContext: HandContext = {
|
|
155
|
+
pwPage,
|
|
156
|
+
context,
|
|
157
|
+
ensureOpen,
|
|
158
|
+
screenshotsDir,
|
|
159
|
+
};
|
|
138
160
|
const {page, dispose: disposeHands} = composeWithHands(
|
|
139
161
|
handContext,
|
|
140
162
|
extraHands,
|
|
@@ -8,8 +8,10 @@ import {
|
|
|
8
8
|
import {composeWithHands, type Hand, type HandContext} from './hand-host.js';
|
|
9
9
|
import {
|
|
10
10
|
resolveProfileLocation,
|
|
11
|
+
resolveScreenshotsDir,
|
|
11
12
|
type ProfileLocationOptions,
|
|
12
13
|
} from './profile-location.js';
|
|
14
|
+
import {hostResolverRulesArg, parseSocksProxy} from './socks-proxy.js';
|
|
13
15
|
import type {OpenTarget, Session, Transport} from './seam.js';
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -116,6 +118,29 @@ export interface PlaywrightLaunchTransportOptions {
|
|
|
116
118
|
* Default: none.
|
|
117
119
|
*/
|
|
118
120
|
readonly ignoreDefaultArgs?: boolean | readonly string[];
|
|
121
|
+
/**
|
|
122
|
+
* Route ALL browser traffic AND DNS through a single SOCKS proxy, given as a
|
|
123
|
+
* SOCKS URL: `socks5h://host:1080` (or `socks5://host:1080`, optionally with a
|
|
124
|
+
* `user:pass@` userinfo). When set, the transport forwards the proxy to
|
|
125
|
+
* Playwright's `proxy` launch option AND adds Chromium's `--host-resolver-rules`
|
|
126
|
+
* catch-all so no DNS query escapes locally (see {@link proxyNoLeak}).
|
|
127
|
+
*
|
|
128
|
+
* Scheme convention: `socks5h://` means "resolve DNS at the proxy" (no leak),
|
|
129
|
+
* `socks5://` means "SOCKS5, local DNS allowed" (Chromium still resolves URL
|
|
130
|
+
* hostnames at the proxy, but its DNS prefetcher etc. may issue local DNS). Use
|
|
131
|
+
* {@link proxyNoLeak} to override the scheme's implied DNS behaviour. A
|
|
132
|
+
* malformed value throws the typed {@link InvalidProxyError} rather than
|
|
133
|
+
* launching unproxied. Default: no proxy.
|
|
134
|
+
*/
|
|
135
|
+
readonly proxy?: string;
|
|
136
|
+
/**
|
|
137
|
+
* Override whether the {@link proxy} enforces NO local DNS. `true` forces the
|
|
138
|
+
* leak-free catch-all even for a plain `socks5://` URL; `false` allows local
|
|
139
|
+
* DNS even for a `socks5h://` URL. When omitted, the SCHEME decides
|
|
140
|
+
* (`socks5h` => no leak, `socks5`/`socks` => local DNS allowed). Ignored when
|
|
141
|
+
* {@link proxy} is unset.
|
|
142
|
+
*/
|
|
143
|
+
readonly proxyNoLeak?: boolean;
|
|
119
144
|
/**
|
|
120
145
|
* INTERNAL test seam: override how the stealth chromium is imported. Omit in
|
|
121
146
|
* production (defaults to `import('patchright')`). See
|
|
@@ -173,6 +198,8 @@ export class PlaywrightLaunchTransport implements Transport {
|
|
|
173
198
|
readonly #noViewport: boolean | undefined;
|
|
174
199
|
readonly #extraLaunchArgs: readonly string[] | undefined;
|
|
175
200
|
readonly #ignoreDefaultArgs: boolean | readonly string[] | undefined;
|
|
201
|
+
readonly #proxy: string | undefined;
|
|
202
|
+
readonly #proxyNoLeak: boolean | undefined;
|
|
176
203
|
readonly #importStealthChromium: StealthChromiumImporter;
|
|
177
204
|
|
|
178
205
|
/**
|
|
@@ -202,6 +229,8 @@ export class PlaywrightLaunchTransport implements Transport {
|
|
|
202
229
|
this.#noViewport = options.noViewport;
|
|
203
230
|
this.#extraLaunchArgs = options.extraLaunchArgs;
|
|
204
231
|
this.#ignoreDefaultArgs = options.ignoreDefaultArgs;
|
|
232
|
+
this.#proxy = options.proxy;
|
|
233
|
+
this.#proxyNoLeak = options.proxyNoLeak;
|
|
205
234
|
this.#importStealthChromium =
|
|
206
235
|
options.importStealthChromium ?? defaultStealthImporter;
|
|
207
236
|
}
|
|
@@ -266,15 +295,35 @@ export class PlaywrightLaunchTransport implements Transport {
|
|
|
266
295
|
} else if (this.#stealth) {
|
|
267
296
|
launchOptions.ignoreDefaultArgs = ['--enable-automation'];
|
|
268
297
|
}
|
|
298
|
+
// Proxy: route ALL traffic + DNS through one SOCKS proxy. We parse the URL
|
|
299
|
+
// HERE (a malformed value is the typed InvalidProxyError, never a silent
|
|
300
|
+
// unproxied launch), forward it to Playwright's `proxy` option, and when
|
|
301
|
+
// no-leak is in effect add Chromium's --host-resolver-rules catch-all so even
|
|
302
|
+
// the DNS prefetcher cannot leak a raw local DNS query.
|
|
303
|
+
const hardeningArgs: string[] = [];
|
|
304
|
+
if (this.#proxy !== undefined && this.#proxy.trim() !== '') {
|
|
305
|
+
const parsed = parseSocksProxy(this.#proxy, this.#proxyNoLeak);
|
|
306
|
+
launchOptions.proxy = {
|
|
307
|
+
server: parsed.server,
|
|
308
|
+
...(parsed.username !== undefined ? {username: parsed.username} : {}),
|
|
309
|
+
...(parsed.password !== undefined ? {password: parsed.password} : {}),
|
|
310
|
+
};
|
|
311
|
+
if (parsed.noLeak) {
|
|
312
|
+
hardeningArgs.push(hostResolverRulesArg(parsed.host));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
269
315
|
// Extra launch args (the hardening escape hatch) are appended verbatim. We do
|
|
270
316
|
// NOT set user-agent/locale/timezone/headers here: a wrong UA is a bigger
|
|
271
317
|
// tell than none (Patchright warns against overriding them), so those stay
|
|
272
|
-
// untouched by default.
|
|
318
|
+
// untouched by default. The proxy's no-leak DNS arg (if any) rides alongside.
|
|
273
319
|
if (
|
|
274
320
|
this.#extraLaunchArgs !== undefined &&
|
|
275
321
|
this.#extraLaunchArgs.length > 0
|
|
276
322
|
) {
|
|
277
|
-
|
|
323
|
+
hardeningArgs.push(...this.#extraLaunchArgs);
|
|
324
|
+
}
|
|
325
|
+
if (hardeningArgs.length > 0) {
|
|
326
|
+
launchOptions.args = hardeningArgs;
|
|
278
327
|
}
|
|
279
328
|
|
|
280
329
|
let context: BrowserContext;
|
|
@@ -298,7 +347,11 @@ export class PlaywrightLaunchTransport implements Transport {
|
|
|
298
347
|
// the single active page (PRD: single active session in v1). Create one if
|
|
299
348
|
// the build ever changes that invariant.
|
|
300
349
|
const pwPage = context.pages()[0] ?? (await context.newPage());
|
|
301
|
-
|
|
350
|
+
// The managed screenshots dir resolves from the SAME location override as
|
|
351
|
+
// profiles (`<homeRoot>/screenshots`), so a test's temp `root` isolates
|
|
352
|
+
// screenshots too and the real `~/.webhands/screenshots` stays untouched.
|
|
353
|
+
const screenshotsDir = resolveScreenshotsDir(this.#location);
|
|
354
|
+
return makeSession(context, pwPage, this.#hands, screenshotsDir);
|
|
302
355
|
}
|
|
303
356
|
|
|
304
357
|
/**
|
|
@@ -379,6 +432,7 @@ function makeSession(
|
|
|
379
432
|
context: BrowserContext,
|
|
380
433
|
pwPage: Page,
|
|
381
434
|
extraHands: readonly Hand[],
|
|
435
|
+
screenshotsDir: string,
|
|
382
436
|
): Session {
|
|
383
437
|
let closed = false;
|
|
384
438
|
const ensureOpen = () => {
|
|
@@ -405,7 +459,12 @@ function makeSession(
|
|
|
405
459
|
// Build the verb surface from the built-in hands over a live hand-context.
|
|
406
460
|
// The host keeps the live `pwPage`/`context` in-process (they never cross the
|
|
407
461
|
// seam, ADR-0003); the hand-context carries live page access only.
|
|
408
|
-
const handContext: HandContext = {
|
|
462
|
+
const handContext: HandContext = {
|
|
463
|
+
pwPage,
|
|
464
|
+
context,
|
|
465
|
+
ensureOpen,
|
|
466
|
+
screenshotsDir,
|
|
467
|
+
};
|
|
409
468
|
const {page, dispose: disposeHands} = composeWithHands(
|
|
410
469
|
handContext,
|
|
411
470
|
extraHands,
|
package/src/profile-location.ts
CHANGED
|
@@ -30,6 +30,16 @@ export const CONTROLLER_HOME_ENV = 'WEBHANDS_HOME';
|
|
|
30
30
|
/** The subdirectory under the home root that holds dedicated profiles. */
|
|
31
31
|
export const PROFILES_DIRNAME = 'profiles';
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The subdirectory under the home root where the `screenshot` verb MINTS its
|
|
35
|
+
* PNG files (the Tier-4 managed screenshots dir, prd
|
|
36
|
+
* `broaden-agent-verb-surface`, R3). It lives BESIDE `profiles/` under the SAME
|
|
37
|
+
* overridable home root, so the same `root`/`WEBHANDS_HOME` override that
|
|
38
|
+
* isolates profiles in a test also isolates screenshots — nothing writes to the
|
|
39
|
+
* real `~/.webhands/screenshots` unless the home root points there.
|
|
40
|
+
*/
|
|
41
|
+
export const SCREENSHOTS_DIRNAME = 'screenshots';
|
|
42
|
+
|
|
33
43
|
/** Inputs that influence where a profile resolves (all optional, for tests). */
|
|
34
44
|
export interface ProfileLocationOptions {
|
|
35
45
|
/**
|
|
@@ -90,3 +100,18 @@ export function resolveProfileLocation(
|
|
|
90
100
|
profile,
|
|
91
101
|
};
|
|
92
102
|
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve the managed SCREENSHOTS directory (`<homeRoot>/screenshots`) the
|
|
106
|
+
* `screenshot` verb mints PNGs under (prd `broaden-agent-verb-surface`, R3).
|
|
107
|
+
* Like {@link resolveProfileLocation} it is PURE (creates no directory) and
|
|
108
|
+
* honours the same `root`/`WEBHANDS_HOME` precedence, so a test that points the
|
|
109
|
+
* home root at a temp dir isolates screenshots there and the real
|
|
110
|
+
* `~/.webhands/screenshots` stays untouched. The verb (in the transport) is
|
|
111
|
+
* responsible for creating the dir lazily on first write.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveScreenshotsDir(
|
|
114
|
+
options: ProfileLocationOptions = {},
|
|
115
|
+
): string {
|
|
116
|
+
return join(resolveHomeRoot(options), SCREENSHOTS_DIRNAME);
|
|
117
|
+
}
|