#!/usr/bin/env python3 """ Automated muse.ai sign-in with in-browser OTP approval. Usage: signin.py --email [--otp ] If --otp is not provided, the script will: 1. Navigate through login flow up to OTP prompt 2. Print "APPROVAL_NEEDED: OTP required for [redacted]" 3. Exit with code 2 The operator then obtains the OTP (via chat with user) and re-runs: signin.py --email --otp This keeps credentials out of the automation — the OTP is provided transiently via the operator, never stored. Exit codes: 0: Success / already logged in 2: APPROVAL_NEEDED (OTP prompt reached, awaiting code) 3: NEEDS_HUMAN (multi-account selection needs manual choice) 4: NEEDS_SIGNUP (account not found / registration required) 1: General error """ import json, urllib.request, websocket, time, sys, argparse, importlib.util CDP_PORT_DEFAULT = 9410 def _registry_port(node): try: path = "/home/super/Projects/NetVM/bin/netvm-registry.py" spec = importlib.util.spec_from_file_location("netvm_registry", path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod.port_for(node) except Exception: return None def is_registered_in_accounts(email): path = "/home/super/Projects/NetVM/ACCOUNTS.md" try: with open(path) as f: for line in f: if line.startswith("|") and email.lower() in line.lower(): return True except Exception: pass return False def get_page(port): cdp_url = "http://127.0.0.1:%s/json/list" % port with urllib.request.urlopen(cdp_url, timeout=5) as r: ts = json.load(r) pages = [t for t in ts if t.get('type') == 'page'] if not pages: print("ERROR: No page found", file=sys.stderr) sys.exit(1) return pages[0] def ev(ws, expr, await_p=False): ws.send(json.dumps({ "id": 1, "method": "Runtime.evaluate", "params": {"expression": expr, "returnByValue": True, "awaitPromise": await_p} })) resp = json.loads(ws.recv()) return resp.get('result', {}).get('result', {}).get('value') def main(): p = argparse.ArgumentParser() p.add_argument('--email', required=True) p.add_argument('--otp', default=None) p.add_argument('--node', default=None, help='node name: CDP port comes from the NODES.md registry') p.add_argument('--cdp-port', default=None, help='explicit CDP port (overrides --node lookup)') p.add_argument('--account-name', default=None, help='display-name hint for Meta multi-account selection ' '(never the "+1" entry)') args = p.parse_args() if is_registered_in_accounts(args.email): print("Registry check: [redacted] is registered in ACCOUNTS.md") if args.cdp_port: port = args.cdp_port elif args.node: port = _registry_port(args.node) or CDP_PORT_DEFAULT else: port = CDP_PORT_DEFAULT page = get_page(port) ws = websocket.create_connection(page['webSocketDebuggerUrl'], timeout=15) # Step 1: Check if already logged in title = ev(ws, "document.title") body = ev(ws, "document.body.innerText.slice(0,200)") if "Connected" in body or "Chats" in body: print(f"Already logged in (title: {title})") ws.close() return 0 # Step 2: Click Log in (if on landing page) if "Log in" in body: print("Clicking Log in...") ev(ws, """(async()=>{ const b=[...document.querySelectorAll('button')].find(x=>x.innerText.includes('Log in')); if(b) b.click(); return !!b; })()""", True) time.sleep(3) # Step 3: Enter email print("Entering email: [redacted]") result = ev(ws, f"""(async()=>{{ const inp=[...document.querySelectorAll('input')].find(i=> (i.placeholder&&i.placeholder.toLowerCase().includes('email'))|| (i.getAttribute('aria-label')&&i.getAttribute('aria-label').toLowerCase().includes('email')) ); if(!inp) return 'NOINPUT'; inp.focus(); document.execCommand('insertText',false,'{args.email}'); await new Promise(r=>setTimeout(r,500)); return 'entered:'+inp.value; }})()""", True) print("Email: [redacted]") if result == 'NOINPUT': print("ERROR: Email input not found", file=sys.stderr) ws.close() sys.exit(1) time.sleep(1) # Step 4: Click Continue print("Clicking Continue...") ev(ws, """(async()=>{ const b=[...document.querySelectorAll('button')].find(x=>x.innerText.includes('Continue')); if(b) b.click(); return !!b; })()""", True) time.sleep(4) # Step 5: Check for OTP prompt body = ev(ws, "document.body.innerText.slice(0,500)") if "Enter your code" in body or "code we sent" in body or "To log in" in body or "To confirm your account" in body: print("OTP prompt detected for [redacted]") if not args.otp: print("APPROVAL_NEEDED: OTP required for [redacted]") print("Re-run with: signin.py --email [redacted] --otp ") ws.close() sys.exit(2) # Approval needed # Enter OTP print("Entering OTP...") result = ev(ws, f"""(async()=>{{ const inp=[...document.querySelectorAll('input')].find(i=>i.type==='text'||i.type==='number'); if(!inp) return 'NOINPUT'; inp.focus(); document.execCommand('insertText',false,'{args.otp}'); await new Promise(r=>setTimeout(r,500)); return 'entered:'+inp.value; }})()""", True) print("OTP: [redacted]") time.sleep(1) # Click Next/Verify/Confirm print("Submitting OTP...") ev(ws, """(async()=>{ const b=[...document.querySelectorAll('button')].find(x=> x.innerText.includes('Next')||x.innerText.includes('Verify')||x.innerText.includes('Confirm') ); if(b) b.click(); return !!b; })()""", True) time.sleep(5) # Verify login body = ev(ws, "document.body.innerText.slice(0,500)") title = ev(ws, "document.title") or "" if "Connected" in body or "Chats" in body or "Muse" in title or "Chat" in title: print(f"SUCCESS: Logged in as [redacted] (title: {title})") ws.close() return 0 # Multi-account selection: Meta shows one button per matching # account plus a "+1" collapsed entry. The "+1" is NOT a real # account — click the button whose text contains the hinted # display name. Without a hint (or no match), a human must pick. if "Your email matches multiple accounts" in ev( ws, "document.body.innerText.slice(0,2000)"): if not args.account_name: print("NEEDS_HUMAN: multiple accounts match and no " "--account-name hint was given", file=sys.stderr) ws.close() sys.exit(3) picked = ev(ws, """(async()=>{ const want=%s.toLowerCase(); const btns=[...document.querySelectorAll('button')]; const t=btns.find(b=>b.innerText.toLowerCase().includes(want) && !b.innerText.includes('+1')); if(t){t.click();return true;} return false; })()""" % json.dumps(args.account_name), True) if not picked: print(f"NEEDS_HUMAN: no account button matched " f"'[redacted]'", file=sys.stderr) ws.close() sys.exit(3) print(f"Selected account '[redacted]', waiting...") time.sleep(5) body = ev(ws, "document.body.innerText.slice(0,500)") title = ev(ws, "document.title") or "" if "Connected" in body or "Chats" in body or "Muse" in title or "Chat" in title: print("SUCCESS: Logged in as [redacted] " "(account: [redacted])") ws.close() return 0 print("WARNING: account selection did not land in chat", file=sys.stderr) ws.close() sys.exit(1) else: print(f"WARNING: Login may have failed. Title: {title}", file=sys.stderr) print(f"Body: {body[:150]}", file=sys.stderr) ws.close() sys.exit(1) else: err_msg = ev(ws, """(()=>{ const el = document.querySelector('[role="alert"], .error, [data-error]'); return el ? el.innerText.trim() : ''; })()""") if err_msg: print(f"ERROR: {err_msg}", file=sys.stderr) else: print("No OTP prompt found - check page state", file=sys.stderr) print(f"Body: {body[:200]}", file=sys.stderr) ws.close() sys.exit(1) if __name__ == '__main__': sys.exit(main())