@mearl/client 2.20.1 → 2.20.3
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 +10 -0
- package/dist/actionTimeouts.js +1 -1
- package/dist/commands.d.ts +25 -5
- package/dist/commands.js +36 -6
- package/dist/generated-browser-action-protocol.d.ts +24 -1
- package/dist/generated-browser-action-protocol.js +20 -0
- package/dist/index.d.ts +1 -1
- package/dist/pageEvalArguments.d.ts +3 -0
- package/dist/pageEvalArguments.js +47 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.js +108 -79
- package/dist/pluginPageBundle.d.ts +1 -0
- package/dist/pluginPageBundle.js +27 -62
- package/dist/pluginRegistry.d.ts +2 -0
- package/dist/pluginRegistry.js +14 -2
- package/dist/pythonPlugin.d.ts +1 -0
- package/dist/pythonPlugin.js +15 -3
- package/dist/socket.d.ts +7 -0
- package/dist/socket.js +74 -10
- package/dist/unified.d.ts +3 -5
- package/dist/unified.js +138 -15
- package/package.json +3 -3
package/dist/unified.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { invokePageEvalWithArgs } from './pageEvalArguments.js';
|
|
1
2
|
import { stageFile, exportArtifact } from './artifacts.js';
|
|
2
3
|
import WebSocket from 'ws';
|
|
3
4
|
import { DEFAULT_CONNECT_TIMEOUT, DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_REQUEST_TIMEOUT, MAX_BUFFER_SIZE, isHeartbeatMessage, resolveCloudConnectorSelector, } from '@mearl/cloud-types';
|
|
4
5
|
import { resolveActionTimeoutSec } from './actionTimeouts.js';
|
|
5
6
|
import { validateRequiredActionTabId, } from './generated-browser-action-protocol.js';
|
|
6
|
-
import { invoke as invokeLocalBrowser } from './socket.js';
|
|
7
|
+
import { invoke as invokeLocalBrowser, resolveLocalActionTarget } from './socket.js';
|
|
7
8
|
import { loadCloudServerConfig, loadLocalConnectorId } from './unifiedConfig.js';
|
|
8
9
|
const RESPONSE_GRACE_MS = 1_000;
|
|
9
10
|
const GLOBAL_BROWSER_PREFIX = 'mearl:';
|
|
@@ -49,16 +50,6 @@ export function normalizeBrowserSelector(selector) {
|
|
|
49
50
|
const decodedTarget = decodeBrowserTarget(selector);
|
|
50
51
|
return (decodedTarget ? encodeBrowserTarget(decodedTarget) : selector.trim()).toLowerCase();
|
|
51
52
|
}
|
|
52
|
-
export function decodeControlSessionId(id) {
|
|
53
|
-
const match = /^mearl-session:([^:]+):(cs_[a-f0-9-]+):(extension|cdp|provider)$/.exec(id);
|
|
54
|
-
if (!match)
|
|
55
|
-
throw new Error('Invalid session id; reuse session.sessionId from the first response');
|
|
56
|
-
return {
|
|
57
|
-
browser: decodeURIComponent(match[1]),
|
|
58
|
-
id: match[2],
|
|
59
|
-
transport: match[3],
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
53
|
function decorateBrowser(browser, source) {
|
|
63
54
|
const safeBrowser = omitTransientBrowserSecrets(browser);
|
|
64
55
|
const originalBrowserId = String(browser.browserId);
|
|
@@ -102,6 +93,7 @@ function errorMessage(error) {
|
|
|
102
93
|
return error instanceof Error ? error.message : String(error);
|
|
103
94
|
}
|
|
104
95
|
export class UnifiedClient {
|
|
96
|
+
controlSessions = new Map();
|
|
105
97
|
options;
|
|
106
98
|
defaultBrowser;
|
|
107
99
|
defaultConnector;
|
|
@@ -139,7 +131,7 @@ export class UnifiedClient {
|
|
|
139
131
|
}
|
|
140
132
|
async invoke(action, data = {}, options = {}) {
|
|
141
133
|
if (options.session && options.session !== 'new') {
|
|
142
|
-
const session =
|
|
134
|
+
const session = await this.resolveControlSession(options.session, options);
|
|
143
135
|
const selected = decodeBrowserTarget(options.browser);
|
|
144
136
|
const pinned = decodeBrowserTarget(session.browser);
|
|
145
137
|
if (options.browser &&
|
|
@@ -162,6 +154,25 @@ export class UnifiedClient {
|
|
|
162
154
|
throw new Error('--session new requires tab_open, page_snapshot or page_screenshot');
|
|
163
155
|
options = { ...options, controlSessionId: 'new' };
|
|
164
156
|
}
|
|
157
|
+
if (action === 'page_eval' && data.args !== undefined) {
|
|
158
|
+
let pinnedOptions = {
|
|
159
|
+
...options,
|
|
160
|
+
...(options.browser || !this.defaultBrowser ? {} : { browser: this.defaultBrowser }),
|
|
161
|
+
...(options.connector ||
|
|
162
|
+
!this.defaultConnector ||
|
|
163
|
+
decodeBrowserTarget(options.browser ?? this.defaultBrowser)
|
|
164
|
+
? {}
|
|
165
|
+
: { connector: this.defaultConnector }),
|
|
166
|
+
};
|
|
167
|
+
let targetPinned = false;
|
|
168
|
+
return invokePageEvalWithArgs(async (request, transfer) => {
|
|
169
|
+
if (transfer && !targetPinned) {
|
|
170
|
+
pinnedOptions = await this.pinPageEvalTarget(pinnedOptions, request);
|
|
171
|
+
targetPinned = true;
|
|
172
|
+
}
|
|
173
|
+
return this.invokeRouted(action, request, pinnedOptions);
|
|
174
|
+
}, data);
|
|
175
|
+
}
|
|
165
176
|
if (action === 'page_upload' &&
|
|
166
177
|
data.files !== undefined &&
|
|
167
178
|
(!Array.isArray(data.files) ||
|
|
@@ -198,9 +209,63 @@ export class UnifiedClient {
|
|
|
198
209
|
saveArtifact(artifact, outputPath, options = {}) {
|
|
199
210
|
return exportArtifact(this, artifact, outputPath, options);
|
|
200
211
|
}
|
|
212
|
+
async pinPageEvalTarget(options, data) {
|
|
213
|
+
const target = decodeBrowserTarget(options.browser);
|
|
214
|
+
if (target && !options.connector)
|
|
215
|
+
return options;
|
|
216
|
+
const local = options.local ||
|
|
217
|
+
options.transport === 'cdp' ||
|
|
218
|
+
(!this.options.serverUrl && !options.connector);
|
|
219
|
+
if (local) {
|
|
220
|
+
const resolved = resolveLocalActionTarget('page_eval', data, {
|
|
221
|
+
browser: options.browser,
|
|
222
|
+
transport: options.transport,
|
|
223
|
+
});
|
|
224
|
+
if (resolved?.socketPath)
|
|
225
|
+
return { ...options, socketPath: resolved.socketPath };
|
|
226
|
+
if (resolved?.browser)
|
|
227
|
+
return { ...options, browser: encodeBrowserTarget({ browser: resolved.browser }) };
|
|
228
|
+
}
|
|
229
|
+
if (options.connector && !options.browser) {
|
|
230
|
+
const timeoutSec = options.timeoutSec ?? this.options.requestTimeoutSec;
|
|
231
|
+
const connector = await this.resolveConnector(options.connector, timeoutSec);
|
|
232
|
+
const resolved = await this.requestRemote('__resolve_action_target', { action: 'page_eval', data }, timeoutSec, undefined, connector.connectorId);
|
|
233
|
+
if (!resolved?.browser)
|
|
234
|
+
throw new Error('Cannot pin the connector browser for large page_eval arguments. Pass --browser <global-id>.');
|
|
235
|
+
const pinned = {
|
|
236
|
+
...options,
|
|
237
|
+
browser: encodeBrowserTarget({
|
|
238
|
+
browser: resolved.browser,
|
|
239
|
+
connector: connector.connectorId,
|
|
240
|
+
}),
|
|
241
|
+
};
|
|
242
|
+
delete pinned.connector;
|
|
243
|
+
return pinned;
|
|
244
|
+
}
|
|
245
|
+
const listing = (await this.invokeRouted('browser_list', {}, options));
|
|
246
|
+
const connected = listing.browsers.filter(browser => browser.status === 'connected' && (!local || browser.transport !== 'provider'));
|
|
247
|
+
const implicitHost = options.local || options.transport === 'cdp' || options.connector || !this.options.serverUrl;
|
|
248
|
+
const automatic = connected.find(browser => browser.browserId === listing.defaultBrowserId) ??
|
|
249
|
+
(implicitHost || connected.length === 1 ? connected[0] : undefined);
|
|
250
|
+
const candidates = options.browser
|
|
251
|
+
? findBrowsers(listing.browsers, options.browser)
|
|
252
|
+
: automatic
|
|
253
|
+
? [automatic]
|
|
254
|
+
: [];
|
|
255
|
+
if (candidates.length !== 1)
|
|
256
|
+
throw new Error('Large page_eval arguments require a unique connected browser. Pass --browser <global-id>.');
|
|
257
|
+
const browser = candidates[0];
|
|
258
|
+
const pinned = { ...options, browser: browser.browserId };
|
|
259
|
+
delete pinned.connector;
|
|
260
|
+
if (browser.transport === 'cdp' && !decodeBrowserTarget(browser.browserId)?.connector)
|
|
261
|
+
pinned.transport = 'cdp';
|
|
262
|
+
return pinned;
|
|
263
|
+
}
|
|
201
264
|
async invokeRouted(action, data = {}, options = {}) {
|
|
202
265
|
validateRequiredActionTabId(action, data);
|
|
203
266
|
const timeoutSec = options.timeoutSec ?? resolveActionTimeoutSec(action, data, this.options.requestTimeoutSec);
|
|
267
|
+
if (options.socketPath)
|
|
268
|
+
return this.invokeLocal(action, data, timeoutSec, undefined, options.transport, options.controlSessionId, options.socketPath);
|
|
204
269
|
if (action === 'connector_list')
|
|
205
270
|
return this.listConnectors(timeoutSec);
|
|
206
271
|
if (action === 'browser_list') {
|
|
@@ -482,11 +547,12 @@ export class UnifiedClient {
|
|
|
482
547
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
483
548
|
});
|
|
484
549
|
}
|
|
485
|
-
async invokeLocal(action, data, timeoutSec, browser, transport, controlSessionId) {
|
|
550
|
+
async invokeLocal(action, data, timeoutSec, browser, transport, controlSessionId, socketPath) {
|
|
486
551
|
const result = await invokeLocalBrowser(action, data, {
|
|
487
552
|
timeoutSec,
|
|
488
553
|
...(browser ? { browser } : {}),
|
|
489
554
|
...(transport ? { transport } : {}),
|
|
555
|
+
...(socketPath ? { socketPath } : {}),
|
|
490
556
|
controlSource: 'local',
|
|
491
557
|
...(controlSessionId ? { controlSessionId } : {}),
|
|
492
558
|
});
|
|
@@ -501,17 +567,23 @@ export class UnifiedClient {
|
|
|
501
567
|
});
|
|
502
568
|
}
|
|
503
569
|
decorateActionResult(action, result, source) {
|
|
504
|
-
if (result?.session?.sessionId
|
|
570
|
+
if (result?.session?.sessionId) {
|
|
505
571
|
const browserId = encodeBrowserTarget({
|
|
506
572
|
browser: result.session.browserId,
|
|
507
573
|
...(source.connectorId ? { connector: source.connectorId } : {}),
|
|
508
574
|
});
|
|
575
|
+
if (action !== '__control_session_resolve') {
|
|
576
|
+
this.controlSessions.set(result.session.sessionId, {
|
|
577
|
+
browser: browserId,
|
|
578
|
+
id: result.session.sessionId,
|
|
579
|
+
transport: result.session.transport,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
509
582
|
result = {
|
|
510
583
|
...result,
|
|
511
584
|
session: {
|
|
512
585
|
...result.session,
|
|
513
586
|
browserId,
|
|
514
|
-
sessionId: `mearl-session:${encodeURIComponent(browserId)}:${result.session.sessionId}:${result.session.transport}`,
|
|
515
587
|
},
|
|
516
588
|
};
|
|
517
589
|
}
|
|
@@ -539,6 +611,57 @@ export class UnifiedClient {
|
|
|
539
611
|
return result;
|
|
540
612
|
return decorateBrowser(result, source);
|
|
541
613
|
}
|
|
614
|
+
async resolveControlSession(id, options) {
|
|
615
|
+
if (!/^s_[A-Za-z0-9_-]{8}$/.test(id))
|
|
616
|
+
throw new Error('Invalid session id; reuse session.sessionId from the first response');
|
|
617
|
+
const cached = this.controlSessions.get(id);
|
|
618
|
+
if (cached)
|
|
619
|
+
return cached;
|
|
620
|
+
const timeoutSec = options.timeoutSec ?? 15;
|
|
621
|
+
const localLookup = this.invokeLocal('__control_session_resolve', { sessionId: id }, timeoutSec, undefined);
|
|
622
|
+
const discover = async () => {
|
|
623
|
+
const { connectors } = await this.listConnectors(timeoutSec);
|
|
624
|
+
return Promise.allSettled(connectors.map(connector => this.invokeRemote('__control_session_resolve', { sessionId: id }, timeoutSec, undefined, connector)));
|
|
625
|
+
};
|
|
626
|
+
const [local, remote] = await Promise.allSettled([
|
|
627
|
+
localLookup,
|
|
628
|
+
this.options.serverUrl && !options.local && !options.transport
|
|
629
|
+
? discover()
|
|
630
|
+
: Promise.resolve([]),
|
|
631
|
+
]);
|
|
632
|
+
const localSession = local.status === 'fulfilled' ? local.value?.session : undefined;
|
|
633
|
+
const localTarget = decodeBrowserTarget(localSession?.browserId);
|
|
634
|
+
const localConnectorId = localSession?.sessionId === id ? loadLocalConnectorId() : null;
|
|
635
|
+
const results = [local, ...(remote.status === 'fulfilled' ? remote.value : [remote])].filter(result => {
|
|
636
|
+
if (result === local || result.status === 'rejected')
|
|
637
|
+
return true;
|
|
638
|
+
const session = result.value?.session;
|
|
639
|
+
const target = decodeBrowserTarget(session?.browserId);
|
|
640
|
+
// The same host can be reached over both its local socket and its cloud connector.
|
|
641
|
+
return !(localConnectorId &&
|
|
642
|
+
target?.connector === localConnectorId &&
|
|
643
|
+
target.browser === localTarget?.browser &&
|
|
644
|
+
session?.sessionId === id &&
|
|
645
|
+
session.transport === localSession.transport);
|
|
646
|
+
});
|
|
647
|
+
const sessions = results.flatMap(result => result.status === 'fulfilled' && result.value?.session?.sessionId === id
|
|
648
|
+
? [result.value.session]
|
|
649
|
+
: []);
|
|
650
|
+
const ambiguous = results.some(result => result.status === 'rejected' &&
|
|
651
|
+
errorMessage(result.reason).startsWith('CONTROL_SESSION_AMBIGUOUS'));
|
|
652
|
+
if (sessions.length > 1 || ambiguous) {
|
|
653
|
+
this.controlSessions.delete(id);
|
|
654
|
+
throw new Error('CONTROL_SESSION_AMBIGUOUS: multiple hosts own this id');
|
|
655
|
+
}
|
|
656
|
+
const session = sessions[0];
|
|
657
|
+
if (!session) {
|
|
658
|
+
const errors = results.flatMap(result => result.status === 'rejected' ? [errorMessage(result.reason)] : []);
|
|
659
|
+
throw new Error(`CONTROL_SESSION_NOT_FOUND: ${id}${errors.length ? ` (${errors.join('; ')})` : ''}`);
|
|
660
|
+
}
|
|
661
|
+
const target = { browser: session.browserId, id, transport: session.transport };
|
|
662
|
+
this.controlSessions.set(id, target);
|
|
663
|
+
return target;
|
|
664
|
+
}
|
|
542
665
|
async ensureConnected() {
|
|
543
666
|
if (this.ws?.readyState === WebSocket.OPEN)
|
|
544
667
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/client",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.3",
|
|
4
4
|
"description": "Unified Mearl SDK & CLI for local and remote browsers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"commander": "^15.0.0",
|
|
60
60
|
"fastest-levenshtein": "^1.0.16",
|
|
61
61
|
"ws": "^8.18.0",
|
|
62
|
-
"@mearl/cloud-types": "2.20.
|
|
63
|
-
"@mearl/provider": "2.20.
|
|
62
|
+
"@mearl/cloud-types": "2.20.3",
|
|
63
|
+
"@mearl/provider": "2.20.3"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "^24.9.1",
|