feat(cred): harden client onboarding with Instagram linking portal, age verification bypass, and fleet runbook

This commit is contained in:
operator
2026-10-04 21:43:32 +00:00
parent 7902622852
commit 178ddc5dbf
10 changed files with 898 additions and 41 deletions
+5 -1
View File
@@ -68,8 +68,12 @@ try:
ws.close()
dom = json.loads(resp["result"]["result"]["value"])
# Heuristic: login buttons present + no avatar => logged out.
# access/verification gate => needs verification.
# No login buttons (or avatar present) => likely logged in.
if dom["loginButtons"] and not dom["hasAvatar"]:
if "access/verification" in dom["url"]:
result["session_alive"] = False
result["detail"] = "age verification gate (access/verification)"
elif dom["loginButtons"] and not dom["hasAvatar"]:
result["session_alive"] = False
result["detail"] = f"login wall visible: {dom['loginButtons'][:2]}"
else:
+378
View File
@@ -0,0 +1,378 @@
#!/usr/bin/env python3
"""cred-client.py — Agent API client & CLI for cred.muse-dev.online.
Provides programmatic and CLI access for agents and operators to:
- initiate: start onboarding for a client email/node
- submit-otp: submit verification code transiently
- status: query onboarding & vitality state for a node
- list: list fleet accounts, emails, and statuses
Authentication:
- Machine identity signature via ~/.ssh/muse-health (machine bl)
- Or Bearer token from CRED_TOKEN / OPERATOR_TOKEN environment variable.
Usage:
cred-client.py initiate --node <node> --email <email> [--service muse] [--account-name <name>]
cred-client.py submit-otp --node <node> --otp <code> [--email <email>]
cred-client.py status --node <node> [--json]
cred-client.py list [--json]
"""
import argparse
import datetime
import importlib.util
import json
import os
import re
import subprocess
import sys
import tempfile
import urllib.error
import urllib.request
NETVM_DIR = os.environ.get("NETVM_DIR", "/home/super/Projects/NetVM")
KEY_DEFAULT = os.path.expanduser("~/.ssh/muse-health")
CRED_API_DEFAULT = os.environ.get("CRED_API_URL", "https://cred.muse-dev.online/api/cred")
def load_registry():
path = os.path.join(NETVM_DIR, "bin", "netvm-registry.py")
try:
spec = importlib.util.spec_from_file_location("netvm_registry", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
except Exception:
return None
def get_accounts():
"""Parse ACCOUNTS.md into a structured list of accounts."""
path = os.path.join(NETVM_DIR, "ACCOUNTS.md")
accounts = []
if not os.path.exists(path):
return accounts
with open(path) as f:
for line in f:
line = line.strip()
if not line.startswith("|") or line.startswith("| agent") or line.startswith("|-------"):
continue
cols = [c.strip() for c in line.strip("|").split("|")]
if len(cols) >= 9:
accounts.append({
"agent": cols[0],
"node": cols[1],
"profile": cols[2],
"login_type": cols[3],
"meta_label": cols[4],
"email": cols[5],
"phone_otp": cols[6],
"instagram_linked": cols[7],
"status": cols[8],
"egress_ip": cols[9] if len(cols) > 9 else "",
"cdp_port": cols[10] if len(cols) > 10 else "",
"display_name": cols[11] if len(cols) > 11 else "",
"notes": cols[12] if len(cols) > 12 else ""
})
return accounts
def sign_payload(payload_bytes, key_path=KEY_DEFAULT, namespace="cred"):
"""Sign payload using SSH ed25519 key (zero secret across wire)."""
if not os.path.exists(key_path):
return None
tmp = tempfile.mkdtemp()
try:
data_file = os.path.join(tmp, "data")
with open(data_file, "wb") as f:
f.write(payload_bytes)
r = subprocess.run(
["ssh-keygen", "-Y", "sign", "-f", key_path, "-n", namespace, data_file],
capture_output=True, text=True
)
if r.returncode != 0:
return None
with open(data_file + ".sig") as f:
return f.read().strip()
finally:
subprocess.run(["rm", "-rf", tmp], capture_output=True)
class CredClient:
def __init__(self, api_url=CRED_API_DEFAULT, key_path=KEY_DEFAULT):
self.api_url = api_url.rstrip("/")
self.key_path = key_path
self.token = os.environ.get("CRED_TOKEN") or os.environ.get("OPERATOR_TOKEN")
def run_local_driver(self, node, step, email, otp=None, account_name=None):
"""Execute local onboard-driver.py inside the node's netns."""
exec_script = os.path.join(NETVM_DIR, "bin", "netvm-exec.sh")
driver_script = os.path.join(NETVM_DIR, "bin", "onboard-driver.py")
cmd = [exec_script, node, "--", sys.executable, driver_script,
"--node", node, "--service", "muse", "--id-type", "email", "--step", step]
if account_name:
cmd += ["--account-name", account_name]
input_data = email + "\n"
if otp:
input_data += str(otp) + "\n"
p = subprocess.run(cmd, input=input_data, capture_output=True, text=True, timeout=240)
return p.returncode, p.stdout.strip(), p.stderr.strip()
def initiate(self, node, email, service="muse", account_name=None):
"""Initiate client onboarding."""
ret, stdout, stderr = self.run_local_driver(node, "initiate", email, account_name=account_name)
if ret == 0:
return {"status": "active", "node": node, "email": email, "message": "Already authenticated and session active."}
elif ret == 2:
return {"status": "awaiting_otp", "node": node, "email": email, "message": "OTP verification code sent. Awaiting input."}
elif ret == 3:
return {"status": "needs_human", "node": node, "email": email, "message": "Multiple accounts match identifier. Manual selection required."}
else:
return {"status": "error", "node": node, "email": email, "code": ret, "detail": stderr or stdout}
def submit_otp(self, node, otp, email=None, service="muse"):
"""Submit transient verification code."""
if not email:
# Look up email from ACCOUNTS.md
for acct in get_accounts():
if acct["node"] == node and acct["email"] not in ("-", ""):
email = acct["email"]
break
if not email:
email = "unknown"
ret, stdout, stderr = self.run_local_driver(node, "submit", email, otp=otp)
if ret == 0:
return {"status": "active", "node": node, "email": email, "message": "Authentication successful. Chat session active."}
elif ret == 3:
return {"status": "needs_human", "node": node, "email": email, "message": "Multiple accounts match identifier."}
else:
return {"status": "error", "node": node, "email": email, "code": ret, "detail": stderr or stdout}
def status(self, node):
"""Check status of a node."""
accounts = get_accounts()
target = next((a for a in accounts if a["node"] == node), None)
port = None
reg = load_registry()
if reg:
port = reg.port_for(node)
# Vitality check
alive = False
detail = ""
if port:
try:
checker = os.path.join(NETVM_DIR, "bin", "accounts-health.py")
cmd = ["sudo", "-n", "ip", "netns", "exec", f"warp-{node}", sys.executable, checker, str(port)]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if r.returncode == 0:
data = json.loads(r.stdout.strip().splitlines()[-1])
alive = data.get("session_alive", False)
detail = data.get("detail", "")
except Exception as e:
detail = str(e)
return {
"node": node,
"status": target["status"] if target else "unregistered",
"email": target["email"] if target else "-",
"cdp_port": port,
"session_alive": alive,
"detail": detail
}
def list_all(self):
"""List all accounts and live statuses."""
res = []
for a in get_accounts():
st = self.status(a["node"])
res.append(st)
return res
def notify_operator(self, subject, body, to_email="defnotabotnet@gmail.com"):
"""Send notification to operator via local MTA (msmtp or mail)."""
sent = False
err = None
# Try msmtp first
try:
p = subprocess.Popen(["msmtp", to_email], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
msg = f"Subject: {subject}\nTo: {to_email}\nFrom: NetVM Automation <root@bl>\n\n{body}\n"
stdout, stderr = p.communicate(input=msg)
if p.returncode == 0:
sent = True
else:
err = stderr.strip() or stdout.strip()
except Exception as e:
err = str(e)
if not sent:
# Fallback to mail / s-nail
try:
p = subprocess.Popen(["mail", "-s", subject, to_email], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
stdout, stderr = p.communicate(input=body)
if p.returncode == 0:
sent = True
else:
err = stderr.strip() or stdout.strip()
except Exception as e:
err = str(e)
return {"sent": sent, "recipient": to_email, "error": err if not sent else None}
def link_instagram(self, node, notify=False):
"""Fetch the Meta Accounts Center OAuth URL and provide one-tap Tailscale link."""
portal_script = os.path.join(NETVM_DIR, "bin", "tailscale-verify-portal.py")
ts_ip = "100.123.153.75"
ts_dns = "bl.tailfb5960.ts.net"
try:
import importlib.util
spec = importlib.util.spec_from_file_location("portal", portal_script)
portal_mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(portal_mod)
ts_ip = portal_mod.get_tailscale_ip()
ts_dns = portal_mod.get_tailscale_dns()
ig_data = portal_mod.fetch_node_ig_link(node)
except Exception as e:
ig_data = {"error": str(e)}
direct_url = ig_data.get("url") if isinstance(ig_data, dict) else None
portal_url = f"http://{ts_dns}:8765/verify/{node}"
portal_ip_url = f"http://{ts_ip}:8765/verify/{node}"
email_result = None
if notify and direct_url:
subject = f"[NetVM Action Required] Link Instagram for Node '{node}'"
body = (
f"Node '{node}' is waiting at the Muse age verification gate.\n\n"
f"Tap the Tailscale portal link from your device to approve:\n"
f" {portal_url}\n"
f" (or {portal_ip_url})\n\n"
f"Direct OAuth URL:\n"
f" {direct_url}\n\n"
f"After approving in Instagram, NetVM will automatically transition node '{node}' to active."
)
email_result = self.notify_operator(subject, body)
return {
"node": node,
"status": "needs_verification",
"portal_url": portal_url,
"portal_ip_url": portal_ip_url,
"direct_oauth_url": direct_url,
"error": ig_data.get("error") if isinstance(ig_data, dict) else None,
"email_notified": email_result.get("sent") if email_result else False
}
def main():
common = argparse.ArgumentParser(add_help=False)
common.add_argument("--json", action="store_true", help="Output JSON response")
p = argparse.ArgumentParser(description="cred-client: Agent API client for cred onboarding", parents=[common])
sub = p.add_subparsers(dest="command", required=True)
# initiate
p_init = sub.add_parser("initiate", parents=[common], help="Initiate onboarding flow for a node")
p_init.add_argument("--node", required=True, help="Node label (e.g. opm, pip, client1)")
p_init.add_argument("--email", required=True, help="Client login email")
p_init.add_argument("--service", default="muse", help="Service name (default: muse)")
p_init.add_argument("--account-name", default=None, help="Display name hint for multi-account selector")
# submit-otp
p_otp = sub.add_parser("submit-otp", parents=[common], help="Submit transient OTP verification code")
p_otp.add_argument("--node", required=True, help="Node label")
p_otp.add_argument("--otp", required=True, help="6-digit verification code")
p_otp.add_argument("--email", default=None, help="Client email (optional, auto-detected from registry)")
# status
p_stat = sub.add_parser("status", parents=[common], help="Query onboarding and session status for a node")
p_stat.add_argument("--node", required=True, help="Node label")
# link-instagram
p_link = sub.add_parser("link-instagram", parents=[common], help="Generate Tailscale one-tap portal & OAuth link for age verification")
p_link.add_argument("--node", required=True, help="Node label")
p_link.add_argument("--notify", action="store_true", help="Send email alert to operator via local MTA")
# list
sub.add_parser("list", parents=[common], help="List all registered nodes and vitality statuses")
args = p.parse_args()
client = CredClient()
if args.command == "link-instagram":
res = client.link_instagram(args.node, notify=args.notify)
if args.json:
print(json.dumps(res, indent=2))
else:
print(f"\n=== INSTAGRAM LINKING: {args.node} ===")
if res.get("error"):
print(f"Error: {res['error']}", file=sys.stderr)
sys.exit(1)
print(f"Tailscale Portal: {res['portal_url']}")
print(f"Direct IP Portal: {res['portal_ip_url']}")
print(f"OAuth URL: {res['direct_oauth_url'][:80]}...")
if args.notify:
print(f"Email Notified: {'YES' if res['email_notified'] else 'FAILED'}")
print("\nTap either Tailscale link from your phone/browser to complete Meta age verification.")
sys.exit(0)
if args.command == "initiate":
res = client.initiate(args.node, args.email, service=args.service, account_name=args.account_name)
if args.json:
print(json.dumps(res, indent=2))
else:
st = res.get("status")
if st == "awaiting_otp":
print(f"[APPROVAL_NEEDED] Code sent to [redacted] for node '{args.node}'.")
print(f"Submit OTP with: super cred submit-otp --node {args.node} --otp <code>")
sys.exit(2)
elif st == "active":
print(f"[SUCCESS] Node '{args.node}' is already authenticated and active.")
sys.exit(0)
else:
print(f"[{st.upper()}] {res.get('message') or res.get('detail')}")
sys.exit(res.get("code", 1))
elif args.command == "submit-otp":
res = client.submit_otp(args.node, args.otp, email=args.email)
if args.json:
print(json.dumps(res, indent=2))
else:
st = res.get("status")
if st == "active":
print(f"[SUCCESS] Node '{args.node}' successfully signed in!")
sys.exit(0)
else:
print(f"[{st.upper()}] {res.get('message') or res.get('detail')}")
sys.exit(res.get("code", 1))
elif args.command == "status":
res = client.status(args.node)
if args.json:
print(json.dumps(res, indent=2))
else:
print(f"Node: {res['node']}")
print(f"Email: {res['email']}")
print(f"Status: {res['status']}")
print(f"CDP Port: {res['cdp_port'] or '-'}")
print(f"Session Alive: {'YES' if res['session_alive'] else 'NO'}")
if res["detail"]:
print(f"Detail: {res['detail']}")
elif args.command == "list":
items = client.list_all()
if args.json:
print(json.dumps(items, indent=2))
else:
print(f"{'NODE':<10} {'STATUS':<14} {'CDP':<6} {'ALIVE':<7} {'EMAIL'}")
print("-" * 65)
for it in items:
alive_str = "YES" if it["session_alive"] else "NO"
print(f"{it['node']:<10} {it['status']:<14} {str(it['cdp_port'] or '-'):<6} {alive_str:<7} {it['email']}")
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
cred-client.py
+77 -37
View File
@@ -95,50 +95,71 @@ def main():
page = get_page(port)
ws = websocket.create_connection(page['webSocketDebuggerUrl'], timeout=15)
# Step 1: Check if already logged in
# Step 1: Check if already logged in or blocked at verification gate
title = ev(ws, "document.title")
body = ev(ws, "document.body.innerText.slice(0,200)")
if "Connected" in body or "Chats" in body:
body = ev(ws, "document.body.innerText.slice(0,500)")
url = ev(ws, "location.href") or ""
if "access/verification" in url or "confirm your age" in body.lower():
ig_link = ev(ws, """(async()=>{
try {
const r = await fetch('/api/hatch/age-confirmation/linking-web-auth?account_type=instagram', {
headers: {'Accept': 'application/json'}
});
const j = await r.json();
return j && j.url ? j.url : null;
} catch(e) { return null; }
})()""", True)
if ig_link:
print(f"LINK_INSTAGRAM_URL: {ig_link}")
print(f"NEEDS_HUMAN: Age verification required for {args.email} at {url}. Client intervention required to confirm age or link Instagram/Facebook.", file=sys.stderr)
ws.close()
sys.exit(3)
if ("Connected" in body or "Chats" in body) and "Enter your code" not in body and "access/verification" not in url:
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...")
already_on_otp = ("Enter your code" in body or "code we sent" in body) and args.email in body
if not already_on_otp:
# 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('Log in'));
const b=[...document.querySelectorAll('button')].find(x=>x.innerText.includes('Continue'));
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)
time.sleep(4)
else:
print("Page is already on OTP prompt for this email, skipping email entry.")
# Step 5: Check for OTP prompt
body = ev(ws, "document.body.innerText.slice(0,500)")
@@ -176,7 +197,26 @@ def main():
# 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:
url = ev(ws, "location.href") or ""
# Check for post-login gates requiring human/client intervention
if "access/verification" in url or "confirm your age" in body.lower():
ig_link = ev(ws, """(async()=>{
try {
const r = await fetch('/api/hatch/age-confirmation/linking-web-auth?account_type=instagram', {
headers: {'Accept': 'application/json'}
});
const j = await r.json();
return j && j.url ? j.url : null;
} catch(e) { return null; }
})()""", True)
if ig_link:
print(f"LINK_INSTAGRAM_URL: {ig_link}")
print(f"NEEDS_HUMAN: Age verification required for {args.email} at {url}. Client intervention required to confirm age or link Instagram/Facebook.", file=sys.stderr)
ws.close()
sys.exit(3)
if "Connected" in body or "Chats" in body or (("Muse" in title or "Chat" in title) and "access/verification" not in url):
print(f"SUCCESS: Logged in as [redacted] (title: {title})")
ws.close()
return 0
+29 -3
View File
@@ -1878,11 +1878,11 @@ def cmd_cred_initiate(args):
else:
st = res.get("status")
if st == "awaiting_otp":
print("\n" + c_warn(f"[APPROVAL_NEEDED] Code sent to {args.email} for node '{args.node}'.") + "\n")
print("\n" + c_yellow(f"[APPROVAL_NEEDED] Code sent to {args.email} for node '{args.node}'.") + "\n")
print(f" Submit OTP with: super cred submit-otp --node {args.node} --otp <code>\n")
sys.exit(2)
elif st == "active":
print("\n" + c_ok(f"[SUCCESS] Node '{args.node}' is already authenticated and active.") + "\n")
print("\n" + c_green(f"[SUCCESS] Node '{args.node}' is already authenticated and active.") + "\n")
else:
print(f"[{st.upper()}] {res.get('message') or res.get('detail')}")
sys.exit(res.get("code", 1))
@@ -1896,7 +1896,7 @@ def cmd_cred_submit_otp(args):
else:
st = res.get("status")
if st == "active":
print("\n" + c_ok(f"[SUCCESS] Node '{args.node}' successfully signed in!") + "\n")
print("\n" + c_green(f"[SUCCESS] Node '{args.node}' successfully signed in!") + "\n")
else:
print(f"[{st.upper()}] {res.get('message') or res.get('detail')}")
sys.exit(res.get("code", 1))
@@ -1919,6 +1919,26 @@ def cmd_cred_status(args):
print(f" Detail : {c_dim(res['detail'])}")
print()
def cmd_cred_link_instagram(args):
import cred_client
client = cred_client.CredClient()
res = client.link_instagram(args.node, notify=getattr(args, "notify", False))
if getattr(args, "json", False):
print(json.dumps(res, indent=2))
else:
print("\n" + c_bold(f"=== INSTAGRAM LINKING: {args.node} ===") + "\n")
if res.get("error"):
print(c_err(f" Error: {res['error']}"))
sys.exit(1)
print(f" Tailscale Portal: {c_cyan(res['portal_url'])}")
print(f" Direct IP Portal: {c_cyan(res['portal_ip_url'])}")
oauth_preview = res['direct_oauth_url'][:75] + "..." if res.get('direct_oauth_url') else "-"
print(f" OAuth URL : {c_dim(oauth_preview)}")
if getattr(args, "notify", False):
n_badge = badge_ok("SENT") if res['email_notified'] else badge_err("FAILED")
print(f" Email Alert : {n_badge}")
print("\n" + c_yellow(" → Tap either Tailscale link from your phone/browser to complete Meta age verification.") + "\n")
def cmd_cred_list(args):
import cred_client
client = cred_client.CredClient()
@@ -3082,6 +3102,10 @@ def build_parser():
p_c_stat = cred_sub.add_parser("status", parents=[common], help="Query onboarding & vitality status for a node")
p_c_stat.add_argument("--node", required=True, help="Node label")
p_c_link = cred_sub.add_parser("link-instagram", parents=[common], help="Generate Tailscale one-tap portal & OAuth link for age verification")
p_c_link.add_argument("--node", required=True, help="Node label")
p_c_link.add_argument("--notify", action="store_true", help="Send email alert to operator via local MTA")
cred_sub.add_parser("list", parents=[common], help="List all registered nodes and vitality statuses")
# Domain: HARVEST
@@ -3356,6 +3380,8 @@ def main():
cmd_cred_submit_otp(args)
elif act == "status":
cmd_cred_status(args)
elif act == "link-instagram":
cmd_cred_link_instagram(args)
else:
parser.print_help()
elif args.domain == "harvest":
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""
tailscale-verify-portal.py: Tailscale verification portal for Human-in-the-Loop age verification.
Serves on Tailscale IP (100.123.153.75:8765) and/or localhost.
Endpoints:
GET /status - JSON status of nodes awaiting verification
GET /verify/<node> - Generates fresh Instagram OAuth linking URL from the node's browser and 302 redirects
GET /check/<node> - Polls node to see if age gate has cleared into active chat session
"""
import http.server
import json
import os
import re
import socketserver
import subprocess
import sys
import time
import urllib.parse
PORT = 8765
BIND_HOST = "0.0.0.0"
NETVM_DIR = "/home/super/Projects/NetVM"
def get_tailscale_ip():
try:
out = subprocess.check_output(["tailscale", "ip", "-4"], text=True).strip().splitlines()
for line in out:
line = line.strip()
if re.match(r"^\d+\.\d+\.\d+\.\d+$", line):
return line
except Exception:
pass
return "100.123.153.75"
def get_tailscale_dns():
try:
out = subprocess.check_output(["tailscale", "status", "--json"], text=True)
data = json.loads(out)
self_dns = data.get("Self", {}).get("DNSName")
if self_dns:
return self_dns.rstrip(".")
except Exception:
pass
return "bl.tailfb5960.ts.net"
def fetch_node_ig_link(node):
"""Query CDP within node netns to get fresh Meta Accounts Center OAuth URL."""
script = """
import json, websocket, sys, urllib.request
try:
tabs = json.loads(urllib.request.urlopen("http://127.0.0.1:9450/json").read())
except Exception as e:
print(json.dumps({"error": f"CDP unreachable: {e}"}))
sys.exit(0)
muse_tab = next((t for t in tabs if "muse.ai" in t.get("url", "") and t.get("type") == "page"), None)
if not muse_tab:
print(json.dumps({"error": "No muse.ai tab open"}))
sys.exit(0)
try:
ws = websocket.create_connection(muse_tab["webSocketDebuggerUrl"], timeout=5)
expr = '''(async()=>{
try {
const r = await fetch('/api/hatch/age-confirmation/linking-web-auth?account_type=instagram', {
headers: {'Accept': 'application/json'}
});
return await r.json();
} catch(e) { return {error: String(e)}; }
})()'''
ws.send(json.dumps({"id": 1, "method": "Runtime.evaluate", "params": {"expression": expr, "awaitPromise": True, "returnByValue": True}}))
while True:
msg = json.loads(ws.recv())
if msg.get("id") == 1:
val = msg.get("result", {}).get("result", {}).get("value", {})
print(json.dumps(val))
break
ws.close()
except Exception as e:
print(json.dumps({"error": f"CDP evaluate failed: {e}"}))
"""
cmd = ["sudo", "-n", "ip", "netns", "exec", f"warp-{node}", sys.executable, "-c", script]
try:
res = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
lines = res.stdout.strip().splitlines()
if lines:
data = json.loads(lines[-1])
return data
except Exception as e:
return {"error": str(e)}
return {"error": "No response from node"}
def check_node_cleared(node):
"""Check if node has transitioned past access/verification into active chat."""
checker = os.path.join(NETVM_DIR, "bin", "accounts-health.py")
# Lookup port from registry or default to 9450 for def
port = 9450
cmd = ["sudo", "-n", "ip", "netns", "exec", f"warp-{node}", sys.executable, checker, str(port)]
try:
res = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
lines = res.stdout.strip().splitlines()
if lines:
data = json.loads(lines[-1])
return data
except Exception as e:
return {"error": str(e)}
return {"error": "No response from checker"}
class VerifyHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
parts = [p for p in parsed.path.split("/") if p]
if not parts or parts[0] == "":
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
html = """<!DOCTYPE html>
<html>
<head><title>NetVM Operator Portal</title>
<style>body{font-family:system-ui,sans-serif;padding:2rem;background:#111;color:#eee;}a{color:#4ea8de;font-size:1.2rem;text-decoration:none;display:inline-block;margin:1rem 0;padding:0.75rem 1.5rem;background:#222;border-radius:8px;}a:hover{background:#333;}</style>
</head>
<body>
<h1>NetVM Client Verification Portal</h1>
<p>Nodes pending verification:</p>
<p><a href="/verify/def">Tap to Link Instagram for Node 'def'</a></p>
</body></html>"""
self.wfile.write(html.encode("utf-8"))
return
if parts[0] == "verify" and len(parts) >= 2:
node = parts[1]
data = fetch_node_ig_link(node)
url = data.get("url")
if url:
# 302 redirect directly to Instagram OAuth login
self.send_response(302)
self.send_header("Location", url)
self.end_headers()
return
else:
self.send_response(500)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"error": "Failed to get Instagram link", "detail": data}).encode("utf-8"))
return
if parts[0] == "check" and len(parts) >= 2:
node = parts[1]
st = check_node_cleared(node)
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(st, indent=2).encode("utf-8"))
return
if parts[0] == "status":
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"portal": "online", "ts_ip": get_tailscale_ip(), "ts_dns": get_tailscale_dns()}).encode("utf-8"))
return
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
# Concise logging
sys.stderr.write(f"[PORTAL] {self.address_string()} - {format % args}\n")
def run():
with socketserver.TCPServer((BIND_HOST, PORT), VerifyHandler) as httpd:
print(f"Tailscale Verification Portal serving on http://{get_tailscale_ip()}:{PORT}/")
print(f"Tailscale DNS: http://{get_tailscale_dns()}:{PORT}/")
sys.stdout.flush()
httpd.serve_forever()
if __name__ == "__main__":
run()