@tangle-network/browser-agent-driver 0.10.0 → 0.12.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.
Files changed (97) hide show
  1. package/dist/brain/index.d.ts +3 -0
  2. package/dist/brain/index.d.ts.map +1 -1
  3. package/dist/brain/index.js +103 -11
  4. package/dist/brain/index.js.map +1 -1
  5. package/dist/browser-launch.d.ts +3 -0
  6. package/dist/browser-launch.d.ts.map +1 -1
  7. package/dist/browser-launch.js +12 -2
  8. package/dist/browser-launch.js.map +1 -1
  9. package/dist/cli-auth.d.ts +19 -0
  10. package/dist/cli-auth.d.ts.map +1 -0
  11. package/dist/cli-auth.js +195 -0
  12. package/dist/cli-auth.js.map +1 -0
  13. package/dist/cli-design-audit.d.ts +8 -0
  14. package/dist/cli-design-audit.d.ts.map +1 -1
  15. package/dist/cli-design-audit.js +858 -31
  16. package/dist/cli-design-audit.js.map +1 -1
  17. package/dist/cli-showcase.d.ts +28 -0
  18. package/dist/cli-showcase.d.ts.map +1 -0
  19. package/dist/cli-showcase.js +131 -0
  20. package/dist/cli-showcase.js.map +1 -0
  21. package/dist/cli-ui.d.ts.map +1 -1
  22. package/dist/cli-ui.js +41 -2
  23. package/dist/cli-ui.js.map +1 -1
  24. package/dist/cli.js +418 -211
  25. package/dist/cli.js.map +1 -1
  26. package/dist/config.d.ts +4 -0
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/config.js +2 -1
  29. package/dist/config.js.map +1 -1
  30. package/dist/design/compare.d.ts +9 -0
  31. package/dist/design/compare.d.ts.map +1 -0
  32. package/dist/design/compare.js +339 -0
  33. package/dist/design/compare.js.map +1 -0
  34. package/dist/design/index.d.ts +6 -0
  35. package/dist/design/index.d.ts.map +1 -0
  36. package/dist/design/index.js +5 -0
  37. package/dist/design/index.js.map +1 -0
  38. package/dist/design/page-interaction.d.ts +24 -0
  39. package/dist/design/page-interaction.d.ts.map +1 -0
  40. package/dist/design/page-interaction.js +244 -0
  41. package/dist/design/page-interaction.js.map +1 -0
  42. package/dist/design/rip.d.ts +9 -0
  43. package/dist/design/rip.d.ts.map +1 -0
  44. package/dist/design/rip.js +362 -0
  45. package/dist/design/rip.js.map +1 -0
  46. package/dist/design/types.d.ts +126 -0
  47. package/dist/design/types.d.ts.map +1 -0
  48. package/dist/design/types.js +3 -0
  49. package/dist/design/types.js.map +1 -0
  50. package/dist/drivers/playwright.d.ts +1 -0
  51. package/dist/drivers/playwright.d.ts.map +1 -1
  52. package/dist/drivers/playwright.js +21 -5
  53. package/dist/drivers/playwright.js.map +1 -1
  54. package/dist/drivers/types.d.ts +8 -0
  55. package/dist/drivers/types.d.ts.map +1 -1
  56. package/dist/index.d.ts +5 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +4 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/runner/effect-verification.d.ts +12 -0
  61. package/dist/runner/effect-verification.d.ts.map +1 -0
  62. package/dist/runner/effect-verification.js +169 -0
  63. package/dist/runner/effect-verification.js.map +1 -0
  64. package/dist/runner/index.d.ts +1 -0
  65. package/dist/runner/index.d.ts.map +1 -1
  66. package/dist/runner/index.js +2 -0
  67. package/dist/runner/index.js.map +1 -1
  68. package/dist/runner/runner.d.ts.map +1 -1
  69. package/dist/runner/runner.js +34 -57
  70. package/dist/runner/runner.js.map +1 -1
  71. package/dist/runner.d.ts +1 -1
  72. package/dist/runner.d.ts.map +1 -1
  73. package/dist/runner.js +2 -0
  74. package/dist/runner.js.map +1 -1
  75. package/dist/showcase/annotate.d.ts +28 -0
  76. package/dist/showcase/annotate.d.ts.map +1 -0
  77. package/dist/showcase/annotate.js +95 -0
  78. package/dist/showcase/annotate.js.map +1 -0
  79. package/dist/showcase/assemble.d.ts +31 -0
  80. package/dist/showcase/assemble.d.ts.map +1 -0
  81. package/dist/showcase/assemble.js +107 -0
  82. package/dist/showcase/assemble.js.map +1 -0
  83. package/dist/showcase/demo-player.d.ts +53 -0
  84. package/dist/showcase/demo-player.d.ts.map +1 -0
  85. package/dist/showcase/demo-player.js +325 -0
  86. package/dist/showcase/demo-player.js.map +1 -0
  87. package/dist/showcase/index.d.ts +21 -0
  88. package/dist/showcase/index.d.ts.map +1 -0
  89. package/dist/showcase/index.js +289 -0
  90. package/dist/showcase/index.js.map +1 -0
  91. package/dist/showcase/types.d.ts +111 -0
  92. package/dist/showcase/types.d.ts.map +1 -0
  93. package/dist/showcase/types.js +3 -0
  94. package/dist/showcase/types.js.map +1 -0
  95. package/dist/types.d.ts +69 -0
  96. package/dist/types.d.ts.map +1 -1
  97. package/package.json +13 -1
package/dist/cli.js CHANGED
@@ -101,6 +101,22 @@ 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
+ evolve: { type: 'string' },
108
+ 'evolve-rounds': { type: 'string' },
109
+ 'project-dir': { type: 'string' },
110
+ reproducibility: { type: 'boolean' },
111
+ // showcase
112
+ script: { type: 'string' },
113
+ capture: { type: 'string' },
114
+ crop: { type: 'string' },
115
+ highlight: { type: 'string' },
116
+ format: { type: 'string' },
117
+ viewport: { type: 'string' },
118
+ scale: { type: 'string' },
119
+ 'color-scheme': { type: 'string' },
104
120
  'llm-timeout': { type: 'string' },
105
121
  retries: { type: 'string' },
106
122
  'retry-delay-ms': { type: 'string' },
@@ -116,6 +132,8 @@ async function main() {
116
132
  timeout: { type: 'string' },
117
133
  extension: { type: 'string', multiple: true },
118
134
  'user-data-dir': { type: 'string' },
135
+ 'profile-dir': { type: 'string' },
136
+ 'cdp-url': { type: 'string' },
119
137
  wallet: { type: 'boolean' },
120
138
  'wallet-auto-approve': { type: 'boolean' },
121
139
  'wallet-password': { type: 'string' },
@@ -141,6 +159,11 @@ async function main() {
141
159
  'block-analytics': { type: 'boolean', default: false },
142
160
  'block-images': { type: 'boolean', default: false },
143
161
  'block-media': { type: 'boolean', default: false },
162
+ // Auth
163
+ fill: { type: 'string', multiple: true },
164
+ cookie: { type: 'string', multiple: true },
165
+ 'wait-for': { type: 'string' },
166
+ 'wait-timeout': { type: 'string' },
144
167
  help: { type: 'boolean', short: 'h', default: false },
145
168
  version: { type: 'boolean', short: 'v', default: false },
146
169
  },
@@ -160,6 +183,32 @@ async function main() {
160
183
  cliError('--url is required for design-audit.');
161
184
  process.exit(1);
162
185
  }
186
+ // --design-compare mode
187
+ if (values['design-compare']) {
188
+ if (!values['compare-url']) {
189
+ cliError('--compare-url is required with --design-compare.');
190
+ process.exit(1);
191
+ }
192
+ const { runDesignCompare } = await import('./design/compare.js');
193
+ await runDesignCompare({
194
+ urlA: values.url,
195
+ urlB: values['compare-url'],
196
+ headless: values.headless,
197
+ outputDir: values.sink,
198
+ });
199
+ process.exit(0);
200
+ }
201
+ // --rip mode
202
+ if (values.rip) {
203
+ const { ripSite } = await import('./design/rip.js');
204
+ await ripSite({
205
+ url: values.url,
206
+ pages: values.pages ? parseInt(values.pages) : undefined,
207
+ headless: values.headless,
208
+ outputDir: values.sink,
209
+ });
210
+ process.exit(0);
211
+ }
163
212
  const { runDesignAudit } = await import('./cli-design-audit.js');
164
213
  await runDesignAudit({
165
214
  url: values.url,
@@ -173,6 +222,10 @@ async function main() {
173
222
  headless: values.headless,
174
223
  debug: values.debug,
175
224
  extractTokens: values['extract-tokens'],
225
+ evolve: values.evolve,
226
+ evolveRounds: values['evolve-rounds'] ? parseInt(values['evolve-rounds']) : undefined,
227
+ projectDir: values['project-dir'],
228
+ reproducibility: values.reproducibility,
176
229
  });
177
230
  process.exit(0);
178
231
  }
@@ -208,8 +261,61 @@ async function main() {
208
261
  }
209
262
  process.exit(0);
210
263
  }
264
+ if (command === 'showcase') {
265
+ const { handleShowcase } = await import('./cli-showcase.js');
266
+ await handleShowcase({
267
+ url: values.url,
268
+ script: values.script,
269
+ capture: values.capture,
270
+ crop: values.crop,
271
+ highlight: values.highlight,
272
+ format: values.format,
273
+ viewport: values.viewport,
274
+ output: values.sink,
275
+ headless: values.headless ?? true,
276
+ colorScheme: values['color-scheme'],
277
+ scale: values.scale ? parseFloat(values.scale) : undefined,
278
+ storageState: values['storage-state'],
279
+ quality: values['quality-threshold'] ? parseInt(values['quality-threshold']) : undefined,
280
+ });
281
+ process.exit(0);
282
+ }
283
+ if (command === 'auth') {
284
+ const sub = positionals[1];
285
+ if (sub === 'save') {
286
+ const { handleAuthSave } = await import('./cli-auth.js');
287
+ await handleAuthSave({
288
+ url: values.url || positionals[2],
289
+ output: values['storage-state'] || positionals[3],
290
+ });
291
+ process.exit(0);
292
+ }
293
+ if (sub === 'login') {
294
+ const { handleAuthLogin } = await import('./cli-auth.js');
295
+ await handleAuthLogin({
296
+ url: values.url || positionals[2],
297
+ output: values['storage-state'],
298
+ fill: values.fill,
299
+ cookie: values.cookie,
300
+ waitFor: values['wait-for'],
301
+ waitTimeout: values['wait-timeout'] ? parseInt(values['wait-timeout'], 10) : undefined,
302
+ headless: values.headless,
303
+ });
304
+ process.exit(0);
305
+ }
306
+ if (sub === 'check') {
307
+ const { handleAuthCheck } = await import('./cli-auth.js');
308
+ await handleAuthCheck({
309
+ path: values['storage-state'] || positionals[2],
310
+ origin: positionals[3],
311
+ });
312
+ process.exit(0);
313
+ }
314
+ cliError(`Unknown auth subcommand: ${sub || '(none)'}. Use "auth save", "auth login", or "auth check".`);
315
+ process.exit(1);
316
+ }
211
317
  if (command !== 'run') {
212
- cliError(`Unknown command: ${command}. Use "run", "runs", or "design-audit".`);
318
+ cliError(`Unknown command: ${command}. Use "run", "runs", "design-audit", "showcase", or "auth".`);
213
319
  process.exit(1);
214
320
  }
215
321
  // Validate inputs
@@ -378,6 +484,10 @@ async function main() {
378
484
  }
379
485
  }
380
486
  }
487
+ if (values['profile-dir'])
488
+ cliOverrides.profileDir = values['profile-dir'];
489
+ if (values['cdp-url'])
490
+ cliOverrides.cdpUrl = values['cdp-url'];
381
491
  if (values.memory !== undefined || values['memory-dir']) {
382
492
  cliOverrides.memory = {
383
493
  ...(cliOverrides.memory ?? {}),
@@ -438,12 +548,8 @@ async function main() {
438
548
  cliOverrides.resourceBlocking = {
439
549
  ...(cliOverrides.resourceBlocking ?? {}),
440
550
  blockAnalytics: true,
441
- ...(profile === 'benchmark-webbench'
442
- ? {
443
- blockImages: true,
444
- blockMedia: true,
445
- }
446
- : {}),
551
+ blockImages: true,
552
+ blockMedia: true,
447
553
  };
448
554
  }
449
555
  cliOverrides.microPlan = {
@@ -677,9 +783,9 @@ async function main() {
677
783
  cliError(`storage state file not found: ${storageStatePath}`);
678
784
  process.exit(1);
679
785
  }
680
- if (launchPlan.walletMode && browserName !== 'chromium') {
681
- cliError('wallet mode currently supports Chromium only. Set --browser chromium.');
682
- process.exit(1);
786
+ if (launchPlan.persistentContext && !launchPlan.cdpUrl && browserName !== 'chromium') {
787
+ const feature = launchPlan.walletMode ? 'Wallet mode' : '--profile-dir';
788
+ throw new Error(`${feature} requires Chromium. Set --browser chromium.`);
683
789
  }
684
790
  // Ensure clean exit on interrupt
685
791
  process.on('SIGINT', () => { renderer?.destroy(); process.exit(130); });
@@ -690,145 +796,179 @@ async function main() {
690
796
  let persistentContext;
691
797
  let stopWalletAutoApprover;
692
798
  const launchDiagnostics = {};
693
- if (launchPlan.walletMode) {
799
+ // CDP connection — attach to an existing browser (Atlas, Chrome, Brave, etc.)
800
+ let cdpUrl = launchPlan.cdpUrl || process.env.BROWSER_ENDPOINT;
801
+ let cdpConnected = false;
802
+ if (cdpUrl) {
803
+ // Auto-discover WebSocket URL from HTTP endpoint
804
+ if (cdpUrl.startsWith('http://') || cdpUrl.startsWith('https://')) {
805
+ try {
806
+ const versionUrl = cdpUrl.replace(/\/$/, '') + '/json/version';
807
+ const res = await fetch(versionUrl);
808
+ const info = await res.json();
809
+ if (info.webSocketDebuggerUrl) {
810
+ if (info.Browser && !quiet)
811
+ cliLog('cdp', `connected to ${info.Browser}`);
812
+ cdpUrl = info.webSocketDebuggerUrl;
813
+ }
814
+ }
815
+ catch {
816
+ // Fall through — try the URL as-is
817
+ }
818
+ }
819
+ const cdpStartedAt = Date.now();
820
+ if (cdpUrl.includes('/devtools/') || browserName === 'chromium') {
821
+ browser = await chromium.connectOverCDP(cdpUrl);
822
+ }
823
+ else {
824
+ const browserType = browserName === 'firefox' ? firefox : browserName === 'webkit' ? webkit : chromium;
825
+ browser = await browserType.connect(cdpUrl);
826
+ }
827
+ launchDiagnostics.browserLaunchMs = Date.now() - cdpStartedAt;
828
+ launchDiagnostics.cdpUrl = cdpUrl;
829
+ cdpConnected = true;
830
+ }
831
+ else if (launchPlan.persistentContext) {
694
832
  for (const extensionPath of launchPlan.extensionPaths) {
695
833
  if (!fs.existsSync(extensionPath)) {
696
834
  throw new Error(`Wallet extension path does not exist: ${extensionPath}`);
697
835
  }
698
836
  }
699
- const userDataDir = launchPlan.userDataDir ?? path.resolve('.agent-wallet-profile');
837
+ const userDataDir = launchPlan.userDataDir ?? path.resolve(launchPlan.walletMode ? '.agent-wallet-profile' : '.agent-profile');
700
838
  fs.mkdirSync(userDataDir, { recursive: true });
701
839
  const persistentLaunchStartedAt = Date.now();
702
840
  persistentContext = await chromium.launchPersistentContext(userDataDir, {
703
841
  channel: isStealthProfile ? 'chrome' : 'chromium',
704
- headless: false,
842
+ headless: launchPlan.headless,
705
843
  args: launchPlan.browserArgs,
706
844
  viewport,
707
845
  recordVideo: { dir: videoDir, size: viewport },
708
846
  });
709
847
  launchDiagnostics.browserLaunchMs = Date.now() - persistentLaunchStartedAt;
710
848
  await applyStorageStateToPersistentContext(persistentContext, storageStatePath);
711
- const walletConfig = driverConfig.wallet ?? {};
712
- // Intercept page-level JSON-RPC so dApps see wallet balances from the
713
- // local Anvil fork. Only forward user-specific calls (eth_getBalance
714
- // for the wallet, eth_call with wallet address in calldata). Pool data
715
- // and protocol calls go to real endpoints for reliability.
716
- const walletRpcUrl = walletConfig.preflight?.chain?.rpcUrl;
717
- if (walletRpcUrl) {
718
- // Default to Anvil's first derived address if no wallet address configured
719
- const walletAddrFull = (walletConfig.address ?? '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266').toLowerCase();
720
- const walletAddrHex = walletAddrFull.replace('0x', '');
721
- await persistentContext.route('**/*', async (route) => {
722
- try {
723
- const frame = route.request().frame();
724
- if (frame && frame.url().startsWith('chrome-extension://')) {
849
+ if (launchPlan.walletMode) {
850
+ const walletConfig = driverConfig.wallet ?? {};
851
+ // Intercept page-level JSON-RPC so dApps see wallet balances from the
852
+ // local Anvil fork. Only forward user-specific calls (eth_getBalance
853
+ // for the wallet, eth_call with wallet address in calldata). Pool data
854
+ // and protocol calls go to real endpoints for reliability.
855
+ const walletRpcUrl = walletConfig.preflight?.chain?.rpcUrl;
856
+ if (walletRpcUrl) {
857
+ // Default to Anvil's first derived address if no wallet address configured
858
+ const walletAddrFull = (walletConfig.address ?? '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266').toLowerCase();
859
+ const walletAddrHex = walletAddrFull.replace('0x', '');
860
+ await persistentContext.route('**/*', async (route) => {
861
+ try {
862
+ const frame = route.request().frame();
863
+ if (frame && frame.url().startsWith('chrome-extension://')) {
864
+ await route.continue();
865
+ return;
866
+ }
867
+ }
868
+ catch {
725
869
  await route.continue();
726
870
  return;
727
871
  }
728
- }
729
- catch {
730
- await route.continue();
731
- return;
732
- }
733
- if (route.request().method() !== 'POST') {
734
- await route.continue();
735
- return;
736
- }
737
- const ct = route.request().headers()['content-type'] ?? '';
738
- if (!ct.includes('json')) {
739
- await route.continue();
740
- return;
741
- }
742
- const postData = route.request().postData();
743
- if (!postData) {
744
- await route.continue();
745
- return;
746
- }
747
- try {
748
- const body = JSON.parse(postData);
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)
872
+ if (route.request().method() !== 'POST') {
873
+ await route.continue();
874
+ return;
875
+ }
876
+ const ct = route.request().headers()['content-type'] ?? '';
877
+ if (!ct.includes('json')) {
878
+ await route.continue();
879
+ return;
880
+ }
881
+ const postData = route.request().postData();
882
+ if (!postData) {
883
+ await route.continue();
884
+ return;
885
+ }
886
+ try {
887
+ const body = JSON.parse(postData);
888
+ const items = Array.isArray(body) ? body : [body];
889
+ // Check if any item involves the wallet (balance, contract call, simulation)
890
+ const isUserQuery = items.some((item) => {
891
+ const method = item.method;
892
+ if (!method)
763
893
  return false;
764
- const from = txObj.from?.toLowerCase() ?? '';
765
- const data = txObj.data?.toLowerCase() ?? '';
766
- return from === walletAddrFull || data.includes(walletAddrHex);
767
- }
768
- if (method === 'eth_getTransactionCount') {
769
- const params = item.params;
770
- return params?.[0]?.toLowerCase() === walletAddrFull;
894
+ if (method === 'eth_getBalance') {
895
+ const params = item.params;
896
+ return params?.[0]?.toLowerCase() === walletAddrFull;
897
+ }
898
+ if (method === 'eth_call' || method === 'eth_estimateGas') {
899
+ const params = item.params;
900
+ const txObj = params?.[0];
901
+ if (!txObj)
902
+ return false;
903
+ const from = txObj.from?.toLowerCase() ?? '';
904
+ const data = txObj.data?.toLowerCase() ?? '';
905
+ return from === walletAddrFull || data.includes(walletAddrHex);
906
+ }
907
+ if (method === 'eth_getTransactionCount') {
908
+ const params = item.params;
909
+ return params?.[0]?.toLowerCase() === walletAddrFull;
910
+ }
911
+ return false;
912
+ });
913
+ if (!isUserQuery) {
914
+ await route.continue();
915
+ return;
771
916
  }
772
- return false;
773
- });
774
- if (!isUserQuery) {
917
+ // Normalize: some dApps (Aave) omit jsonrpc/id — Anvil requires them
918
+ let nextId = 1;
919
+ const normalized = items.map((item) => {
920
+ const out = { ...item, jsonrpc: '2.0', id: item.id ?? nextId++ };
921
+ delete out.chainId;
922
+ return out;
923
+ });
924
+ const payload = Array.isArray(body) ? normalized : normalized[0];
925
+ const res = await fetch(walletRpcUrl, {
926
+ method: 'POST',
927
+ headers: { 'Content-Type': 'application/json' },
928
+ body: JSON.stringify(payload),
929
+ });
930
+ await route.fulfill({
931
+ status: res.status,
932
+ contentType: 'application/json',
933
+ body: await res.text(),
934
+ });
935
+ }
936
+ catch {
775
937
  await route.continue();
776
- return;
777
938
  }
778
- // Normalize: some dApps (Aave) omit jsonrpc/id — Anvil requires them
779
- let nextId = 1;
780
- const normalized = items.map((item) => {
781
- const out = { ...item, jsonrpc: '2.0', id: item.id ?? nextId++ };
782
- delete out.chainId;
783
- return out;
784
- });
785
- const payload = Array.isArray(body) ? normalized : normalized[0];
786
- const res = await fetch(walletRpcUrl, {
787
- method: 'POST',
788
- headers: { 'Content-Type': 'application/json' },
789
- body: JSON.stringify(payload),
790
- });
791
- await route.fulfill({
792
- status: res.status,
793
- contentType: 'application/json',
794
- body: await res.text(),
795
- });
796
- }
797
- catch {
798
- await route.continue();
939
+ });
940
+ }
941
+ const shouldAutoApprove = walletConfig.autoApprove ?? true;
942
+ if (shouldAutoApprove) {
943
+ stopWalletAutoApprover = await startWalletAutoApprover(persistentContext, {
944
+ password: walletConfig.password,
945
+ tickMs: walletConfig.tickMs,
946
+ actionSelectors: walletConfig.actionSelectors,
947
+ });
948
+ }
949
+ const preflightEnabled = walletConfig.preflight?.enabled ?? true;
950
+ const preflightSeedUrls = walletConfig.preflight?.seedUrls && walletConfig.preflight.seedUrls.length > 0
951
+ ? walletConfig.preflight.seedUrls
952
+ : [...new Set(cases.map((testCase) => testCase.startUrl).filter(Boolean))];
953
+ if (preflightEnabled && preflightSeedUrls.length > 0) {
954
+ const preflight = await runWalletPreflight(persistentContext, {
955
+ seedUrls: preflightSeedUrls,
956
+ password: walletConfig.password,
957
+ actionSelectors: walletConfig.actionSelectors,
958
+ promptPaths: walletConfig.promptPaths,
959
+ connectSelectors: walletConfig.connectSelectors,
960
+ connectorSelectors: walletConfig.connectorSelectors,
961
+ requestAccounts: walletConfig.preflight?.requestAccounts,
962
+ accountsTimeoutMs: walletConfig.preflight?.accountsTimeoutMs,
963
+ maxChainSwitchAttempts: walletConfig.preflight?.maxChainSwitchAttempts,
964
+ chain: walletConfig.preflight?.chain,
965
+ log: quiet ? undefined : (message) => cliLog('wallet', message),
966
+ });
967
+ if (!preflight.ok) {
968
+ const failed = preflight.results.find((resultEntry) => !resultEntry.ready);
969
+ const details = failed?.details ?? 'unknown reason';
970
+ throw new Error(`Wallet preflight failed for ${preflight.failedUrl ?? 'unknown origin'} (${details})`);
799
971
  }
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
972
  }
833
973
  }
834
974
  }
@@ -838,94 +978,160 @@ async function main() {
838
978
  : browserName === 'webkit'
839
979
  ? webkit
840
980
  : chromium;
841
- if (process.env.BROWSER_ENDPOINT) {
842
- const endpoint = process.env.BROWSER_ENDPOINT;
843
- const browserLaunchStartedAt = Date.now();
844
- // CDP endpoint (chrome, android-chrome) vs Playwright endpoint (firefox, webkit)
845
- if (browserName === 'chromium' || endpoint.includes('/devtools/')) {
846
- browser = await chromium.connectOverCDP(endpoint);
847
- }
848
- else {
849
- browser = await browserType.connect(endpoint);
850
- }
851
- launchDiagnostics.browserLaunchMs = Date.now() - browserLaunchStartedAt;
981
+ const browserLaunchStartedAt = Date.now();
982
+ browser = await browserType.launch({
983
+ headless: launchPlan.headless,
984
+ ...(browserName === 'chromium' ? { args: launchPlan.browserArgs } : {}),
985
+ // Use system Chrome for stealth profiles — real TLS/JA3 fingerprint vs bundled Chromium
986
+ ...(isStealthProfile && browserName === 'chromium' ? { channel: 'chrome' } : {}),
987
+ });
988
+ launchDiagnostics.browserLaunchMs = Date.now() - browserLaunchStartedAt;
989
+ }
990
+ // Headless Chromium sends "HeadlessChrome/..." in the default User-Agent.
991
+ // CDNs like Akamai reject this with ERR_HTTP2_PROTOCOL_ERROR before any JS
992
+ // stealth patches can run. Build a clean UA from the browser version.
993
+ const headlessUserAgent = launchPlan.headless && browser
994
+ ? (() => {
995
+ const ver = browser.version();
996
+ const plat = process.platform;
997
+ const platformToken = plat === 'win32'
998
+ ? 'Windows NT 10.0; Win64; x64'
999
+ : plat === 'linux'
1000
+ ? 'X11; Linux x86_64'
1001
+ : 'Macintosh; Intel Mac OS X 10_15_7';
1002
+ return `Mozilla/5.0 (${platformToken}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ver} Safari/537.36`;
1003
+ })()
1004
+ : undefined;
1005
+ const driverFactory = async () => {
1006
+ const contextStartedAt = Date.now();
1007
+ // CDP: reuse the browser's default context (preserves user cookies/sessions).
1008
+ // Persistent context: use the already-opened persistent context.
1009
+ // Default: create a fresh isolated context.
1010
+ let context;
1011
+ if (cdpConnected) {
1012
+ // Reuse the user's existing browser context — cookies, localStorage, extensions intact
1013
+ const contexts = browser.contexts();
1014
+ context = contexts[0] ?? await browser.newContext({ viewport });
1015
+ }
1016
+ else if (persistentContext) {
1017
+ context = persistentContext;
852
1018
  }
853
1019
  else {
854
- const browserLaunchStartedAt = Date.now();
855
- browser = await browserType.launch({
856
- headless: launchPlan.headless,
857
- ...(browserName === 'chromium' ? { args: launchPlan.browserArgs } : {}),
858
- // Use system Chrome for stealth profiles — real TLS/JA3 fingerprint vs bundled Chromium
859
- ...(isStealthProfile && browserName === 'chromium' ? { channel: 'chrome' } : {}),
1020
+ context = await browser.newContext({
1021
+ viewport,
1022
+ recordVideo: { dir: videoDir, size: viewport },
1023
+ storageState: storageStatePath,
1024
+ locale: 'en-US',
1025
+ timezoneId: 'America/New_York',
1026
+ ...(headlessUserAgent ? { userAgent: headlessUserAgent } : {}),
1027
+ extraHTTPHeaders: {
1028
+ 'Accept-Language': 'en-US,en;q=0.9',
1029
+ },
860
1030
  });
861
- launchDiagnostics.browserLaunchMs = Date.now() - browserLaunchStartedAt;
862
1031
  }
863
- }
864
- const driverFactory = async () => {
865
- const contextStartedAt = Date.now();
866
- const context = persistentContext ?? await browser.newContext({
867
- viewport,
868
- recordVideo: { dir: videoDir, size: viewport },
869
- storageState: storageStatePath,
870
- locale: 'en-US',
871
- timezoneId: 'America/New_York',
872
- extraHTTPHeaders: {
873
- 'Accept-Language': 'en-US,en;q=0.9',
874
- },
1032
+ // Stealth patches: only for Playwright-controlled contexts (not real user browsers)
1033
+ if (!cdpConnected) {
1034
+ await context.addInitScript(`
1035
+ // navigator.webdriver explicit override (backup for --disable-blink-features)
1036
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
1037
+ // navigator.plugins empty in headless, non-empty in real browsers
1038
+ Object.defineProperty(navigator, 'plugins', {
1039
+ get: () => [
1040
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
1041
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
1042
+ { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
1043
+ ],
875
1044
  });
876
- // Stealth: patch navigator/browser properties to reduce automation fingerprint.
877
- // This runs in the browser context (not Node), so we use a string to avoid TS issues.
878
- await context.addInitScript(`
879
- // navigator.webdriver explicit override (backup for --disable-blink-features)
880
- Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
881
- // navigator.pluginsempty in headless, non-empty in real browsers
882
- Object.defineProperty(navigator, 'plugins', {
883
- get: () => [
884
- { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
885
- { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
886
- { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
887
- ],
888
- });
889
- // navigator.languages must match Accept-Language header
890
- Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
891
- // hardware signals realistic desktop values
892
- Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
893
- Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
894
- // window.chrome — full stub matching real Chrome
895
- if (!window.chrome) window.chrome = {};
896
- if (!window.chrome.runtime) window.chrome.runtime = { id: undefined };
897
- 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' } };
898
- if (!window.chrome.csi) window.chrome.csi = function() { return { onloadT: Date.now(), startE: Date.now(), pageT: Date.now() - performance.timing.navigationStart }; };
899
- 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 }; };
900
- // WebGL vendor/renderer match real GPU values
901
- try {
902
- const getParameter = WebGLRenderingContext.prototype.getParameter;
903
- WebGLRenderingContext.prototype.getParameter = function(parameter) {
904
- if (parameter === 37445) return 'Intel Inc.';
905
- if (parameter === 37446) return 'Intel Iris OpenGL Engine';
906
- return getParameter.call(this, parameter);
907
- };
908
- } catch (_) {}
909
- try {
910
- const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
911
- WebGL2RenderingContext.prototype.getParameter = function(parameter) {
912
- if (parameter === 37445) return 'Intel Inc.';
913
- if (parameter === 37446) return 'Intel Iris OpenGL Engine';
914
- return getParameter2.call(this, parameter);
915
- };
916
- } catch (_) {}
917
- // window.outerWidth/outerHeight0 in headless, match viewport in real browsers
918
- if (window.outerWidth === 0) Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth });
919
- if (window.outerHeight === 0) Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight + 85 });
920
- // Patch permissions API for notification checks
921
- try {
922
- const origQuery = navigator.permissions.query.bind(navigator.permissions);
923
- navigator.permissions.query = (params) =>
924
- params.name === 'notifications'
925
- ? Promise.resolve({ state: 'denied', onchange: null })
926
- : origQuery(params);
927
- } catch (_) {}
928
- `);
1045
+ // navigator.languages must match Accept-Language header
1046
+ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
1047
+ // hardware signals — realistic desktop values
1048
+ Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
1049
+ Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
1050
+ // window.chromefull stub matching real Chrome
1051
+ if (!window.chrome) window.chrome = {};
1052
+ if (!window.chrome.runtime) window.chrome.runtime = { id: undefined };
1053
+ 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' } };
1054
+ if (!window.chrome.csi) window.chrome.csi = function() { return { onloadT: Date.now(), startE: Date.now(), pageT: Date.now() - performance.timing.navigationStart }; };
1055
+ 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 }; };
1056
+ // WebGL vendor/renderer — match real GPU values
1057
+ try {
1058
+ const getParameter = WebGLRenderingContext.prototype.getParameter;
1059
+ WebGLRenderingContext.prototype.getParameter = function(parameter) {
1060
+ if (parameter === 37445) return 'Intel Inc.';
1061
+ if (parameter === 37446) return 'Intel Iris OpenGL Engine';
1062
+ return getParameter.call(this, parameter);
1063
+ };
1064
+ } catch (_) {}
1065
+ try {
1066
+ const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
1067
+ WebGL2RenderingContext.prototype.getParameter = function(parameter) {
1068
+ if (parameter === 37445) return 'Intel Inc.';
1069
+ if (parameter === 37446) return 'Intel Iris OpenGL Engine';
1070
+ return getParameter2.call(this, parameter);
1071
+ };
1072
+ } catch (_) {}
1073
+ // window.outerWidth/outerHeight 0 in headless, match viewport in real browsers
1074
+ if (window.outerWidth === 0) Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth });
1075
+ if (window.outerHeight === 0) Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight + 85 });
1076
+ // Patch permissions API — cover all permission types bots commonly mis-handle
1077
+ try {
1078
+ const origQuery = navigator.permissions.query.bind(navigator.permissions);
1079
+ navigator.permissions.query = (params) => {
1080
+ const deny = ['notifications', 'geolocation', 'camera', 'microphone', 'payment-handler'];
1081
+ if (deny.includes(params.name))
1082
+ return Promise.resolve({ state: 'denied', onchange: null });
1083
+ return origQuery(params);
1084
+ };
1085
+ } catch (_) {}
1086
+ // Canvas fingerprint noise add imperceptible per-session noise to canvas readback
1087
+ // so each session produces a unique fingerprint (defeats static fingerprint matching)
1088
+ try {
1089
+ const seed = Math.random() * 0xffff | 0;
1090
+ const noisify = (canvas) => {
1091
+ try {
1092
+ const ctx = canvas.getContext('2d');
1093
+ if (!ctx) return;
1094
+ const { width: w, height: h } = canvas;
1095
+ if (w === 0 || h === 0) return;
1096
+ const img = ctx.getImageData(0, 0, w, h);
1097
+ const d = img.data;
1098
+ for (let i = 0; i < d.length; i += 4) {
1099
+ // deterministic per-pixel noise from seed + position
1100
+ d[i] = d[i] ^ ((seed + i) & 1);
1101
+ }
1102
+ ctx.putImageData(img, 0, 0);
1103
+ } catch (_) {}
1104
+ };
1105
+ const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
1106
+ HTMLCanvasElement.prototype.toDataURL = function(...args) {
1107
+ noisify(this);
1108
+ return origToDataURL.apply(this, args);
1109
+ };
1110
+ const origToBlob = HTMLCanvasElement.prototype.toBlob;
1111
+ HTMLCanvasElement.prototype.toBlob = function(...args) {
1112
+ noisify(this);
1113
+ return origToBlob.apply(this, args);
1114
+ };
1115
+ } catch (_) {}
1116
+ // Fix CDP screenX/screenY bug — CDP Input.dispatchMouseEvent sets
1117
+ // screenX=clientX, screenY=clientY which never happens in real browsers.
1118
+ // Cloudflare Turnstile actively checks this. Add a per-session window
1119
+ // offset so screenX/screenY are realistic and internally consistent.
1120
+ try {
1121
+ const winX = Math.floor(Math.random() * 200) + 50;
1122
+ const winY = Math.floor(Math.random() * 100) + 50;
1123
+ const chrome = 85;
1124
+ Object.defineProperty(MouseEvent.prototype, 'screenX', {
1125
+ get() { return this.clientX + winX; },
1126
+ configurable: true,
1127
+ });
1128
+ Object.defineProperty(MouseEvent.prototype, 'screenY', {
1129
+ get() { return this.clientY + winY + chrome; },
1130
+ configurable: true,
1131
+ });
1132
+ } catch (_) {}
1133
+ `);
1134
+ }
929
1135
  const contextCreateMs = Date.now() - contextStartedAt;
930
1136
  const pageStartedAt = Date.now();
931
1137
  const page = await context.newPage();
@@ -957,8 +1163,9 @@ async function main() {
957
1163
  contextCreateMs,
958
1164
  pageCreateMs,
959
1165
  resourceBlockingSetupMs,
960
- storageStateApplied: Boolean(storageStatePath),
1166
+ storageStateApplied: Boolean(storageStatePath) && !cdpConnected,
961
1167
  persistentContext: Boolean(persistentContext),
1168
+ cdpConnected,
962
1169
  };
963
1170
  // Wrap in a Driver that properly tears down context on close
964
1171
  const wrappedDriver = {
@@ -970,7 +1177,7 @@ async function main() {
970
1177
  async close() {
971
1178
  await driver.close().catch(() => { });
972
1179
  await page.close().catch(() => { });
973
- if (!persistentContext) {
1180
+ if (!persistentContext && !cdpConnected) {
974
1181
  await context.close().catch(() => { });
975
1182
  }
976
1183
  },