@tangle-network/browser-agent-driver 0.10.0 → 0.11.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/brain/index.d.ts +2 -0
- package/dist/brain/index.d.ts.map +1 -1
- package/dist/brain/index.js +96 -10
- package/dist/brain/index.js.map +1 -1
- package/dist/browser-launch.d.ts +3 -0
- package/dist/browser-launch.d.ts.map +1 -1
- package/dist/browser-launch.js +12 -2
- package/dist/browser-launch.js.map +1 -1
- package/dist/cli-design-audit.d.ts.map +1 -1
- package/dist/cli-design-audit.js +250 -2
- package/dist/cli-design-audit.js.map +1 -1
- package/dist/cli-showcase.d.ts +28 -0
- package/dist/cli-showcase.d.ts.map +1 -0
- package/dist/cli-showcase.js +131 -0
- package/dist/cli-showcase.js.map +1 -0
- package/dist/cli-ui.d.ts.map +1 -1
- package/dist/cli-ui.js +23 -2
- package/dist/cli-ui.js.map +1 -1
- package/dist/cli.js +371 -211
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/design/compare.d.ts +9 -0
- package/dist/design/compare.d.ts.map +1 -0
- package/dist/design/compare.js +339 -0
- package/dist/design/compare.js.map +1 -0
- package/dist/design/index.d.ts +6 -0
- package/dist/design/index.d.ts.map +1 -0
- package/dist/design/index.js +5 -0
- package/dist/design/index.js.map +1 -0
- package/dist/design/page-interaction.d.ts +24 -0
- package/dist/design/page-interaction.d.ts.map +1 -0
- package/dist/design/page-interaction.js +244 -0
- package/dist/design/page-interaction.js.map +1 -0
- package/dist/design/rip.d.ts +9 -0
- package/dist/design/rip.d.ts.map +1 -0
- package/dist/design/rip.js +362 -0
- package/dist/design/rip.js.map +1 -0
- package/dist/design/types.d.ts +126 -0
- package/dist/design/types.d.ts.map +1 -0
- package/dist/design/types.js +3 -0
- package/dist/design/types.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/runner/runner.d.ts.map +1 -1
- package/dist/runner/runner.js +24 -0
- package/dist/runner/runner.js.map +1 -1
- package/dist/showcase/annotate.d.ts +28 -0
- package/dist/showcase/annotate.d.ts.map +1 -0
- package/dist/showcase/annotate.js +95 -0
- package/dist/showcase/annotate.js.map +1 -0
- package/dist/showcase/assemble.d.ts +31 -0
- package/dist/showcase/assemble.d.ts.map +1 -0
- package/dist/showcase/assemble.js +107 -0
- package/dist/showcase/assemble.js.map +1 -0
- package/dist/showcase/demo-player.d.ts +53 -0
- package/dist/showcase/demo-player.d.ts.map +1 -0
- package/dist/showcase/demo-player.js +325 -0
- package/dist/showcase/demo-player.js.map +1 -0
- package/dist/showcase/index.d.ts +21 -0
- package/dist/showcase/index.d.ts.map +1 -0
- package/dist/showcase/index.js +289 -0
- package/dist/showcase/index.js.map +1 -0
- package/dist/showcase/types.d.ts +111 -0
- package/dist/showcase/types.d.ts.map +1 -0
- package/dist/showcase/types.js +3 -0
- package/dist/showcase/types.js.map +1 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -1
package/dist/cli.js
CHANGED
|
@@ -101,6 +101,18 @@ async function main() {
|
|
|
101
101
|
'fork-run': { type: 'string' },
|
|
102
102
|
pages: { type: 'string' },
|
|
103
103
|
'extract-tokens': { type: 'boolean' },
|
|
104
|
+
rip: { type: 'boolean' },
|
|
105
|
+
'design-compare': { type: 'boolean' },
|
|
106
|
+
'compare-url': { type: 'string' },
|
|
107
|
+
// showcase
|
|
108
|
+
script: { type: 'string' },
|
|
109
|
+
capture: { type: 'string' },
|
|
110
|
+
crop: { type: 'string' },
|
|
111
|
+
highlight: { type: 'string' },
|
|
112
|
+
format: { type: 'string' },
|
|
113
|
+
viewport: { type: 'string' },
|
|
114
|
+
scale: { type: 'string' },
|
|
115
|
+
'color-scheme': { type: 'string' },
|
|
104
116
|
'llm-timeout': { type: 'string' },
|
|
105
117
|
retries: { type: 'string' },
|
|
106
118
|
'retry-delay-ms': { type: 'string' },
|
|
@@ -116,6 +128,8 @@ async function main() {
|
|
|
116
128
|
timeout: { type: 'string' },
|
|
117
129
|
extension: { type: 'string', multiple: true },
|
|
118
130
|
'user-data-dir': { type: 'string' },
|
|
131
|
+
'profile-dir': { type: 'string' },
|
|
132
|
+
'cdp-url': { type: 'string' },
|
|
119
133
|
wallet: { type: 'boolean' },
|
|
120
134
|
'wallet-auto-approve': { type: 'boolean' },
|
|
121
135
|
'wallet-password': { type: 'string' },
|
|
@@ -160,6 +174,32 @@ async function main() {
|
|
|
160
174
|
cliError('--url is required for design-audit.');
|
|
161
175
|
process.exit(1);
|
|
162
176
|
}
|
|
177
|
+
// --design-compare mode
|
|
178
|
+
if (values['design-compare']) {
|
|
179
|
+
if (!values['compare-url']) {
|
|
180
|
+
cliError('--compare-url is required with --design-compare.');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const { runDesignCompare } = await import('./design/compare.js');
|
|
184
|
+
await runDesignCompare({
|
|
185
|
+
urlA: values.url,
|
|
186
|
+
urlB: values['compare-url'],
|
|
187
|
+
headless: values.headless,
|
|
188
|
+
outputDir: values.sink,
|
|
189
|
+
});
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
// --rip mode
|
|
193
|
+
if (values.rip) {
|
|
194
|
+
const { ripSite } = await import('./design/rip.js');
|
|
195
|
+
await ripSite({
|
|
196
|
+
url: values.url,
|
|
197
|
+
pages: values.pages ? parseInt(values.pages) : undefined,
|
|
198
|
+
headless: values.headless,
|
|
199
|
+
outputDir: values.sink,
|
|
200
|
+
});
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
163
203
|
const { runDesignAudit } = await import('./cli-design-audit.js');
|
|
164
204
|
await runDesignAudit({
|
|
165
205
|
url: values.url,
|
|
@@ -208,8 +248,27 @@ async function main() {
|
|
|
208
248
|
}
|
|
209
249
|
process.exit(0);
|
|
210
250
|
}
|
|
251
|
+
if (command === 'showcase') {
|
|
252
|
+
const { handleShowcase } = await import('./cli-showcase.js');
|
|
253
|
+
await handleShowcase({
|
|
254
|
+
url: values.url,
|
|
255
|
+
script: values.script,
|
|
256
|
+
capture: values.capture,
|
|
257
|
+
crop: values.crop,
|
|
258
|
+
highlight: values.highlight,
|
|
259
|
+
format: values.format,
|
|
260
|
+
viewport: values.viewport,
|
|
261
|
+
output: values.sink,
|
|
262
|
+
headless: values.headless ?? true,
|
|
263
|
+
colorScheme: values['color-scheme'],
|
|
264
|
+
scale: values.scale ? parseFloat(values.scale) : undefined,
|
|
265
|
+
storageState: values['storage-state'],
|
|
266
|
+
quality: values['quality-threshold'] ? parseInt(values['quality-threshold']) : undefined,
|
|
267
|
+
});
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
211
270
|
if (command !== 'run') {
|
|
212
|
-
cliError(`Unknown command: ${command}. Use "run", "runs",
|
|
271
|
+
cliError(`Unknown command: ${command}. Use "run", "runs", "design-audit", or "showcase".`);
|
|
213
272
|
process.exit(1);
|
|
214
273
|
}
|
|
215
274
|
// Validate inputs
|
|
@@ -378,6 +437,10 @@ async function main() {
|
|
|
378
437
|
}
|
|
379
438
|
}
|
|
380
439
|
}
|
|
440
|
+
if (values['profile-dir'])
|
|
441
|
+
cliOverrides.profileDir = values['profile-dir'];
|
|
442
|
+
if (values['cdp-url'])
|
|
443
|
+
cliOverrides.cdpUrl = values['cdp-url'];
|
|
381
444
|
if (values.memory !== undefined || values['memory-dir']) {
|
|
382
445
|
cliOverrides.memory = {
|
|
383
446
|
...(cliOverrides.memory ?? {}),
|
|
@@ -438,12 +501,8 @@ async function main() {
|
|
|
438
501
|
cliOverrides.resourceBlocking = {
|
|
439
502
|
...(cliOverrides.resourceBlocking ?? {}),
|
|
440
503
|
blockAnalytics: true,
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
blockImages: true,
|
|
444
|
-
blockMedia: true,
|
|
445
|
-
}
|
|
446
|
-
: {}),
|
|
504
|
+
blockImages: true,
|
|
505
|
+
blockMedia: true,
|
|
447
506
|
};
|
|
448
507
|
}
|
|
449
508
|
cliOverrides.microPlan = {
|
|
@@ -677,9 +736,9 @@ async function main() {
|
|
|
677
736
|
cliError(`storage state file not found: ${storageStatePath}`);
|
|
678
737
|
process.exit(1);
|
|
679
738
|
}
|
|
680
|
-
if (launchPlan.
|
|
681
|
-
|
|
682
|
-
|
|
739
|
+
if (launchPlan.persistentContext && !launchPlan.cdpUrl && browserName !== 'chromium') {
|
|
740
|
+
const feature = launchPlan.walletMode ? 'Wallet mode' : '--profile-dir';
|
|
741
|
+
throw new Error(`${feature} requires Chromium. Set --browser chromium.`);
|
|
683
742
|
}
|
|
684
743
|
// Ensure clean exit on interrupt
|
|
685
744
|
process.on('SIGINT', () => { renderer?.destroy(); process.exit(130); });
|
|
@@ -690,145 +749,179 @@ async function main() {
|
|
|
690
749
|
let persistentContext;
|
|
691
750
|
let stopWalletAutoApprover;
|
|
692
751
|
const launchDiagnostics = {};
|
|
693
|
-
|
|
752
|
+
// CDP connection — attach to an existing browser (Atlas, Chrome, Brave, etc.)
|
|
753
|
+
let cdpUrl = launchPlan.cdpUrl || process.env.BROWSER_ENDPOINT;
|
|
754
|
+
let cdpConnected = false;
|
|
755
|
+
if (cdpUrl) {
|
|
756
|
+
// Auto-discover WebSocket URL from HTTP endpoint
|
|
757
|
+
if (cdpUrl.startsWith('http://') || cdpUrl.startsWith('https://')) {
|
|
758
|
+
try {
|
|
759
|
+
const versionUrl = cdpUrl.replace(/\/$/, '') + '/json/version';
|
|
760
|
+
const res = await fetch(versionUrl);
|
|
761
|
+
const info = await res.json();
|
|
762
|
+
if (info.webSocketDebuggerUrl) {
|
|
763
|
+
if (info.Browser && !quiet)
|
|
764
|
+
cliLog('cdp', `connected to ${info.Browser}`);
|
|
765
|
+
cdpUrl = info.webSocketDebuggerUrl;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
// Fall through — try the URL as-is
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const cdpStartedAt = Date.now();
|
|
773
|
+
if (cdpUrl.includes('/devtools/') || browserName === 'chromium') {
|
|
774
|
+
browser = await chromium.connectOverCDP(cdpUrl);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
const browserType = browserName === 'firefox' ? firefox : browserName === 'webkit' ? webkit : chromium;
|
|
778
|
+
browser = await browserType.connect(cdpUrl);
|
|
779
|
+
}
|
|
780
|
+
launchDiagnostics.browserLaunchMs = Date.now() - cdpStartedAt;
|
|
781
|
+
launchDiagnostics.cdpUrl = cdpUrl;
|
|
782
|
+
cdpConnected = true;
|
|
783
|
+
}
|
|
784
|
+
else if (launchPlan.persistentContext) {
|
|
694
785
|
for (const extensionPath of launchPlan.extensionPaths) {
|
|
695
786
|
if (!fs.existsSync(extensionPath)) {
|
|
696
787
|
throw new Error(`Wallet extension path does not exist: ${extensionPath}`);
|
|
697
788
|
}
|
|
698
789
|
}
|
|
699
|
-
const userDataDir = launchPlan.userDataDir ?? path.resolve('.agent-wallet-profile');
|
|
790
|
+
const userDataDir = launchPlan.userDataDir ?? path.resolve(launchPlan.walletMode ? '.agent-wallet-profile' : '.agent-profile');
|
|
700
791
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
701
792
|
const persistentLaunchStartedAt = Date.now();
|
|
702
793
|
persistentContext = await chromium.launchPersistentContext(userDataDir, {
|
|
703
794
|
channel: isStealthProfile ? 'chrome' : 'chromium',
|
|
704
|
-
headless:
|
|
795
|
+
headless: launchPlan.headless,
|
|
705
796
|
args: launchPlan.browserArgs,
|
|
706
797
|
viewport,
|
|
707
798
|
recordVideo: { dir: videoDir, size: viewport },
|
|
708
799
|
});
|
|
709
800
|
launchDiagnostics.browserLaunchMs = Date.now() - persistentLaunchStartedAt;
|
|
710
801
|
await applyStorageStateToPersistentContext(persistentContext, storageStatePath);
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
802
|
+
if (launchPlan.walletMode) {
|
|
803
|
+
const walletConfig = driverConfig.wallet ?? {};
|
|
804
|
+
// Intercept page-level JSON-RPC so dApps see wallet balances from the
|
|
805
|
+
// local Anvil fork. Only forward user-specific calls (eth_getBalance
|
|
806
|
+
// for the wallet, eth_call with wallet address in calldata). Pool data
|
|
807
|
+
// and protocol calls go to real endpoints for reliability.
|
|
808
|
+
const walletRpcUrl = walletConfig.preflight?.chain?.rpcUrl;
|
|
809
|
+
if (walletRpcUrl) {
|
|
810
|
+
// Default to Anvil's first derived address if no wallet address configured
|
|
811
|
+
const walletAddrFull = (walletConfig.address ?? '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266').toLowerCase();
|
|
812
|
+
const walletAddrHex = walletAddrFull.replace('0x', '');
|
|
813
|
+
await persistentContext.route('**/*', async (route) => {
|
|
814
|
+
try {
|
|
815
|
+
const frame = route.request().frame();
|
|
816
|
+
if (frame && frame.url().startsWith('chrome-extension://')) {
|
|
817
|
+
await route.continue();
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
725
822
|
await route.continue();
|
|
726
823
|
return;
|
|
727
824
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const items = Array.isArray(body) ? body : [body];
|
|
750
|
-
// Check if any item involves the wallet (balance, contract call, simulation)
|
|
751
|
-
const isUserQuery = items.some((item) => {
|
|
752
|
-
const method = item.method;
|
|
753
|
-
if (!method)
|
|
754
|
-
return false;
|
|
755
|
-
if (method === 'eth_getBalance') {
|
|
756
|
-
const params = item.params;
|
|
757
|
-
return params?.[0]?.toLowerCase() === walletAddrFull;
|
|
758
|
-
}
|
|
759
|
-
if (method === 'eth_call' || method === 'eth_estimateGas') {
|
|
760
|
-
const params = item.params;
|
|
761
|
-
const txObj = params?.[0];
|
|
762
|
-
if (!txObj)
|
|
825
|
+
if (route.request().method() !== 'POST') {
|
|
826
|
+
await route.continue();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const ct = route.request().headers()['content-type'] ?? '';
|
|
830
|
+
if (!ct.includes('json')) {
|
|
831
|
+
await route.continue();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const postData = route.request().postData();
|
|
835
|
+
if (!postData) {
|
|
836
|
+
await route.continue();
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
try {
|
|
840
|
+
const body = JSON.parse(postData);
|
|
841
|
+
const items = Array.isArray(body) ? body : [body];
|
|
842
|
+
// Check if any item involves the wallet (balance, contract call, simulation)
|
|
843
|
+
const isUserQuery = items.some((item) => {
|
|
844
|
+
const method = item.method;
|
|
845
|
+
if (!method)
|
|
763
846
|
return false;
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
847
|
+
if (method === 'eth_getBalance') {
|
|
848
|
+
const params = item.params;
|
|
849
|
+
return params?.[0]?.toLowerCase() === walletAddrFull;
|
|
850
|
+
}
|
|
851
|
+
if (method === 'eth_call' || method === 'eth_estimateGas') {
|
|
852
|
+
const params = item.params;
|
|
853
|
+
const txObj = params?.[0];
|
|
854
|
+
if (!txObj)
|
|
855
|
+
return false;
|
|
856
|
+
const from = txObj.from?.toLowerCase() ?? '';
|
|
857
|
+
const data = txObj.data?.toLowerCase() ?? '';
|
|
858
|
+
return from === walletAddrFull || data.includes(walletAddrHex);
|
|
859
|
+
}
|
|
860
|
+
if (method === 'eth_getTransactionCount') {
|
|
861
|
+
const params = item.params;
|
|
862
|
+
return params?.[0]?.toLowerCase() === walletAddrFull;
|
|
863
|
+
}
|
|
864
|
+
return false;
|
|
865
|
+
});
|
|
866
|
+
if (!isUserQuery) {
|
|
867
|
+
await route.continue();
|
|
868
|
+
return;
|
|
771
869
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
870
|
+
// Normalize: some dApps (Aave) omit jsonrpc/id — Anvil requires them
|
|
871
|
+
let nextId = 1;
|
|
872
|
+
const normalized = items.map((item) => {
|
|
873
|
+
const out = { ...item, jsonrpc: '2.0', id: item.id ?? nextId++ };
|
|
874
|
+
delete out.chainId;
|
|
875
|
+
return out;
|
|
876
|
+
});
|
|
877
|
+
const payload = Array.isArray(body) ? normalized : normalized[0];
|
|
878
|
+
const res = await fetch(walletRpcUrl, {
|
|
879
|
+
method: 'POST',
|
|
880
|
+
headers: { 'Content-Type': 'application/json' },
|
|
881
|
+
body: JSON.stringify(payload),
|
|
882
|
+
});
|
|
883
|
+
await route.fulfill({
|
|
884
|
+
status: res.status,
|
|
885
|
+
contentType: 'application/json',
|
|
886
|
+
body: await res.text(),
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
catch {
|
|
775
890
|
await route.continue();
|
|
776
|
-
return;
|
|
777
891
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
const shouldAutoApprove = walletConfig.autoApprove ?? true;
|
|
895
|
+
if (shouldAutoApprove) {
|
|
896
|
+
stopWalletAutoApprover = await startWalletAutoApprover(persistentContext, {
|
|
897
|
+
password: walletConfig.password,
|
|
898
|
+
tickMs: walletConfig.tickMs,
|
|
899
|
+
actionSelectors: walletConfig.actionSelectors,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
const preflightEnabled = walletConfig.preflight?.enabled ?? true;
|
|
903
|
+
const preflightSeedUrls = walletConfig.preflight?.seedUrls && walletConfig.preflight.seedUrls.length > 0
|
|
904
|
+
? walletConfig.preflight.seedUrls
|
|
905
|
+
: [...new Set(cases.map((testCase) => testCase.startUrl).filter(Boolean))];
|
|
906
|
+
if (preflightEnabled && preflightSeedUrls.length > 0) {
|
|
907
|
+
const preflight = await runWalletPreflight(persistentContext, {
|
|
908
|
+
seedUrls: preflightSeedUrls,
|
|
909
|
+
password: walletConfig.password,
|
|
910
|
+
actionSelectors: walletConfig.actionSelectors,
|
|
911
|
+
promptPaths: walletConfig.promptPaths,
|
|
912
|
+
connectSelectors: walletConfig.connectSelectors,
|
|
913
|
+
connectorSelectors: walletConfig.connectorSelectors,
|
|
914
|
+
requestAccounts: walletConfig.preflight?.requestAccounts,
|
|
915
|
+
accountsTimeoutMs: walletConfig.preflight?.accountsTimeoutMs,
|
|
916
|
+
maxChainSwitchAttempts: walletConfig.preflight?.maxChainSwitchAttempts,
|
|
917
|
+
chain: walletConfig.preflight?.chain,
|
|
918
|
+
log: quiet ? undefined : (message) => cliLog('wallet', message),
|
|
919
|
+
});
|
|
920
|
+
if (!preflight.ok) {
|
|
921
|
+
const failed = preflight.results.find((resultEntry) => !resultEntry.ready);
|
|
922
|
+
const details = failed?.details ?? 'unknown reason';
|
|
923
|
+
throw new Error(`Wallet preflight failed for ${preflight.failedUrl ?? 'unknown origin'} (${details})`);
|
|
799
924
|
}
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
const shouldAutoApprove = walletConfig.autoApprove ?? true;
|
|
803
|
-
if (shouldAutoApprove) {
|
|
804
|
-
stopWalletAutoApprover = await startWalletAutoApprover(persistentContext, {
|
|
805
|
-
password: walletConfig.password,
|
|
806
|
-
tickMs: walletConfig.tickMs,
|
|
807
|
-
actionSelectors: walletConfig.actionSelectors,
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
const preflightEnabled = walletConfig.preflight?.enabled ?? true;
|
|
811
|
-
const preflightSeedUrls = walletConfig.preflight?.seedUrls && walletConfig.preflight.seedUrls.length > 0
|
|
812
|
-
? walletConfig.preflight.seedUrls
|
|
813
|
-
: [...new Set(cases.map((testCase) => testCase.startUrl).filter(Boolean))];
|
|
814
|
-
if (preflightEnabled && preflightSeedUrls.length > 0) {
|
|
815
|
-
const preflight = await runWalletPreflight(persistentContext, {
|
|
816
|
-
seedUrls: preflightSeedUrls,
|
|
817
|
-
password: walletConfig.password,
|
|
818
|
-
actionSelectors: walletConfig.actionSelectors,
|
|
819
|
-
promptPaths: walletConfig.promptPaths,
|
|
820
|
-
connectSelectors: walletConfig.connectSelectors,
|
|
821
|
-
connectorSelectors: walletConfig.connectorSelectors,
|
|
822
|
-
requestAccounts: walletConfig.preflight?.requestAccounts,
|
|
823
|
-
accountsTimeoutMs: walletConfig.preflight?.accountsTimeoutMs,
|
|
824
|
-
maxChainSwitchAttempts: walletConfig.preflight?.maxChainSwitchAttempts,
|
|
825
|
-
chain: walletConfig.preflight?.chain,
|
|
826
|
-
log: quiet ? undefined : (message) => cliLog('wallet', message),
|
|
827
|
-
});
|
|
828
|
-
if (!preflight.ok) {
|
|
829
|
-
const failed = preflight.results.find((resultEntry) => !resultEntry.ready);
|
|
830
|
-
const details = failed?.details ?? 'unknown reason';
|
|
831
|
-
throw new Error(`Wallet preflight failed for ${preflight.failedUrl ?? 'unknown origin'} (${details})`);
|
|
832
925
|
}
|
|
833
926
|
}
|
|
834
927
|
}
|
|
@@ -838,94 +931,160 @@ async function main() {
|
|
|
838
931
|
: browserName === 'webkit'
|
|
839
932
|
? webkit
|
|
840
933
|
: chromium;
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
934
|
+
const browserLaunchStartedAt = Date.now();
|
|
935
|
+
browser = await browserType.launch({
|
|
936
|
+
headless: launchPlan.headless,
|
|
937
|
+
...(browserName === 'chromium' ? { args: launchPlan.browserArgs } : {}),
|
|
938
|
+
// Use system Chrome for stealth profiles — real TLS/JA3 fingerprint vs bundled Chromium
|
|
939
|
+
...(isStealthProfile && browserName === 'chromium' ? { channel: 'chrome' } : {}),
|
|
940
|
+
});
|
|
941
|
+
launchDiagnostics.browserLaunchMs = Date.now() - browserLaunchStartedAt;
|
|
942
|
+
}
|
|
943
|
+
// Headless Chromium sends "HeadlessChrome/..." in the default User-Agent.
|
|
944
|
+
// CDNs like Akamai reject this with ERR_HTTP2_PROTOCOL_ERROR before any JS
|
|
945
|
+
// stealth patches can run. Build a clean UA from the browser version.
|
|
946
|
+
const headlessUserAgent = launchPlan.headless && browser
|
|
947
|
+
? (() => {
|
|
948
|
+
const ver = browser.version();
|
|
949
|
+
const plat = process.platform;
|
|
950
|
+
const platformToken = plat === 'win32'
|
|
951
|
+
? 'Windows NT 10.0; Win64; x64'
|
|
952
|
+
: plat === 'linux'
|
|
953
|
+
? 'X11; Linux x86_64'
|
|
954
|
+
: 'Macintosh; Intel Mac OS X 10_15_7';
|
|
955
|
+
return `Mozilla/5.0 (${platformToken}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ver} Safari/537.36`;
|
|
956
|
+
})()
|
|
957
|
+
: undefined;
|
|
958
|
+
const driverFactory = async () => {
|
|
959
|
+
const contextStartedAt = Date.now();
|
|
960
|
+
// CDP: reuse the browser's default context (preserves user cookies/sessions).
|
|
961
|
+
// Persistent context: use the already-opened persistent context.
|
|
962
|
+
// Default: create a fresh isolated context.
|
|
963
|
+
let context;
|
|
964
|
+
if (cdpConnected) {
|
|
965
|
+
// Reuse the user's existing browser context — cookies, localStorage, extensions intact
|
|
966
|
+
const contexts = browser.contexts();
|
|
967
|
+
context = contexts[0] ?? await browser.newContext({ viewport });
|
|
968
|
+
}
|
|
969
|
+
else if (persistentContext) {
|
|
970
|
+
context = persistentContext;
|
|
852
971
|
}
|
|
853
972
|
else {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
973
|
+
context = await browser.newContext({
|
|
974
|
+
viewport,
|
|
975
|
+
recordVideo: { dir: videoDir, size: viewport },
|
|
976
|
+
storageState: storageStatePath,
|
|
977
|
+
locale: 'en-US',
|
|
978
|
+
timezoneId: 'America/New_York',
|
|
979
|
+
...(headlessUserAgent ? { userAgent: headlessUserAgent } : {}),
|
|
980
|
+
extraHTTPHeaders: {
|
|
981
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
982
|
+
},
|
|
860
983
|
});
|
|
861
|
-
launchDiagnostics.browserLaunchMs = Date.now() - browserLaunchStartedAt;
|
|
862
984
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
985
|
+
// Stealth patches: only for Playwright-controlled contexts (not real user browsers)
|
|
986
|
+
if (!cdpConnected) {
|
|
987
|
+
await context.addInitScript(`
|
|
988
|
+
// navigator.webdriver — explicit override (backup for --disable-blink-features)
|
|
989
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
990
|
+
// navigator.plugins — empty in headless, non-empty in real browsers
|
|
991
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
992
|
+
get: () => [
|
|
993
|
+
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
|
994
|
+
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
|
|
995
|
+
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
|
|
996
|
+
],
|
|
875
997
|
});
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
998
|
+
// navigator.languages — must match Accept-Language header
|
|
999
|
+
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
|
|
1000
|
+
// hardware signals — realistic desktop values
|
|
1001
|
+
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
|
|
1002
|
+
Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
|
|
1003
|
+
// window.chrome — full stub matching real Chrome
|
|
1004
|
+
if (!window.chrome) window.chrome = {};
|
|
1005
|
+
if (!window.chrome.runtime) window.chrome.runtime = { id: undefined };
|
|
1006
|
+
if (!window.chrome.app) window.chrome.app = { isInstalled: false, InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' }, RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' } };
|
|
1007
|
+
if (!window.chrome.csi) window.chrome.csi = function() { return { onloadT: Date.now(), startE: Date.now(), pageT: Date.now() - performance.timing.navigationStart }; };
|
|
1008
|
+
if (!window.chrome.loadTimes) window.chrome.loadTimes = function() { return { commitLoadTime: Date.now() / 1000, connectionInfo: 'h2', finishDocumentLoadTime: Date.now() / 1000, finishLoadTime: Date.now() / 1000, firstPaintAfterLoadTime: 0, firstPaintTime: Date.now() / 1000, navigationType: 'Other', npnNegotiatedProtocol: 'h2', requestTime: Date.now() / 1000 - 0.16, startLoadTime: Date.now() / 1000 - 0.16, wasAlternateProtocolAvailable: false, wasFetchedViaSpdy: true, wasNpnNegotiated: true }; };
|
|
1009
|
+
// WebGL vendor/renderer — match real GPU values
|
|
1010
|
+
try {
|
|
1011
|
+
const getParameter = WebGLRenderingContext.prototype.getParameter;
|
|
1012
|
+
WebGLRenderingContext.prototype.getParameter = function(parameter) {
|
|
1013
|
+
if (parameter === 37445) return 'Intel Inc.';
|
|
1014
|
+
if (parameter === 37446) return 'Intel Iris OpenGL Engine';
|
|
1015
|
+
return getParameter.call(this, parameter);
|
|
1016
|
+
};
|
|
1017
|
+
} catch (_) {}
|
|
1018
|
+
try {
|
|
1019
|
+
const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
|
|
1020
|
+
WebGL2RenderingContext.prototype.getParameter = function(parameter) {
|
|
1021
|
+
if (parameter === 37445) return 'Intel Inc.';
|
|
1022
|
+
if (parameter === 37446) return 'Intel Iris OpenGL Engine';
|
|
1023
|
+
return getParameter2.call(this, parameter);
|
|
1024
|
+
};
|
|
1025
|
+
} catch (_) {}
|
|
1026
|
+
// window.outerWidth/outerHeight — 0 in headless, match viewport in real browsers
|
|
1027
|
+
if (window.outerWidth === 0) Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth });
|
|
1028
|
+
if (window.outerHeight === 0) Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight + 85 });
|
|
1029
|
+
// Patch permissions API — cover all permission types bots commonly mis-handle
|
|
1030
|
+
try {
|
|
1031
|
+
const origQuery = navigator.permissions.query.bind(navigator.permissions);
|
|
1032
|
+
navigator.permissions.query = (params) => {
|
|
1033
|
+
const deny = ['notifications', 'geolocation', 'camera', 'microphone', 'payment-handler'];
|
|
1034
|
+
if (deny.includes(params.name))
|
|
1035
|
+
return Promise.resolve({ state: 'denied', onchange: null });
|
|
1036
|
+
return origQuery(params);
|
|
1037
|
+
};
|
|
1038
|
+
} catch (_) {}
|
|
1039
|
+
// Canvas fingerprint noise — add imperceptible per-session noise to canvas readback
|
|
1040
|
+
// so each session produces a unique fingerprint (defeats static fingerprint matching)
|
|
1041
|
+
try {
|
|
1042
|
+
const seed = Math.random() * 0xffff | 0;
|
|
1043
|
+
const noisify = (canvas) => {
|
|
1044
|
+
try {
|
|
1045
|
+
const ctx = canvas.getContext('2d');
|
|
1046
|
+
if (!ctx) return;
|
|
1047
|
+
const { width: w, height: h } = canvas;
|
|
1048
|
+
if (w === 0 || h === 0) return;
|
|
1049
|
+
const img = ctx.getImageData(0, 0, w, h);
|
|
1050
|
+
const d = img.data;
|
|
1051
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
1052
|
+
// deterministic per-pixel noise from seed + position
|
|
1053
|
+
d[i] = d[i] ^ ((seed + i) & 1);
|
|
1054
|
+
}
|
|
1055
|
+
ctx.putImageData(img, 0, 0);
|
|
1056
|
+
} catch (_) {}
|
|
1057
|
+
};
|
|
1058
|
+
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
1059
|
+
HTMLCanvasElement.prototype.toDataURL = function(...args) {
|
|
1060
|
+
noisify(this);
|
|
1061
|
+
return origToDataURL.apply(this, args);
|
|
1062
|
+
};
|
|
1063
|
+
const origToBlob = HTMLCanvasElement.prototype.toBlob;
|
|
1064
|
+
HTMLCanvasElement.prototype.toBlob = function(...args) {
|
|
1065
|
+
noisify(this);
|
|
1066
|
+
return origToBlob.apply(this, args);
|
|
1067
|
+
};
|
|
1068
|
+
} catch (_) {}
|
|
1069
|
+
// Fix CDP screenX/screenY bug — CDP Input.dispatchMouseEvent sets
|
|
1070
|
+
// screenX=clientX, screenY=clientY which never happens in real browsers.
|
|
1071
|
+
// Cloudflare Turnstile actively checks this. Add a per-session window
|
|
1072
|
+
// offset so screenX/screenY are realistic and internally consistent.
|
|
1073
|
+
try {
|
|
1074
|
+
const winX = Math.floor(Math.random() * 200) + 50;
|
|
1075
|
+
const winY = Math.floor(Math.random() * 100) + 50;
|
|
1076
|
+
const chrome = 85;
|
|
1077
|
+
Object.defineProperty(MouseEvent.prototype, 'screenX', {
|
|
1078
|
+
get() { return this.clientX + winX; },
|
|
1079
|
+
configurable: true,
|
|
1080
|
+
});
|
|
1081
|
+
Object.defineProperty(MouseEvent.prototype, 'screenY', {
|
|
1082
|
+
get() { return this.clientY + winY + chrome; },
|
|
1083
|
+
configurable: true,
|
|
1084
|
+
});
|
|
1085
|
+
} catch (_) {}
|
|
1086
|
+
`);
|
|
1087
|
+
}
|
|
929
1088
|
const contextCreateMs = Date.now() - contextStartedAt;
|
|
930
1089
|
const pageStartedAt = Date.now();
|
|
931
1090
|
const page = await context.newPage();
|
|
@@ -957,8 +1116,9 @@ async function main() {
|
|
|
957
1116
|
contextCreateMs,
|
|
958
1117
|
pageCreateMs,
|
|
959
1118
|
resourceBlockingSetupMs,
|
|
960
|
-
storageStateApplied: Boolean(storageStatePath),
|
|
1119
|
+
storageStateApplied: Boolean(storageStatePath) && !cdpConnected,
|
|
961
1120
|
persistentContext: Boolean(persistentContext),
|
|
1121
|
+
cdpConnected,
|
|
962
1122
|
};
|
|
963
1123
|
// Wrap in a Driver that properly tears down context on close
|
|
964
1124
|
const wrappedDriver = {
|
|
@@ -970,7 +1130,7 @@ async function main() {
|
|
|
970
1130
|
async close() {
|
|
971
1131
|
await driver.close().catch(() => { });
|
|
972
1132
|
await page.close().catch(() => { });
|
|
973
|
-
if (!persistentContext) {
|
|
1133
|
+
if (!persistentContext && !cdpConnected) {
|
|
974
1134
|
await context.close().catch(() => { });
|
|
975
1135
|
}
|
|
976
1136
|
},
|