@geometra/mcp 1.58.0 → 1.59.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/dist/proxy-spawn.d.ts +14 -0
- package/dist/proxy-spawn.js +10 -0
- package/dist/server.js +31 -2
- package/dist/session.d.ts +9 -1
- package/dist/session.js +34 -2
- package/package.json +1 -1
package/dist/proxy-spawn.d.ts
CHANGED
|
@@ -11,6 +11,19 @@ export declare function resolveProxyScriptPath(): string;
|
|
|
11
11
|
export declare function resolveProxyScriptPathWith(customRequire: NodeRequire, moduleDir?: string): string;
|
|
12
12
|
export declare function resolveProxyRuntimePath(): string;
|
|
13
13
|
export declare function resolveProxyRuntimePathWith(customRequire: NodeRequire, moduleDir?: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* BYO outbound proxy for the spawned Chromium. JobForge sets this when the
|
|
16
|
+
* user configures a residential / mobile / SOCKS proxy in `profile.yml` to
|
|
17
|
+
* bypass datacenter-IP fingerprinting on apply portals (Ashby class B,
|
|
18
|
+
* Lever Mapbox geocoder, Cloudflare Bot Management, etc.). Geometra is the
|
|
19
|
+
* wire — the user supplies the proxy.
|
|
20
|
+
*/
|
|
21
|
+
export interface SpawnProxyConfig {
|
|
22
|
+
server: string;
|
|
23
|
+
username?: string;
|
|
24
|
+
password?: string;
|
|
25
|
+
bypass?: string;
|
|
26
|
+
}
|
|
14
27
|
export interface SpawnProxyParams {
|
|
15
28
|
pageUrl: string;
|
|
16
29
|
port: number;
|
|
@@ -19,6 +32,7 @@ export interface SpawnProxyParams {
|
|
|
19
32
|
height?: number;
|
|
20
33
|
slowMo?: number;
|
|
21
34
|
eagerInitialExtract?: boolean;
|
|
35
|
+
proxy?: SpawnProxyConfig;
|
|
22
36
|
}
|
|
23
37
|
export declare function startEmbeddedGeometraProxy(opts: SpawnProxyParams): Promise<{
|
|
24
38
|
runtime: EmbeddedProxyRuntime;
|
package/dist/proxy-spawn.js
CHANGED
|
@@ -139,6 +139,7 @@ export async function startEmbeddedGeometraProxy(opts) {
|
|
|
139
139
|
headed: opts.headless !== true,
|
|
140
140
|
slowMo: opts.slowMo,
|
|
141
141
|
eagerInitialExtract: opts.eagerInitialExtract,
|
|
142
|
+
...(opts.proxy && { proxy: opts.proxy }),
|
|
142
143
|
});
|
|
143
144
|
return { runtime, wsUrl: runtime.wsUrl };
|
|
144
145
|
}
|
|
@@ -192,6 +193,15 @@ export function spawnGeometraProxy(opts) {
|
|
|
192
193
|
args.push('--headed');
|
|
193
194
|
if (opts.eagerInitialExtract === false)
|
|
194
195
|
args.push('--lazy-initial-extract');
|
|
196
|
+
if (opts.proxy?.server) {
|
|
197
|
+
args.push('--proxy-server', opts.proxy.server);
|
|
198
|
+
if (opts.proxy.username !== undefined)
|
|
199
|
+
args.push('--proxy-username', opts.proxy.username);
|
|
200
|
+
if (opts.proxy.password !== undefined)
|
|
201
|
+
args.push('--proxy-password', opts.proxy.password);
|
|
202
|
+
if (opts.proxy.bypass !== undefined)
|
|
203
|
+
args.push('--proxy-bypass', opts.proxy.bypass);
|
|
204
|
+
}
|
|
195
205
|
return new Promise((resolve, reject) => {
|
|
196
206
|
const child = spawn(process.execPath, args, {
|
|
197
207
|
stdio: ['ignore', 'pipe', 'pipe'],
|
package/dist/server.js
CHANGED
|
@@ -376,6 +376,20 @@ Chromium opens **visible** by default unless \`headless: true\`. File upload / w
|
|
|
376
376
|
.optional()
|
|
377
377
|
.default(false)
|
|
378
378
|
.describe('When true, bypass the reusable proxy pool and spawn a brand-new Chromium for this session that is destroyed on disconnect. Required for safe parallel form submission — without this, two parallel sessions can land on the same pooled proxy and contaminate each other. Default false (use the pool for speed).'),
|
|
379
|
+
proxy: z
|
|
380
|
+
.object({
|
|
381
|
+
server: z
|
|
382
|
+
.string()
|
|
383
|
+
.describe('Proxy URL (http://host:port, https://host:port, or socks5://host:port).'),
|
|
384
|
+
username: z.string().optional().describe('Proxy auth username.'),
|
|
385
|
+
password: z.string().optional().describe('Proxy auth password.'),
|
|
386
|
+
bypass: z
|
|
387
|
+
.string()
|
|
388
|
+
.optional()
|
|
389
|
+
.describe('Comma-separated host patterns to bypass (e.g. "*.internal,localhost").'),
|
|
390
|
+
})
|
|
391
|
+
.optional()
|
|
392
|
+
.describe('BYO outbound proxy for the spawned Chromium. Routes all browser traffic through the supplied residential / mobile / SOCKS proxy — useful for apply portals (Ashby, Lever, Cloudflare-fronted ATSes) that fingerprint datacenter IPs and flag headless sessions as bots. The reusable proxy pool is partitioned by proxy identity so callers with different proxy configs never share a Chromium instance.'),
|
|
379
393
|
returnForms: z
|
|
380
394
|
.boolean()
|
|
381
395
|
.optional()
|
|
@@ -431,6 +445,7 @@ Chromium opens **visible** by default unless \`headless: true\`. File upload / w
|
|
|
431
445
|
height: input.height,
|
|
432
446
|
slowMo: input.slowMo,
|
|
433
447
|
isolated: input.isolated,
|
|
448
|
+
proxy: input.proxy,
|
|
434
449
|
awaitInitialFrame: deferInlinePageModel ? false : undefined,
|
|
435
450
|
eagerInitialExtract: deferInlinePageModel ? true : undefined,
|
|
436
451
|
});
|
|
@@ -501,9 +516,23 @@ Use this when you can prepare ahead of the user-facing task so the next \`geomet
|
|
|
501
516
|
.nonnegative()
|
|
502
517
|
.optional()
|
|
503
518
|
.describe('Playwright slowMo (ms) for the warmed browser.'),
|
|
504
|
-
|
|
519
|
+
proxy: z
|
|
520
|
+
.object({
|
|
521
|
+
server: z
|
|
522
|
+
.string()
|
|
523
|
+
.describe('Proxy URL (http://host:port, https://host:port, or socks5://host:port).'),
|
|
524
|
+
username: z.string().optional().describe('Proxy auth username.'),
|
|
525
|
+
password: z.string().optional().describe('Proxy auth password.'),
|
|
526
|
+
bypass: z
|
|
527
|
+
.string()
|
|
528
|
+
.optional()
|
|
529
|
+
.describe('Comma-separated host patterns to bypass.'),
|
|
530
|
+
})
|
|
531
|
+
.optional()
|
|
532
|
+
.describe('BYO outbound proxy for the warmed Chromium. The pool entry is partitioned by proxy identity, so a later geometra_connect with the same proxy config will reuse this warmed browser; a different proxy config (or no proxy) will not.'),
|
|
533
|
+
}, async ({ pageUrl, port, headless, width, height, slowMo, proxy }) => {
|
|
505
534
|
try {
|
|
506
|
-
const prepared = await prewarmProxy({ pageUrl, port, headless, width, height, slowMo });
|
|
535
|
+
const prepared = await prewarmProxy({ pageUrl, port, headless, width, height, slowMo, proxy });
|
|
507
536
|
return ok(JSON.stringify(prepared));
|
|
508
537
|
}
|
|
509
538
|
catch (e) {
|
package/dist/session.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChildProcess } from 'node:child_process';
|
|
2
2
|
import WebSocket from 'ws';
|
|
3
|
-
import { type EmbeddedProxyRuntime } from './proxy-spawn.js';
|
|
3
|
+
import { type EmbeddedProxyRuntime, type SpawnProxyConfig } from './proxy-spawn.js';
|
|
4
4
|
/**
|
|
5
5
|
* Parsed accessibility node from the UI tree + computed layout.
|
|
6
6
|
* Mirrors the shape of @geometra/core's AccessibilityNode without importing it
|
|
@@ -492,6 +492,7 @@ export declare function prewarmProxy(options: {
|
|
|
492
492
|
width?: number;
|
|
493
493
|
height?: number;
|
|
494
494
|
slowMo?: number;
|
|
495
|
+
proxy?: SpawnProxyConfig;
|
|
495
496
|
}): Promise<{
|
|
496
497
|
prepared: true;
|
|
497
498
|
reused: boolean;
|
|
@@ -535,6 +536,13 @@ export declare function connectThroughProxy(options: {
|
|
|
535
536
|
* leak into another. Default false preserves the existing pool behavior.
|
|
536
537
|
*/
|
|
537
538
|
isolated?: boolean;
|
|
539
|
+
/**
|
|
540
|
+
* BYO outbound proxy for the Chromium. Routes all browser traffic through
|
|
541
|
+
* the supplied residential / mobile / SOCKS proxy. The reusable pool is
|
|
542
|
+
* partitioned by proxy identity so two callers with different proxy
|
|
543
|
+
* configs never share a Chromium instance.
|
|
544
|
+
*/
|
|
545
|
+
proxy?: SpawnProxyConfig;
|
|
538
546
|
}): Promise<Session>;
|
|
539
547
|
export declare function getSession(id?: string): Session | null;
|
|
540
548
|
export declare function pruneDisconnectedSessions(): string[];
|
package/dist/session.js
CHANGED
|
@@ -3,6 +3,19 @@ import { performance } from 'node:perf_hooks';
|
|
|
3
3
|
import WebSocket from 'ws';
|
|
4
4
|
import { spawnGeometraProxy, startEmbeddedGeometraProxy } from './proxy-spawn.js';
|
|
5
5
|
import { completeSessionLifecycle, failSessionLifecycle, heartbeatSessionLifecycle, initializeSessionLifecycle, recordSessionSnapshot, } from './session-state.js';
|
|
6
|
+
/**
|
|
7
|
+
* Stable identity for an outbound proxy config, used as the reusable-pool
|
|
8
|
+
* partition key. Two sessions with different proxy configs MUST NOT share a
|
|
9
|
+
* pooled Chromium — otherwise the first apply's IP leaks into subsequent
|
|
10
|
+
* applies even when the caller opted into a fresh proxy. Password is
|
|
11
|
+
* excluded from the key to keep logs safe; `server + username + bypass` is
|
|
12
|
+
* enough to distinguish every realistic multi-tenant config.
|
|
13
|
+
*/
|
|
14
|
+
function proxyKeyFor(proxy) {
|
|
15
|
+
if (!proxy?.server)
|
|
16
|
+
return '';
|
|
17
|
+
return `${proxy.server}|${proxy.username ?? ''}|${proxy.bypass ?? ''}`;
|
|
18
|
+
}
|
|
6
19
|
const activeSessions = new Map();
|
|
7
20
|
let defaultSessionId = null;
|
|
8
21
|
const MAX_ACTIVE_SESSIONS = 5;
|
|
@@ -129,6 +142,7 @@ function enforceReusableProxyPoolLimit() {
|
|
|
129
142
|
function setReusableProxy(proxy, wsUrl, opts) {
|
|
130
143
|
clearReusableProxiesIfExited();
|
|
131
144
|
const now = Date.now();
|
|
145
|
+
const proxyKey = proxyKeyFor(opts.proxy);
|
|
132
146
|
const existing = reusableProxies.find(entry => sameReusableProxyEntry(entry, proxy));
|
|
133
147
|
if (existing) {
|
|
134
148
|
existing.wsUrl = wsUrl;
|
|
@@ -137,6 +151,7 @@ function setReusableProxy(proxy, wsUrl, opts) {
|
|
|
137
151
|
existing.width = opts.width ?? 1280;
|
|
138
152
|
existing.height = opts.height ?? 720;
|
|
139
153
|
existing.pageUrl = opts.pageUrl;
|
|
154
|
+
existing.proxyKey = proxyKey;
|
|
140
155
|
existing.snapshotReady = opts.snapshotReady ?? existing.snapshotReady;
|
|
141
156
|
existing.lastUsedAt = now;
|
|
142
157
|
return;
|
|
@@ -151,6 +166,7 @@ function setReusableProxy(proxy, wsUrl, opts) {
|
|
|
151
166
|
width: opts.width ?? 1280,
|
|
152
167
|
height: opts.height ?? 720,
|
|
153
168
|
pageUrl: opts.pageUrl,
|
|
169
|
+
proxyKey,
|
|
154
170
|
snapshotReady: opts.snapshotReady === true,
|
|
155
171
|
lastUsedAt: now,
|
|
156
172
|
};
|
|
@@ -176,6 +192,7 @@ function setReusableProxy(proxy, wsUrl, opts) {
|
|
|
176
192
|
width: opts.width ?? 1280,
|
|
177
193
|
height: opts.height ?? 720,
|
|
178
194
|
pageUrl: opts.pageUrl,
|
|
195
|
+
proxyKey,
|
|
179
196
|
snapshotReady: opts.snapshotReady === true,
|
|
180
197
|
lastUsedAt: now,
|
|
181
198
|
});
|
|
@@ -390,7 +407,8 @@ function reusableProxyMatchesOptions(entry, options) {
|
|
|
390
407
|
entry.headless === (options.headless === true) &&
|
|
391
408
|
entry.slowMo === (options.slowMo ?? 0) &&
|
|
392
409
|
entry.width === (options.width ?? 1280) &&
|
|
393
|
-
entry.height === (options.height ?? 720)
|
|
410
|
+
entry.height === (options.height ?? 720) &&
|
|
411
|
+
entry.proxyKey === proxyKeyFor(options.proxy));
|
|
394
412
|
}
|
|
395
413
|
function findExactReusableProxy(options) {
|
|
396
414
|
clearReusableProxiesIfExited();
|
|
@@ -407,8 +425,14 @@ function findReusableProxy(options) {
|
|
|
407
425
|
const desiredSlowMo = options.slowMo ?? 0;
|
|
408
426
|
const desiredWidth = options.width ?? 1280;
|
|
409
427
|
const desiredHeight = options.height ?? 720;
|
|
428
|
+
const desiredProxyKey = proxyKeyFor(options.proxy);
|
|
410
429
|
return reusableProxies
|
|
411
|
-
.filter(entry => entry.headless === desiredHeadless
|
|
430
|
+
.filter(entry => entry.headless === desiredHeadless
|
|
431
|
+
&& entry.slowMo === desiredSlowMo
|
|
432
|
+
// Proxy partition is hard — a session with residential proxy MUST NOT
|
|
433
|
+
// attach to a pooled direct-connection Chromium (and vice versa).
|
|
434
|
+
// Different proxy credentials also get separate pool entries.
|
|
435
|
+
&& entry.proxyKey === desiredProxyKey)
|
|
412
436
|
.sort((a, b) => {
|
|
413
437
|
const score = (entry) => {
|
|
414
438
|
let value = 0;
|
|
@@ -448,6 +472,7 @@ export async function prewarmProxy(options) {
|
|
|
448
472
|
width: options.width,
|
|
449
473
|
height: options.height,
|
|
450
474
|
slowMo: options.slowMo,
|
|
475
|
+
proxy: options.proxy,
|
|
451
476
|
});
|
|
452
477
|
try {
|
|
453
478
|
await runtime.ready;
|
|
@@ -463,6 +488,7 @@ export async function prewarmProxy(options) {
|
|
|
463
488
|
height: options.height,
|
|
464
489
|
pageUrl: options.pageUrl,
|
|
465
490
|
snapshotReady: true,
|
|
491
|
+
proxy: options.proxy,
|
|
466
492
|
});
|
|
467
493
|
return {
|
|
468
494
|
prepared: true,
|
|
@@ -486,6 +512,7 @@ export async function prewarmProxy(options) {
|
|
|
486
512
|
width: options.width,
|
|
487
513
|
height: options.height,
|
|
488
514
|
slowMo: options.slowMo,
|
|
515
|
+
proxy: options.proxy,
|
|
489
516
|
});
|
|
490
517
|
setReusableProxy({ child }, wsUrl, {
|
|
491
518
|
headless: options.headless,
|
|
@@ -493,6 +520,7 @@ export async function prewarmProxy(options) {
|
|
|
493
520
|
width: options.width,
|
|
494
521
|
height: options.height,
|
|
495
522
|
pageUrl: options.pageUrl,
|
|
523
|
+
proxy: options.proxy,
|
|
496
524
|
});
|
|
497
525
|
return {
|
|
498
526
|
prepared: true,
|
|
@@ -594,6 +622,7 @@ async function startFreshProxySession(options) {
|
|
|
594
622
|
height: options.height,
|
|
595
623
|
slowMo: options.slowMo,
|
|
596
624
|
eagerInitialExtract,
|
|
625
|
+
proxy: options.proxy,
|
|
597
626
|
});
|
|
598
627
|
pendingEmbeddedRuntime = runtime;
|
|
599
628
|
const proxyStartMs = performance.now() - proxyStartStartedAt;
|
|
@@ -624,6 +653,7 @@ async function startFreshProxySession(options) {
|
|
|
624
653
|
height: options.height,
|
|
625
654
|
pageUrl: options.pageUrl,
|
|
626
655
|
snapshotReady: Boolean(session.tree && session.layout),
|
|
656
|
+
proxy: options.proxy,
|
|
627
657
|
});
|
|
628
658
|
}
|
|
629
659
|
const baseConnectTrace = session.connectTrace;
|
|
@@ -667,6 +697,7 @@ async function startFreshProxySession(options) {
|
|
|
667
697
|
height: options.height,
|
|
668
698
|
slowMo: options.slowMo,
|
|
669
699
|
eagerInitialExtract,
|
|
700
|
+
proxy: options.proxy,
|
|
670
701
|
});
|
|
671
702
|
const proxyStartMs = performance.now() - proxyStartStartedAt;
|
|
672
703
|
try {
|
|
@@ -691,6 +722,7 @@ async function startFreshProxySession(options) {
|
|
|
691
722
|
height: options.height,
|
|
692
723
|
pageUrl: options.pageUrl,
|
|
693
724
|
snapshotReady: Boolean(session.tree && session.layout),
|
|
725
|
+
proxy: options.proxy,
|
|
694
726
|
});
|
|
695
727
|
}
|
|
696
728
|
const baseConnectTrace = session.connectTrace;
|
package/package.json
CHANGED