@fudrouter/fsrouter 0.6.79 → 0.6.81

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.
@@ -0,0 +1,652 @@
1
+ #!/usr/bin/env python3
2
+ """OpenVecta account auto-signup via Camoufox (anti-fingerprint) + Fsmail email verification.
3
+
4
+ Flow:
5
+ 1. Navigate to openvecta.com → click "Get started"
6
+ 2. Privy modal opens → enter email → receive verification code via Fsmail
7
+ 3. Enter verification code → account created (Privy auto-creates Solana wallet)
8
+ 4. Navigate to dashboard → API keys tab → create new key
9
+ 5. Copy the ov_sk_live_... key
10
+
11
+ Outputs JSON lines to stdout:
12
+ {"step": "..."} — progress update
13
+ {"status": "success", "api_key": "ov_sk_live_...", "email": "..."} — final result
14
+ {"status": "error", "error": "..."} — failure
15
+ """
16
+
17
+ import sys
18
+ import json
19
+ import argparse
20
+ import time
21
+ import random
22
+ import string
23
+ import re
24
+ import urllib.request
25
+ import urllib.parse
26
+ import urllib.error
27
+ from pathlib import Path
28
+
29
+
30
+ # ── Stdout JSON helpers ────────────────────────────────────────────────────────
31
+ def emit(obj):
32
+ print(json.dumps(obj), flush=True)
33
+
34
+
35
+ def log_step(msg):
36
+ emit({"step": msg})
37
+
38
+
39
+ def success(api_key, email):
40
+ emit({"status": "success", "api_key": api_key, "email": email})
41
+
42
+
43
+ def die(msg):
44
+ emit({"status": "error", "error": msg})
45
+ sys.exit(1)
46
+
47
+
48
+ # ── Fsmail helpers ─────────────────────────────────────────────────────────────
49
+ def fsmail_request(base_url, api_key, path, method="GET", data=None, host_header=None):
50
+ url = base_url.rstrip("/") + "/api" + path
51
+ req = urllib.request.Request(url, method=method)
52
+ req.add_header("Authorization", f"Bearer {api_key}")
53
+ req.add_header("X-API-Key", api_key)
54
+ req.add_header("Content-Type", "application/json")
55
+ req.add_header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
56
+ req.add_header("Accept", "application/json, */*")
57
+ if host_header:
58
+ req.add_header("Host", host_header)
59
+ elif "localhost" in base_url or "127.0.0.1" in base_url:
60
+ req.add_header("Host", "fsmail.klipers.site")
61
+ if data:
62
+ req.data = json.dumps(data).encode()
63
+ with urllib.request.urlopen(req, timeout=15) as resp:
64
+ return json.loads(resp.read())
65
+
66
+
67
+ def create_fsmail_inbox(base_url, api_key, email):
68
+ """Create inbox by splitting email into alias + domain."""
69
+ try:
70
+ alias, domain = email.split("@", 1)
71
+ fsmail_request(base_url, api_key, "/inboxes", method="POST",
72
+ data={"alias": alias, "domain": domain})
73
+ except Exception:
74
+ pass # might already exist
75
+
76
+
77
+ def wait_for_openvecta_verify_email(base_url, api_key, email, timeout=300):
78
+ """Wait for OpenVecta/Privy verification email and extract the code or link."""
79
+ log_step(f"Menunggu email verifikasi OpenVecta ({email})...")
80
+ alias = email.split("@")[0]
81
+ deadline = time.time() + timeout
82
+ seen_ids = set()
83
+ while time.time() < deadline:
84
+ try:
85
+ data = fsmail_request(base_url, api_key, f"/inboxes/{urllib.parse.quote(alias)}/messages")
86
+ messages = data.get("messages", [])
87
+ for msg in messages:
88
+ msg_id = msg.get("id", "")
89
+ subject = msg.get("subject", "")
90
+ if msg_id in seen_ids:
91
+ continue
92
+ seen_ids.add(msg_id)
93
+ subj_lower = subject.lower()
94
+ is_ov_email = (
95
+ "openvecta" in subj_lower or
96
+ "privy" in subj_lower or
97
+ "verify" in subj_lower or
98
+ "confirm" in subj_lower or
99
+ "code" in subj_lower or
100
+ "login" in subj_lower or
101
+ "sign in" in subj_lower or
102
+ "magic" in subj_lower or
103
+ "verification" in subj_lower
104
+ )
105
+ if is_ov_email:
106
+ # Fetch full message body
107
+ try:
108
+ full = fsmail_request(base_url, api_key, f"/messages/{urllib.parse.quote(msg_id)}")
109
+ msg_body = full.get("message", full)
110
+ body = msg_body.get("body", msg_body.get("html", msg_body.get("text", "")))
111
+ except Exception:
112
+ body = msg.get("snippet", "")
113
+
114
+ # Look for verification link
115
+ link_patterns = [
116
+ r'https://auth\.privy\.io[^\s\'"<>]+',
117
+ r'https://openvecta\.com[^\s\'"<>]*verify[^\s\'"<>]*',
118
+ r'https://openvecta\.com[^\s\'"<>]*confirm[^\s\'"<>]*',
119
+ r'https://openvecta\.com[^\s\'"<>]*auth[^\s\'"<>]*',
120
+ r'https://[^\s\'"<>]*privy[^\s\'"<>]*',
121
+ ]
122
+ for pat in link_patterns:
123
+ links = re.findall(pat, body)
124
+ if links:
125
+ link = links[0].rstrip(".")
126
+ log_step(f"Link verifikasi ditemukan!")
127
+ return {"type": "link", "value": link}
128
+
129
+ # Look for verification code (typically 6 digits)
130
+ code_patterns = [
131
+ r'(?:code|otp|pin|verification)[:\s]*(\d{4,8})',
132
+ r'(\d{6})',
133
+ r'<strong[^>]*>(\d{4,8})</strong>',
134
+ r'<span[^>]*>(\d{6})</span>',
135
+ r'<h1[^>]*>(\d{6})</h1>',
136
+ r'<p[^>]*>\s*(\d{6})\s*</p>',
137
+ ]
138
+ for pat in code_patterns:
139
+ codes = re.findall(pat, body, re.IGNORECASE)
140
+ if codes:
141
+ code = codes[0].strip()
142
+ if len(code) >= 4:
143
+ log_step(f"Kode verifikasi ditemukan: {code}")
144
+ return {"type": "code", "value": code}
145
+
146
+ # If we found the email but no link/code, return the body for debugging
147
+ log_step(f"Email ditemukan tapi tidak ada link/kode yang bisa diekstrak")
148
+ return {"type": "unknown", "value": body[:500]}
149
+ except Exception as e:
150
+ log_step(f"Fsmail poll error: {e}")
151
+ time.sleep(5)
152
+ return None
153
+
154
+
155
+ # ── Human-like typing ──────────────────────────────────────────────────────────
156
+ def human_type(page, selector, text, delay_min=50, delay_max=150):
157
+ """Type text with human-like delays."""
158
+ el = page.locator(selector).first
159
+ el.click()
160
+ time.sleep(0.3)
161
+ for char in text:
162
+ el.type(char, delay=random.randint(delay_min, delay_max))
163
+ time.sleep(0.5)
164
+
165
+
166
+ def safe_click(page, selector, timeout=10000):
167
+ """Click element with retry."""
168
+ try:
169
+ el = page.locator(selector).first
170
+ el.wait_for(state="visible", timeout=timeout)
171
+ el.click()
172
+ time.sleep(1)
173
+ return True
174
+ except Exception as e:
175
+ log_step(f"Click failed ({selector}): {e}")
176
+ return False
177
+
178
+
179
+ def wait_for_any(page, selectors, timeout=15000):
180
+ """Wait for any of the selectors to appear, return the first matching selector."""
181
+ deadline = time.time() + timeout / 1000
182
+ while time.time() < deadline:
183
+ for sel in selectors:
184
+ try:
185
+ el = page.locator(sel).first
186
+ if el.count() > 0 and el.is_visible(timeout=500):
187
+ return sel
188
+ except Exception:
189
+ continue
190
+ time.sleep(0.5)
191
+ return None
192
+
193
+
194
+ # ── Main ───────────────────────────────────────────────────────────────────────
195
+ def main():
196
+ parser = argparse.ArgumentParser()
197
+ parser.add_argument("--email", required=True)
198
+ parser.add_argument("--password", required=True, help="Used as placeholder, not for OpenVecta login")
199
+ parser.add_argument("--fsmail-base-url", default="")
200
+ parser.add_argument("--fsmail-api-key", default="")
201
+ parser.add_argument("--fsmail-domain", default="")
202
+ parser.add_argument("--profiles-dir", default="profiles/openvecta")
203
+ parser.add_argument("--headless", action="store_true")
204
+ parser.add_argument("--proxy-server")
205
+ parser.add_argument("--proxy-user")
206
+ parser.add_argument("--proxy-pass")
207
+ parser.add_argument("--stagger-delay", type=int, default=0)
208
+ args = parser.parse_args()
209
+
210
+ email = args.email
211
+
212
+ # Import Camoufox
213
+ try:
214
+ from camoufox.sync_api import Camoufox
215
+ except ImportError:
216
+ die("Camoufox tidak terinstall. Jalankan: pip install camoufox && python -m camoufox fetch")
217
+
218
+ profiles_dir = Path(args.profiles_dir)
219
+ profiles_dir.mkdir(parents=True, exist_ok=True)
220
+
221
+ # Pre-create Fsmail inbox if we have credentials
222
+ fsmail_ok = bool(args.fsmail_base_url and args.fsmail_api_key and args.fsmail_domain)
223
+ if fsmail_ok:
224
+ log_step(f"Membuat inbox Fsmail untuk {email}...")
225
+ try:
226
+ create_fsmail_inbox(args.fsmail_base_url, args.fsmail_api_key, email)
227
+ except Exception as e:
228
+ log_step(f"Fsmail inbox warning: {e}")
229
+
230
+ log_step("Meluncurkan browser Camoufox (anti-fingerprint)...")
231
+
232
+ if args.stagger_delay > 0:
233
+ log_step(f"Stagger delay {args.stagger_delay}s...")
234
+ time.sleep(args.stagger_delay)
235
+
236
+ proxy_dict = None
237
+ if args.proxy_server:
238
+ proxy_dict = {"server": args.proxy_server}
239
+ if args.proxy_user:
240
+ proxy_dict["username"] = args.proxy_user
241
+ if args.proxy_pass:
242
+ proxy_dict["password"] = args.proxy_pass
243
+
244
+ launch_kwargs = dict(
245
+ headless=args.headless,
246
+ os="windows",
247
+ locale="en-US",
248
+ )
249
+ if proxy_dict:
250
+ launch_kwargs["proxy"] = proxy_dict
251
+ launch_kwargs["geoip"] = True
252
+
253
+ def _make_camoufox(kw):
254
+ """Launch Camoufox, stripping unsupported kwargs one by one."""
255
+ try:
256
+ return Camoufox(**kw)
257
+ except TypeError:
258
+ kw.pop("os", None)
259
+ try:
260
+ return Camoufox(**kw)
261
+ except TypeError:
262
+ kw.pop("locale", None)
263
+ return Camoufox(**kw)
264
+
265
+ try:
266
+ browser_ctx = _make_camoufox(dict(launch_kwargs))
267
+ except Exception as _pe:
268
+ _ps = str(_pe)
269
+ if proxy_dict and any(k in _ps for k in ("InvalidProxy", "Tunnel connection", "Failed to connect to proxy", "ProxyError")):
270
+ log_step(f"Proxy dead ({proxy_dict.get('server', '?')}) — fallback tanpa proxy")
271
+ launch_kwargs.pop("proxy", None)
272
+ launch_kwargs.pop("geoip", None)
273
+ browser_ctx = _make_camoufox(dict(launch_kwargs))
274
+ else:
275
+ raise
276
+
277
+ with browser_ctx as browser:
278
+ page = browser.new_page()
279
+
280
+ try:
281
+ # ── Step 1: Navigate to OpenVecta ─────────────────────────────────
282
+ log_step("Membuka openvecta.com...")
283
+ page.goto("https://openvecta.com", wait_until="domcontentloaded", timeout=30000)
284
+ time.sleep(3)
285
+
286
+ # ── Step 2: Click "Get started" to open Privy auth ────────────────
287
+ log_step("Mencari tombol 'Get started'...")
288
+ get_started = page.locator("button:has-text('Get started')").first
289
+ get_started.wait_for(state="visible", timeout=15000)
290
+ get_started.click()
291
+ time.sleep(3)
292
+
293
+ # ── Step 3: Handle Privy auth modal ───────────────────────────────
294
+ log_step("Menunggu modal Privy auth...")
295
+
296
+ # Privy modal may appear as an iframe or overlay
297
+ # Try to find the email input in Privy modal
298
+ privy_email_input = None
299
+
300
+ # Method 1: Look for input in Privy iframe
301
+ for attempt in range(3):
302
+ frames = page.frames
303
+ for frame in frames:
304
+ try:
305
+ url = frame.url or ""
306
+ if "privy" in url:
307
+ log_step(f"Frame Privy ditemukan: {url[:80]}")
308
+ email_input = frame.locator("input[type='email'], input[placeholder*='email' i], input[name='email']").first
309
+ if email_input.count() > 0:
310
+ privy_email_input = email_input
311
+ break
312
+ except Exception:
313
+ continue
314
+ if privy_email_input:
315
+ break
316
+ # Also check main page
317
+ try:
318
+ email_input = page.locator("input[type='email'], input[placeholder*='email' i], input[name='email']").first
319
+ if email_input.count() > 0 and email_input.is_visible(timeout=2000):
320
+ privy_email_input = email_input
321
+ break
322
+ except Exception:
323
+ pass
324
+ time.sleep(2)
325
+
326
+ if not privy_email_input:
327
+ # Take screenshot for debugging
328
+ try:
329
+ page.screenshot(path="/tmp/openvecta_no_privy.png")
330
+ log_step("Screenshot: /tmp/openvecta_no_privy.png")
331
+ except Exception:
332
+ pass
333
+ die("Tidak dapat menemukan input email Privy. Mungkin UI berubah.")
334
+
335
+ # ── Step 4: Enter email in Privy ──────────────────────────────────
336
+ log_step(f"Memasukkan email: {email}")
337
+ privy_email_input.click()
338
+ time.sleep(0.5)
339
+ privy_email_input.fill("")
340
+ time.sleep(0.3)
341
+ # Type with human-like delays
342
+ for char in email:
343
+ privy_email_input.type(char, delay=random.randint(30, 80))
344
+ time.sleep(1)
345
+
346
+ # Click the submit/continue button
347
+ log_step("Mengklik tombol submit...")
348
+ # Look for submit button in the same context
349
+ submit_clicked = False
350
+ for frame in page.frames:
351
+ try:
352
+ submit_btn = frame.locator("button[type='submit'], button:has-text('Continue'), button:has-text('Sign in'), button:has-text('Log in'), button:has-text('Submit')").first
353
+ if submit_btn.count() > 0 and submit_btn.is_visible(timeout=2000):
354
+ submit_btn.click()
355
+ submit_clicked = True
356
+ break
357
+ except Exception:
358
+ continue
359
+
360
+ if not submit_clicked:
361
+ # Try main page
362
+ try:
363
+ submit_btn = page.locator("button[type='submit'], button:has-text('Continue'), button:has-text('Sign in'), button:has-text('Log in')").first
364
+ if submit_btn.count() > 0:
365
+ submit_btn.click()
366
+ submit_clicked = True
367
+ except Exception:
368
+ pass
369
+
370
+ if not submit_clicked:
371
+ # Try pressing Enter
372
+ page.keyboard.press("Enter")
373
+
374
+ time.sleep(3)
375
+
376
+ # ── Step 5: Wait for verification code/link via Fsmail ────────────
377
+ if not fsmail_ok:
378
+ die("Fsmail credentials not provided. Cannot verify email.")
379
+
380
+ verify_result = wait_for_openvecta_verify_email(
381
+ args.fsmail_base_url, args.fsmail_api_key, email, timeout=300
382
+ )
383
+
384
+ if not verify_result:
385
+ try:
386
+ page.screenshot(path="/tmp/openvecta_no_verify.png")
387
+ log_step("Screenshot: /tmp/openvecta_no_verify.png")
388
+ except Exception:
389
+ pass
390
+ die("Timeout menunggu email verifikasi OpenVecta.")
391
+
392
+ # ── Step 6: Handle verification ───────────────────────────────────
393
+ if verify_result["type"] == "link":
394
+ log_step("Membuka link verifikasi...")
395
+ page.goto(verify_result["value"], wait_until="domcontentloaded", timeout=30000)
396
+ time.sleep(5)
397
+ elif verify_result["type"] == "code":
398
+ code = verify_result["value"]
399
+ log_step(f"Memasukkan kode verifikasi: {code}")
400
+ # Find code input fields (Privy typically has separate digit inputs or a single input)
401
+ code_entered = False
402
+
403
+ # Method 1: Single code input
404
+ for frame in page.frames:
405
+ try:
406
+ code_input = frame.locator("input[type='text'], input[type='number'], input[type='tel'], input[inputmode='numeric']").first
407
+ if code_input.count() > 0 and code_input.is_visible(timeout=2000):
408
+ code_input.click()
409
+ code_input.fill(code)
410
+ code_entered = True
411
+ break
412
+ except Exception:
413
+ continue
414
+
415
+ # Method 2: Multiple single-digit inputs (OTP style)
416
+ if not code_entered:
417
+ for frame in page.frames:
418
+ try:
419
+ digit_inputs = frame.locator("input[maxlength='1']")
420
+ count = digit_inputs.count()
421
+ if count >= 4:
422
+ for i, digit in enumerate(code[:count]):
423
+ if i < count:
424
+ digit_inputs.nth(i).fill(digit)
425
+ time.sleep(0.1)
426
+ code_entered = True
427
+ break
428
+ except Exception:
429
+ continue
430
+
431
+ if not code_entered:
432
+ # Try main page
433
+ try:
434
+ code_input = page.locator("input[type='text'], input[type='number']").first
435
+ if code_input.count() > 0:
436
+ code_input.fill(code)
437
+ code_entered = True
438
+ except Exception:
439
+ pass
440
+
441
+ time.sleep(3)
442
+
443
+ # Click verify/submit button
444
+ for frame in page.frames:
445
+ try:
446
+ verify_btn = frame.locator("button:has-text('Verify'), button:has-text('Confirm'), button:has-text('Submit'), button[type='submit']").first
447
+ if verify_btn.count() > 0 and verify_btn.is_visible(timeout=2000):
448
+ verify_btn.click()
449
+ break
450
+ except Exception:
451
+ continue
452
+
453
+ time.sleep(5)
454
+ else:
455
+ log_step(f"Email ditemukan tapi format tidak dikenali: {str(verify_result['value'])[:200]}")
456
+
457
+ # ── Step 7: Wait for dashboard to load ────────────────────────────
458
+ log_step("Menunggu dashboard OpenVecta...")
459
+ time.sleep(5)
460
+
461
+ # Check if we need to navigate to dashboard
462
+ current_url = page.url
463
+ if "dashboard" not in current_url:
464
+ log_step("Navigasi ke dashboard...")
465
+ page.goto("https://openvecta.com/dashboard", wait_until="domcontentloaded", timeout=30000)
466
+ time.sleep(5)
467
+
468
+ # ── Step 8: Find API keys tab and create key ──────────────────────
469
+ log_step("Mencari tab API keys...")
470
+
471
+ # Look for API keys tab/button
472
+ api_keys_clicked = False
473
+ for selector in [
474
+ "text=API keys",
475
+ "text=API Keys",
476
+ "text=Keys",
477
+ "button:has-text('API')",
478
+ "a:has-text('API keys')",
479
+ "[data-tab='keys']",
480
+ "text=api keys",
481
+ ]:
482
+ try:
483
+ el = page.locator(selector).first
484
+ if el.count() > 0 and el.is_visible(timeout=3000):
485
+ el.click()
486
+ api_keys_clicked = True
487
+ log_step(f"Tab API keys ditemukan dan diklik")
488
+ time.sleep(2)
489
+ break
490
+ except Exception:
491
+ continue
492
+
493
+ if not api_keys_clicked:
494
+ log_step("Tab API keys tidak ditemukan, mencoba screenshot...")
495
+ try:
496
+ page.screenshot(path="/tmp/openvecta_dashboard.png")
497
+ log_step("Screenshot: /tmp/openvecta_dashboard.png")
498
+ except Exception:
499
+ pass
500
+
501
+ # Click "Create key" button
502
+ log_step("Mencari tombol 'Create key'...")
503
+ create_clicked = False
504
+ for selector in [
505
+ "button:has-text('Create key')",
506
+ "button:has-text('Create Key')",
507
+ "button:has-text('Generate')",
508
+ "button:has-text('New key')",
509
+ "button:has-text('Add key')",
510
+ "text=Create key",
511
+ "text=Generate key",
512
+ ]:
513
+ try:
514
+ el = page.locator(selector).first
515
+ if el.count() > 0 and el.is_visible(timeout=3000):
516
+ el.click()
517
+ create_clicked = True
518
+ log_step("Tombol 'Create key' diklik")
519
+ time.sleep(3)
520
+ break
521
+ except Exception:
522
+ continue
523
+
524
+ if not create_clicked:
525
+ try:
526
+ page.screenshot(path="/tmp/openvecta_no_create_btn.png")
527
+ log_step("Screenshot: /tmp/openvecta_no_create_btn.png")
528
+ except Exception:
529
+ pass
530
+ die("Tidak dapat menemukan tombol 'Create key'. Mungkin akun belum di-fund atau UI berubah.")
531
+
532
+ # ── Step 9: Copy the generated API key ────────────────────────────
533
+ log_step("Mengambil API key yang dihasilkan...")
534
+ time.sleep(3)
535
+
536
+ api_key = None
537
+
538
+ # Method 1: Look for text matching ov_sk_ pattern on page
539
+ try:
540
+ page_text = page.content()
541
+ ov_matches = re.findall(r'(ov_sk_live_[A-Za-z0-9_\-]{20,})', page_text)
542
+ if ov_matches:
543
+ api_key = ov_matches[0]
544
+ log_step(f"API key ditemukan di halaman: {api_key[:20]}...")
545
+ except Exception:
546
+ pass
547
+
548
+ # Method 2: Look in code/pre elements
549
+ if not api_key:
550
+ for selector in ["code", "pre", "[class*='key']", "[class*='token']", "[class*='secret']"]:
551
+ try:
552
+ el = page.locator(selector).first
553
+ if el.count() > 0:
554
+ text = el.text_content() or ""
555
+ ov_match = re.search(r'(ov_sk_live_[A-Za-z0-9_\-]{20,})', text)
556
+ if ov_match:
557
+ api_key = ov_match.group(1)
558
+ log_step(f"API key ditemukan di element {selector}")
559
+ break
560
+ except Exception:
561
+ continue
562
+
563
+ # Method 3: Look for a copy button and read the nearby text
564
+ if not api_key:
565
+ for selector in [
566
+ "button:has-text('Copy')",
567
+ "button:has-text('copy')",
568
+ "[data-copy]",
569
+ "button[aria-label*='copy' i]",
570
+ ]:
571
+ try:
572
+ copy_btn = page.locator(selector).first
573
+ if copy_btn.count() > 0 and copy_btn.is_visible(timeout=2000):
574
+ # Read sibling or parent text
575
+ parent = copy_btn.locator("xpath=..")
576
+ parent_text = parent.text_content() or ""
577
+ ov_match = re.search(r'(ov_sk_live_[A-Za-z0-9_\-]{20,})', parent_text)
578
+ if ov_match:
579
+ api_key = ov_match.group(1)
580
+ break
581
+ # Try grandparent
582
+ gparent = parent.locator("xpath=..")
583
+ gparent_text = gparent.text_content() or ""
584
+ ov_match = re.search(r'(ov_sk_live_[A-Za-z0-9_\-]{20,})', gparent_text)
585
+ if ov_match:
586
+ api_key = ov_match.group(1)
587
+ break
588
+ except Exception:
589
+ continue
590
+
591
+ # Method 4: Try clipboard (click copy button and read clipboard)
592
+ if not api_key:
593
+ try:
594
+ copy_btn = page.locator("button:has-text('Copy')").first
595
+ if copy_btn.count() > 0:
596
+ copy_btn.click()
597
+ time.sleep(1)
598
+ # Try to read from clipboard via JS
599
+ clipboard = page.evaluate("() => navigator.clipboard.readText().catch(() => '')")
600
+ if clipboard and re.match(r'ov_sk_live_', clipboard):
601
+ api_key = clipboard
602
+ except Exception:
603
+ pass
604
+
605
+ # Method 5: Scan all text nodes for ov_sk_ pattern
606
+ if not api_key:
607
+ try:
608
+ all_text = page.evaluate("""
609
+ () => {
610
+ const walker = document.createTreeWalker(
611
+ document.body, NodeFilter.SHOW_TEXT, null, false
612
+ );
613
+ const texts = [];
614
+ let node;
615
+ while (node = walker.nextNode()) {
616
+ const t = node.textContent.trim();
617
+ if (t.includes('ov_sk_')) texts.push(t);
618
+ }
619
+ return texts;
620
+ }
621
+ """)
622
+ for text in (all_text or []):
623
+ ov_match = re.search(r'(ov_sk_live_[A-Za-z0-9_\-]{20,})', text)
624
+ if ov_match:
625
+ api_key = ov_match.group(1)
626
+ break
627
+ except Exception:
628
+ pass
629
+
630
+ if not api_key:
631
+ try:
632
+ page.screenshot(path="/tmp/openvecta_no_key.png")
633
+ log_step("Screenshot: /tmp/openvecta_no_key.png")
634
+ except Exception:
635
+ pass
636
+ die("Tidak dapat menemukan API key ov_sk_live_... di halaman. Mungkin akun belum di-fund dengan USDC.")
637
+
638
+ # ── Step 10: Return success ───────────────────────────────────────
639
+ log_step(f"OpenVecta signup berhasil! API key: {api_key[:20]}...")
640
+ success(api_key, email)
641
+
642
+ except Exception as e:
643
+ try:
644
+ page.screenshot(path="/tmp/openvecta_error.png")
645
+ log_step(f"Screenshot error: /tmp/openvecta_error.png")
646
+ except Exception:
647
+ pass
648
+ die(f"Error selama otomatisasi OpenVecta: {str(e)}")
649
+
650
+
651
+ if __name__ == "__main__":
652
+ main()