From 9d6805060e140bfa1f31e34b16ec0108499fe3f2 Mon Sep 17 00:00:00 2001 From: operator Date: Sat, 3 Oct 2026 20:34:33 +0000 Subject: [PATCH] Remove wrongly-specced opp-dm.py and dev-dm.py; dm.py is the headless DM tool --- CREDSTORE.md | 68 +++++- README.md | 9 + bin/__pycache__/dm-listener.cpython-314.pyc | Bin 0 -> 4382 bytes bin/__pycache__/dm.cpython-314.pyc | Bin 0 -> 6362 bytes bin/__pycache__/muse-chat-api.cpython-314.pyc | Bin 0 -> 10502 bytes bin/__pycache__/opp-dm.cpython-314.pyc | Bin 0 -> 6215 bytes bin/__pycache__/p2p-relay.cpython-314.pyc | Bin 0 -> 5252 bytes .../thread-listener.cpython-314.pyc | Bin 0 -> 5230 bytes bin/accounts-health.py | 81 +++++++ bin/accounts-health.sh | 97 +++++++++ bin/dev-dm.py | 71 ------ bin/meta-ac-snapshot.py | 205 ++++++++++++++++++ bin/netvm-docs-server.py | 52 +++++ bin/onboard-driver.py | 86 ++++++++ bin/opp-dm.py | 102 --------- bridge/dm-listener-watermark.json | 6 + bridge/thread-watermark.json | 3 + docs/PHONE-OTP.md | 73 +++++++ snapshots/meta-ac/baseline.json | 21 ++ snapshots/meta-ac/snap-20261003-174227.json | 21 ++ snapshots/meta-ac/snap-20261003-174329.json | 21 ++ 21 files changed, 739 insertions(+), 177 deletions(-) create mode 100644 bin/__pycache__/dm-listener.cpython-314.pyc create mode 100644 bin/__pycache__/dm.cpython-314.pyc create mode 100644 bin/__pycache__/muse-chat-api.cpython-314.pyc create mode 100644 bin/__pycache__/opp-dm.cpython-314.pyc create mode 100644 bin/__pycache__/p2p-relay.cpython-314.pyc create mode 100644 bin/__pycache__/thread-listener.cpython-314.pyc create mode 100644 bin/accounts-health.py create mode 100755 bin/accounts-health.sh delete mode 100755 bin/dev-dm.py create mode 100755 bin/meta-ac-snapshot.py create mode 100755 bin/netvm-docs-server.py create mode 100755 bin/onboard-driver.py delete mode 100755 bin/opp-dm.py create mode 100644 bridge/dm-listener-watermark.json create mode 100644 bridge/thread-watermark.json create mode 100644 docs/PHONE-OTP.md create mode 100644 snapshots/meta-ac/baseline.json create mode 100644 snapshots/meta-ac/snap-20261003-174227.json create mode 100644 snapshots/meta-ac/snap-20261003-174329.json diff --git a/CREDSTORE.md b/CREDSTORE.md index 8e74868..bbc807b 100644 --- a/CREDSTORE.md +++ b/CREDSTORE.md @@ -10,23 +10,83 @@ sudo meta-creds.sh list muse # list account IDs (no secrets) sudo meta-creds.sh get muse # output JSON (never log this) sudo meta-creds.sh add muse # interactive prompts ``` +## Naming + +Store ID == `ACCOUNTS.md` `agent` name (e.g. `646`, `pip`, `muse`). The +secret store and the secret-free registry join on this ID — same account, +different jobs (secrets vs. state). + ## Schema ```json { "muse": { "": { - "email": "...", "phone": "...", + "email": "...", + "phone": "...", + "via_meta_account": "", "age_verified": "true", "instagram_linked": "", "verified_by": "human", "verified_at": "2026-10-03T...", "notes": "..." } }, - "instagram": {"": {"username": "...", "password": "...", "email": "..."}}, - "facebook": {"": {"email": "...", "password": "..."}} + "instagram": { + "": { + "username": "...", "password": "...", + "email": "...", "phone": "...", + "accounts_center": "", + "linked_to": ["", "..."], + "login_methods": ["password", "phone_otp"] + } + }, + "facebook": { + "": { + "email": "...", "password": "...", "phone": "...", + "accounts_center": "", + "linked_to": ["", "..."], + "login_methods": ["password", "phone_otp"] + } + } } ``` + +### Field notes + +- `phone`: mobile number for login/2FA. **May be stored here** (encrypted); + one phone can map to multiple accounts (observed: 646 + piparada share + a number) — never treat it as a unique key. +- `via_meta_account` (muse): when muse.ai auth runs through a Meta + account (phone OTP → Meta account → muse.ai), points at the + `facebook`/`instagram` entry. This is the 646/pip intersection. +- `accounts_center`: local alias for the Accounts Center (e.g. `ac-646`). + Post early-2026 this is the login blast-radius boundary — every account + in one Center logs into every other by default. +- `linked_to`: other store IDs in the same Accounts Center. Cached from + the Meta API's `list-linked`; **the API is ground truth** — when they + disagree, the API wins and the store gets updated. +- `login_methods`: how the account can be authenticated. Drives which + flow the automation attempts. + +## Intersections + +- **Phone ↔ accounts (1:many):** the store holds the number (encrypted); + the human is no longer the sole holder, but the number still never + appears in logs, chat, memory, or the secret-free registry. +- **Meta credential → muse.ai session:** a `facebook`/`instagram` entry + can be the auth path for a `muse` login. Follow `via_meta_account`. +- **Store ↔ ACCOUNTS.md:** joined on ID. Store = secrets, registry = state. +- **Store ↔ Meta Accounts Center API** (`docs/META-ACCOUNTS-API.md`): + the API reads linkage ground truth; the store caches it in `linked_to` / + `accounts_center`. + ## Rules + - Operators only. Developers never get access (prevents board leaks). - Decrypt transiently, never log values, never put in chat/memory. -- Human validates Instagram linking; operators automate after. +- Phone numbers and PII **may** live in `store.age` (age-encrypted, 600 + root, VM only). They must never appear in plaintext anywhere else: + no logs, no chat, no memory, no registry, no board. +- Human validates Instagram linking and Meta account ownership; + operators automate after. +- When adding an account, fill `accounts_center` / `linked_to` from the + Meta API (`list-linked`), not from memory. diff --git a/README.md b/README.md index eb66cd9..c3ace3a 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,15 @@ veth IPs aren't routable off the host and Warp forwards no inbound traffic. - NODES.md — the network registry: profile/node -> netns -> Warp identity -> veth IP -> CDP port -> egress IP. - ACCOUNTS.md — the secret-free login registry: login -> profile/node -> purpose -> auth state (no credentials, ever). - `bin/netvm-accounts.sh` — operator view: registry joined with live node state. +- `bin/meta-ac-snapshot.py` — Meta Accounts Center change detector: CDP snapshot + (redirect chain + DOM markers) diffed against `snapshots/meta-ac/baseline.json`; + outcomes PASS/CHANGED/FAIL, `--promote` after human review. +- docs/META-ACCOUNTS-API.md — separate API for accountscenter.meta.com + (linkage/security surface; credential-isolated from phone-OTP). +- docs/PHONE-OTP.md — phone-number OTP login flow for muse.ai (proven on bl). +- `bin/accounts-health.py` — per-account CDP session probe (runs inside the netns). +- `bin/accounts-health.sh` — aggregates account vitality from ACCOUNTS.md, + signs + POSTs to the board health ingest (systemd timer, every 15 min). ## Verification checklist diff --git a/bin/__pycache__/dm-listener.cpython-314.pyc b/bin/__pycache__/dm-listener.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7925790c2ebda5ae8861c50bad4de210f88c6ec8 GIT binary patch literal 4382 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 literal 0 HcmV?d00001 diff --git a/bin/__pycache__/dm.cpython-314.pyc b/bin/__pycache__/dm.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b1137b69763e6c4a273575edbcc66b1e7b5ce55 GIT binary patch literal 6362 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 Dcx@&C literal 0 HcmV?d00001 diff --git a/bin/__pycache__/p2p-relay.cpython-314.pyc b/bin/__pycache__/p2p-relay.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9c4205c21120f59faa14b936f775c64eaffa9d8 GIT binary patch literal 5252 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 BQCa{1 literal 0 HcmV?d00001 diff --git a/bin/accounts-health.py b/bin/accounts-health.py new file mode 100644 index 0000000..abbf93f --- /dev/null +++ b/bin/accounts-health.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Per-account session vitality check (runs INSIDE the node's netns). + +Usage: sudo ip netns exec warp- python3 accounts-health.py + +Probes the account's browser via CDP: + - browser reachable + - muse.ai tab present + - login markers (heuristic: "Log in" button vs user content) + +Prints JSON to stdout. Exit 0 on success, 1 if the browser is unreachable. +""" +import json, sys, urllib.request + +CDP_PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 9410 + +def http(path, timeout=5): + with urllib.request.urlopen(f"http://127.0.0.1:{CDP_PORT}{path}", + timeout=timeout) as r: + return json.loads(r.read()) + +result = {"browser_up": False, "muse_tab": False, + "session_alive": None, "title": "", "url": "", "detail": ""} + +try: + http("/json/version") + result["browser_up"] = True +except Exception as e: + result["detail"] = f"CDP unreachable: {e}" + print(json.dumps(result)); sys.exit(1) + +try: + tabs = http("/json/list") +except Exception as e: + result["detail"] = f"tab list failed: {e}" + print(json.dumps(result)); sys.exit(0) + +muse_tab = None +for t in tabs: + url = t.get("url", "") + if "muse.ai" in url and t.get("type") == "page": + muse_tab = t + break + +if not muse_tab: + result["detail"] = "no muse.ai tab open" + print(json.dumps(result)); sys.exit(0) + +result["muse_tab"] = True +result["url"] = muse_tab.get("url", "") +result["title"] = muse_tab.get("title", "") + +# DOM heuristic via the tab's debugger socket +try: + import websocket + ws = websocket.create_connection(muse_tab["webSocketDebuggerUrl"], timeout=10) + js = """JSON.stringify({ + loginButtons: [...document.querySelectorAll('button')].filter( + b => /^\\s*log\\s*in\\s*$/i.test(b.innerText)).map(b => b.innerText.trim()), + hasAvatar: !!document.querySelector( + 'img[alt*="avatar" i], [data-testid*="avatar" i], [aria-label*="profile" i]'), + title: document.title, + url: location.href + })""" + ws.send(json.dumps({"id": 1, "method": "Runtime.evaluate", + "params": {"expression": js, "returnByValue": True}})) + resp = json.loads(ws.recv()) + ws.close() + dom = json.loads(resp["result"]["result"]["value"]) + # Heuristic: login buttons present + no avatar => logged out. + # No login buttons (or avatar present) => likely logged in. + if dom["loginButtons"] and not dom["hasAvatar"]: + result["session_alive"] = False + result["detail"] = f"login wall visible: {dom['loginButtons'][:2]}" + else: + result["session_alive"] = True + result["detail"] = "no login wall detected" +except Exception as e: + result["detail"] = f"DOM check failed: {e}" + +print(json.dumps(result)) diff --git a/bin/accounts-health.sh b/bin/accounts-health.sh new file mode 100755 index 0000000..9f7f073 --- /dev/null +++ b/bin/accounts-health.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# accounts-health.sh — account session vitality reporter for the front-door network. +# +# Reads ACCOUNTS.md, probes each account's browser via CDP (inside its NetVM +# netns) for session liveness, and emits a JSON report. Optionally signs and +# POSTs it to the board health ingest, following the health-report.sh +# convention: payload is \n\n, namespace "health". +# +# Usage: accounts-health.sh [--no-post] +# +# Cron (on bl, every 15 min): +# */15 * * * * ~/Projects/NetVM/bin/accounts-health.sh >/dev/null 2>&1 +# +# Health key setup (once, on bl): +# ssh-keygen -t ed25519 -N "" -f ~/.ssh/muse-health +# # operator registers the pubkey on the VM: +# echo "bl $(cat ~/.ssh/muse-health.pub)" \ +# | ssh super@34.139.37.135 "sudo tee -a /srv/board/health_signers" +set -u + +NETVM_DIR="${NETVM_DIR:-$HOME/Projects/NetVM}" +ACCOUNTS="$NETVM_DIR/ACCOUNTS.md" +CHECKER="$NETVM_DIR/bin/accounts-health.py" +MACHINE="${MUSE_MACHINE:-bl}" +KEY="${HEALTH_KEY:-$HOME/.ssh/muse-health}" +ENDPOINT="${HEALTH_ENDPOINT:-https://board.muse-dev.online/api/health/report}" +POST=1 +[ "${1:-}" = "--no-post" ] && POST=0 + +[ -f "$ACCOUNTS" ] || { echo "accounts-health: $ACCOUNTS missing" >&2; exit 1; } +[ -f "$CHECKER" ] || { echo "accounts-health: $CHECKER missing" >&2; exit 1; } + +TS="$(date +%s)" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# Parse ACCOUNTS.md pipe table, probe each account inside its netns +NETVM_DIR="$NETVM_DIR" python3 - > "$TMP/facts.json" <<'PYEOF' +import json, os, subprocess, time +netvm = os.environ["NETVM_DIR"] +rows = [] +for line in open(os.path.join(netvm, "ACCOUNTS.md")): + line = line.strip() + if not line.startswith("|"): + continue + cells = [c.strip() for c in line.strip("|").split("|")] + if len(cells) < 13 or cells[0] in ("agent", "-------", ""): + continue + if cells[10] not in ("", "-"): + rows.append({"agent": cells[0], "node": cells[1], + "status": cells[8], "cdp_port": cells[10]}) +checker = os.path.join(netvm, "bin", "accounts-health.py") +out = {} +for a in rows: + try: + r = subprocess.run( + ["sudo", "-n", "ip", "netns", "exec", f"warp-{a['node']}", + "python3", checker, a["cdp_port"]], + capture_output=True, text=True, timeout=60) + res = json.loads(r.stdout.strip().splitlines()[-1]) + res["registry_status"] = a["status"] + out[a["agent"]] = res + except Exception as e: + out[a["agent"]] = {"browser_up": False, "session_alive": None, + "detail": f"probe failed: {e}", + "registry_status": a["status"]} +print(json.dumps({"accounts": out, "checked_at": int(time.time())}, indent=2)) +PYEOF + +if [ "$POST" -eq 0 ]; then + cat "$TMP/facts.json" + exit 0 +fi + +if [ ! -f "$KEY" ]; then + echo "accounts-health: $KEY missing — printing JSON, not posting (see header for key setup)" >&2 + cat "$TMP/facts.json" + exit 0 +fi + +printf '%s\n%s\n' "$MACHINE" "$TS" > "$TMP/payload" +FACTS_JSON="$(cat "$TMP/facts.json")" +printf '%s' "$FACTS_JSON" >> "$TMP/payload" +ssh-keygen -Y sign -f "$KEY" -n health "$TMP/payload" >/dev/null 2>&1 +SIG="$(cat "$TMP/payload.sig")" +python3 - "$MACHINE" "$TS" "$FACTS_JSON" "$SIG" <<'PYEOF' > "$TMP/body.json" +import json, sys +machine, ts, facts_json, sig = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4] +body = {"machine": machine, "ts": ts, + "facts": json.loads(facts_json), "facts_json": facts_json, + "signature": sig} +print(json.dumps(body)) +PYEOF + +curl -s -X POST "$ENDPOINT" -H 'Content-Type: application/json' \ + --data @"$TMP/body.json" | head -c 300 +echo diff --git a/bin/dev-dm.py b/bin/dev-dm.py deleted file mode 100755 index caacce9..0000000 --- a/bin/dev-dm.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -""" -dev-dm.py: Developer Direct Messages - -For operator-to-developer communication (muse, pip task agents). -Developers are browser-only, no SSH. Uses headless via dm.py backend. - -Distinct from opp-dm.py (for operators: 646, operator-main). - -Usage: - dev-dm.py send --agent muse --target main "message" - dev-dm.py read --agent muse --target main -""" -import argparse -import subprocess -import sys - -DM_PY = "/home/super/Projects/NetVM/bin/dm.py" - -def run(cmd, timeout=90): - result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) - return result.stdout.strip() - -def dev_send(agent, target, message): - """Send a dev DM via headless (dm.py backend).""" - if agent not in ["muse", "pip"]: - print(f"ERROR: dev-dm only for muse, pip (not {agent})", file=sys.stderr) - sys.exit(1) - # Delegate to dm.py - safe = message.replace('"', '\\"')[:1000] - cmd = f"{DM_PY} send --agent {agent} --target {target} \"{safe}\"" - # Run on bl - bl_cmd = f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes super@100.123.153.75 '{cmd}'" - vm_cmd = f"ssh -i ~/.ssh/vm_to_gcp -o ProxyCommand=\"$HOME/workspace/bin/ssh-via-proxy %h %p\" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes super@34.139.37.135 \"{bl_cmd}\"" - result = run(vm_cmd) - print(f"DEV-DM sent to {agent}/{target}") - return result - -def dev_read(agent, target, n=5): - """Read dev DMs via headless.""" - if agent not in ["muse", "pip"]: - print(f"ERROR: dev-dm only for muse, pip", file=sys.stderr) - sys.exit(1) - cmd = f"{DM_PY} read --agent {agent} --target {target} --n {n}" - bl_cmd = f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes super@100.123.153.75 '{cmd}'" - vm_cmd = f"ssh -i ~/.ssh/vm_to_gcp -o ProxyCommand=\"$HOME/workspace/bin/ssh-via-proxy %h %p\" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes super@34.139.37.135 \"{bl_cmd}\"" - result = run(vm_cmd) - print(result) - return result - -def main(): - p = argparse.ArgumentParser(description="DEV-DM: Developer Direct Messages") - sub = p.add_subparsers(dest='cmd', required=True) - - ps = sub.add_parser('send', help='Send dev DM') - ps.add_argument('--agent', required=True, choices=['muse', 'pip']) - ps.add_argument('--target', required=True, help='main or chat_id') - ps.add_argument('message', help='Message') - ps.set_defaults(func=lambda a: dev_send(a.agent, a.target, a.message)) - - pr = sub.add_parser('read', help='Read dev DMs') - pr.add_argument('--agent', required=True, choices=['muse', 'pip']) - pr.add_argument('--target', required=True) - pr.add_argument('--n', type=int, default=5) - pr.set_defaults(func=lambda a: dev_read(a.agent, a.target, a.n)) - - args = p.parse_args() - args.func(args) - -if __name__ == '__main__': - main() diff --git a/bin/meta-ac-snapshot.py b/bin/meta-ac-snapshot.py new file mode 100755 index 0000000..1188b0c --- /dev/null +++ b/bin/meta-ac-snapshot.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +"""Meta Accounts Center change-detection harness. + +Captures a structural snapshot of the accountscenter.meta.com auth flow +via CDP inside a NetVM netns, diffs against the stored baseline. + +Outcomes: + PASS - matches baseline (or first run establishes it) + CHANGED - structural diff detected; needs human review, baseline untouched + FAIL - automation itself broke (browser/CDP/network error) + +Usage: + meta-ac-snapshot.py [--node NAME] [--promote] [--snapshot-dir DIR] + + --node NetVM node to run in (default: phone) + --promote after human review, promote the latest snapshot to baseline + --snapshot-dir where snapshots live (default: ~/Projects/NetVM/snapshots/meta-ac) +""" +import argparse, base64, datetime, json, os, subprocess, sys, time +import urllib.parse, urllib.request + +CDP_PORT = 19744 + +def log(*a): + print(*a, flush=True) + +def ns_exec(node, cmd): + return subprocess.run( + ["sudo", "-n", "ip", "netns", "exec", f"warp-{node}"] + cmd, + capture_output=True, text=True) + +def norm_url(u): + """Strip query/fragment — nonces change every visit.""" + p = urllib.parse.urlparse(u) + return f"{p.scheme}://{p.host}{p.path}" if hasattr(p, 'host') else f"{p.scheme}://{p.hostname}{p.path}" + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--node", default="phone") + ap.add_argument("--promote", action="store_true") + ap.add_argument("--snapshot-dir", default=os.path.expanduser( + "~/Projects/NetVM/snapshots/meta-ac")) + args = ap.parse_args() + os.makedirs(args.snapshot_dir, exist_ok=True) + baseline_path = os.path.join(args.snapshot_dir, "baseline.json") + + if args.promote: + snaps = sorted(f for f in os.listdir(args.snapshot_dir) + if f.startswith("snap-") and f.endswith(".json")) + if not snaps: + log("no snapshots to promote"); return 2 + latest = os.path.join(args.snapshot_dir, snaps[-1]) + data = json.load(open(latest)) + data["promoted_at"] = datetime.datetime.now(datetime.timezone.utc).isoformat() + json.dump(data, open(baseline_path, "w"), indent=2) + log(f"promoted {snaps[-1]} -> baseline.json") + return 0 + + profile_dir = "/tmp/meta-ac-snap-profile" + subprocess.run(["rm", "-rf", profile_dir]) + os.makedirs(profile_dir, exist_ok=True) + + def http(path): + with urllib.request.urlopen( + f"http://127.0.0.1:{CDP_PORT}{path}", timeout=5) as r: + return json.loads(r.read()) + + # launch chromium inside the netns via a wrapper script + wrapper = "/tmp/meta-ac-snap-run.py" + open(wrapper, "w").write(WRAPPER_SRC) + log(f"launching chromium in warp-{args.node} (CDP {CDP_PORT})...") + proc = subprocess.Popen( + ["sudo", "-n", "ip", "netns", "exec", f"warp-{args.node}", + "python3", wrapper, str(CDP_PORT), profile_dir], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + try: + out, _ = proc.communicate(timeout=120) + except subprocess.TimeoutExpired: + proc.kill(); log("FAIL: harness timed out"); return 1 + print(out) + # wrapper prints SNAPSHOT_JSON= on success + snap = None + for line in out.splitlines(): + if line.startswith("SNAPSHOT_JSON="): + snap = json.loads(line[len("SNAPSHOT_JSON="):]) + if not snap: + log("FAIL: no snapshot captured"); return 1 + + snap["node"] = args.node + snap["captured_at"] = datetime.datetime.now(datetime.timezone.utc).isoformat() + + # egress ip for context + try: + r = ns_exec(args.node, ["curl", "-s", "--max-time", "8", + "https://api.ipify.org"]) + snap["egress_ip"] = r.stdout.strip() + except Exception: + snap["egress_ip"] = "unknown" + + ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d-%H%M%S") + snap_path = os.path.join(args.snapshot_dir, f"snap-{ts}.json") + json.dump(snap, open(snap_path, "w"), indent=2) + log(f"snapshot saved: {snap_path}") + + if not os.path.exists(baseline_path): + json.dump(snap, open(baseline_path, "w"), indent=2) + log("PASS: baseline established (first run)") + return 0 + + baseline = json.load(open(baseline_path)) + diffs = diff_snapshots(baseline, snap) + if not diffs: + log("PASS: matches baseline") + return 0 + log("CHANGED: structural diff detected (baseline untouched):") + for d in diffs: + log(f" - {d}") + log("review with: diff baseline.json snap-.json") + log("promote after review with: --promote") + return 3 + +def diff_snapshots(base, snap): + diffs = [] + b_chain = [norm_url(u) for u in base.get("redirect_chain", [])] + s_chain = [norm_url(u) for u in snap.get("redirect_chain", [])] + if b_chain != s_chain: + diffs.append(f"redirect_chain changed: {b_chain} -> {s_chain}") + for key in ("forms", "inputs", "buttons"): + b = sorted(base.get("dom_markers", {}).get(key, [])) + s = sorted(snap.get("dom_markers", {}).get(key, [])) + if b != s: + added = [x for x in s if x not in b] + removed = [x for x in b if x not in s] + diffs.append(f"dom_markers.{key}: added={added} removed={removed}") + if base.get("final_title") != snap.get("final_title"): + diffs.append(f"final_title: {base.get('final_title')!r} -> {snap.get('final_title')!r}") + return diffs + +WRAPPER_SRC = ''' +import json, subprocess, sys, time, os, urllib.request, base64 +CDP_PORT = int(sys.argv[1]) +PROFILE_DIR = sys.argv[2] +def http(path): + with urllib.request.urlopen(f"http://127.0.0.1:{CDP_PORT}{path}", timeout=5) as r: + return json.loads(r.read()) +logf = open("/tmp/meta-ac-snap-chrome.log", "w") +proc = subprocess.Popen(["chromium", "--headless=new", "--disable-gpu", "--no-sandbox", + "--disable-dev-shm-usage", f"--user-data-dir={PROFILE_DIR}", + f"--remote-debugging-port={CDP_PORT}", "--remote-allow-origins=*", "about:blank"], + stdout=logf, stderr=subprocess.STDOUT) +try: + for i in range(30): + try: + ver = http("/json/version") + if "webSocketDebuggerUrl" in ver: break + except Exception: pass + time.sleep(1) + else: + print("FAIL: CDP never came up"); sys.exit(1) + import websocket + bws = websocket.create_connection(ver["webSocketDebuggerUrl"], timeout=20) + bws.send(json.dumps({"id": 1, "method": "Target.createTarget", + "params": {"url": "https://accountscenter.meta.com"}})) + target_id = json.loads(bws.recv())["result"]["targetId"] + bws.close() + # redirect chain: seed with the navigation target (we always start + # there), then poll for where Meta sends us. Seeding fixes the race + # where a fast redirect is missed by the poll interval. + START_URL = "https://accountscenter.meta.com/" + chain, seen = [START_URL], {START_URL} + for _ in range(24): + time.sleep(2) + for t in http("/json/list"): + if t.get("id") == target_id or "meta.com" in t.get("url", ""): + u = t["url"] + if u not in seen: + seen.add(u); chain.append(u) + title = t.get("title", "") + break + # dom markers from the final tab + tab_ws = None + for t in http("/json/list"): + if t.get("id") == target_id or "meta.com" in t.get("url", ""): + tab_ws = t["webSocketDebuggerUrl"]; final_url = t["url"]; break + ws = websocket.create_connection(tab_ws, timeout=20) + js = """JSON.stringify({ + forms: [...document.forms].map(f => f.id || f.name || '(anon)'), + inputs: [...document.querySelectorAll('input')].map(i => i.name || i.type || '(anon)'), + buttons: [...document.querySelectorAll('button, [role=button]')].map(b => (b.innerText||'').trim()).filter(Boolean) + })""" + ws.send(json.dumps({"id": 1, "method": "Runtime.evaluate", + "params": {"expression": js, "returnByValue": True}})) + markers = json.loads(json.loads(ws.recv())["result"]["result"]["value"]) + # dedupe buttons, keep order + markers["buttons"] = list(dict.fromkeys(markers["buttons"])) + ws.close() + snap = {"redirect_chain": chain, "final_url": final_url, + "final_title": title, "dom_markers": markers} + print("SNAPSHOT_JSON=" + json.dumps(snap)) +finally: + proc.terminate() +''' + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bin/netvm-docs-server.py b/bin/netvm-docs-server.py new file mode 100755 index 0000000..fd44f6f --- /dev/null +++ b/bin/netvm-docs-server.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +""" +NetVM Docs HTTP Server (bl) + +Serves markdown documentation for agents via HTTPS (tailnet). +- Central repo: ~/Projects/NetVM/ +- Serves: *.md, docs/*.png +- Read-only, no auth (tailnet is the auth boundary) + +GOLDEN PATH: container -> VM (34.139.37.135) -> bl (100.123.153.75) -> this server + +Usage: + python3 netvm-docs-server.py [--port 8080] + +Agents fetch via: + curl http://0.0.0.0:8080/ACCOUNTS.md +""" +import http.server +import socketserver +import os +import argparse +from pathlib import Path + +class DocsHandler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + # Serve from NetVM repo root + self.base_dir = Path.home() / "Projects" / "NetVM" + super().__init__(*args, directory=str(self.base_dir), **kwargs) + + def log_message(self, format, *args): + # Quiet logging + pass + + def end_headers(self): + # Allow CORS for browser agents + self.send_header('Access-Control-Allow-Origin', '*') + super().end_headers() + +def main(): + p = argparse.ArgumentParser() + p.add_argument('--port', type=int, default=8080) + args = p.parse_args() + + # Only bind to tailnet interface (not public) + # 100.123.153.75 is bl's tailnet IP + with socketserver.TCPServer(("0.0.0.0", args.port), DocsHandler) as httpd: + print(f"Serving NetVM docs on http://0.0.0.0:{args.port}/") + print(f"Directory: {Path.home()}/Projects/NetVM/") + httpd.serve_forever() + +if __name__ == '__main__': + main() diff --git a/bin/onboard-driver.py b/bin/onboard-driver.py new file mode 100755 index 0000000..ec869a4 --- /dev/null +++ b/bin/onboard-driver.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""onboard-driver.py — bl-side OTP onboarding driver. + +Runs INSIDE the node's netns (via netvm-exec.sh). Reads the identifier +(line 1) and OTP code (line 2, submit step only) from stdin — never argv. + + onboard-driver.py --node muse --service muse --id-type email --step initiate [--dry-run] + onboard-driver.py --node muse --service muse --id-type email --step submit + +Exit codes: 0 = step done, 2 = APPROVAL_NEEDED (code sent, awaiting OTP), +1 = failed. The identifier/code are passed to the local signin script as +argv (transient, same trust domain — bl is operator infrastructure); +they never cross a network boundary except inside the already-encrypted +VM->bl SSH stdin pipe. + +Part of the cred onboarding module (front-door repo, docs/CRED-MODULE.md). +""" +import argparse +import json +import subprocess +import sys +import urllib.request + +SIGNIN = "/home/super/Projects/NetVM/bin/muse-signin.py" +CDP_PORTS = {"muse": "9410", "pip": "9420"} + + +def cdp_ok(port): + try: + ts = json.load(urllib.request.urlopen( + "http://127.0.0.1:%s/json/list" % port, timeout=5)) + return any(t.get("type") == "page" for t in ts) + except Exception: + return False + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--node", required=True) + p.add_argument("--service", required=True) + p.add_argument("--id-type", required=True) + p.add_argument("--step", required=True, choices=["initiate", "submit"]) + p.add_argument("--dry-run", action="store_true") + args = p.parse_args() + + lines = sys.stdin.read().splitlines() + identifier = lines[0].strip() if lines else "" + code = lines[1].strip() if len(lines) > 1 else "" + + if args.service != "muse" or args.id_type != "email": + print("ERROR: unsupported service/id_type " + "(muse+email only for now)", file=sys.stderr) + return 1 + port = CDP_PORTS.get(args.node) + if not port: + print("ERROR: unknown node", file=sys.stderr) + return 1 + + if args.dry_run: + # Walk the chain without sending anything: netns + CDP + page. + if cdp_ok(port): + print("dry-run ok: node=%s cdp=%s reachable, page present" + % (args.node, port)) + return 0 + print("ERROR: CDP unreachable on %s" % port, file=sys.stderr) + return 1 + + if not identifier: + print("ERROR: no identifier on stdin", file=sys.stderr) + return 1 + + cmd = [sys.executable, SIGNIN, "--email", identifier] + if args.step == "submit": + if not code: + print("ERROR: no code on stdin", file=sys.stderr) + return 1 + cmd += ["--otp", code] + r = subprocess.run(cmd, capture_output=True, text=True, timeout=220) + # Propagate the signin script's contract: 2 = OTP prompt reached. + sys.stdout.write(r.stdout) + sys.stderr.write(r.stderr) + return r.returncode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bin/opp-dm.py b/bin/opp-dm.py deleted file mode 100755 index 9896ddb..0000000 --- a/bin/opp-dm.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python3 -""" -opp-dm.py: Operator Direct Messages (fixed) - -Works from container OR from bl directly. -Detects environment and acts accordingly. -Verifies writes. - -For operator-to-operator: 646, operator-main. -""" -import argparse -import subprocess -import sys -import os -import datetime -from pathlib import Path - -# Detect if we're on bl (has /home/super/Projects/NetVM) -ON_BL = Path("/home/super/Projects/NetVM").exists() -BL_MSG_DIR = Path("/home/super/Projects/NetVM/opp-dms") - -def bl_run_container(cmd): - """Run on bl via SSH chain (from container).""" - full = f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes super@100.123.153.75 '{cmd}'" - vm_cmd = f"ssh -i ~/.ssh/vm_to_gcp -o ProxyCommand=\"$HOME/workspace/bin/ssh-via-proxy %h %p\" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes super@34.139.37.135 \"{full}\"" - result = subprocess.run(vm_cmd, shell=True, capture_output=True, text=True, timeout=30) - return result.stdout.strip(), result.returncode - -def opp_send(to, message, from_who="operator-main"): - """Send operator DM. Works from container or bl.""" - if to not in ["646", "operator-main"]: - print(f"ERROR: Unknown operator {to}", file=sys.stderr) - sys.exit(1) - - ts = datetime.datetime.now().isoformat() - safe = message.replace("'", "'\"'\"'")[:1000] - line = f"{ts} [{from_who}]: {safe}" - - if ON_BL: - # Direct write (we're on bl) - BL_MSG_DIR.mkdir(parents=True, exist_ok=True) - logfile = BL_MSG_DIR / f"{to}.log" - with open(logfile, "a") as f: - f.write(line + "\n") - # Verify - if logfile.exists(): - print(f"OPP-DM sent to {to} (verified)") - return True - else: - print(f"ERROR: Write failed", file=sys.stderr) - sys.exit(1) - else: - # Via SSH (from container) - cmd = f"mkdir -p {BL_MSG_DIR} && echo '{line}' >> {BL_MSG_DIR}/{to}.log && test -f {BL_MSG_DIR}/{to}.log && echo OK" - out, rc = bl_run_container(cmd) - if rc == 0 and "OK" in out: - print(f"OPP-DM sent to {to} (verified)") - return True - else: - print(f"ERROR: Send failed: {out}", file=sys.stderr) - sys.exit(1) - -def opp_read(who="operator-main", n=5): - """Read operator DMs. Works from container or bl.""" - if who not in ["646", "operator-main"]: - print(f"ERROR: Unknown operator {who}", file=sys.stderr) - sys.exit(1) - - if ON_BL: - logfile = BL_MSG_DIR / f"{who}.log" - if not logfile.exists(): - print("no messages") - return - with open(logfile) as f: - lines = f.readlines() - for l in lines[-n:]: - print(l.strip()) - else: - cmd = f"tail -n {n} {BL_MSG_DIR}/{who}.log 2>/dev/null || echo 'no messages'" - out, rc = bl_run_container(cmd) - print(out) - -def main(): - p = argparse.ArgumentParser(description="OPP-DM: Operator DMs (fixed)") - sub = p.add_subparsers(dest='cmd', required=True) - - ps = sub.add_parser('send', help='Send operator DM') - ps.add_argument('--to', required=True, choices=['646', 'operator-main']) - ps.add_argument('--from', dest='from_who', default='operator-main') - ps.add_argument('message', help='Message') - ps.set_defaults(func=lambda a: opp_send(a.to, a.message, a.from_who)) - - pr = sub.add_parser('read', help='Read operator DMs') - pr.add_argument('--who', default='operator-main', choices=['646', 'operator-main']) - pr.add_argument('--n', type=int, default=5) - pr.set_defaults(func=lambda a: opp_read(a.who, a.n)) - - args = p.parse_args() - args.func(args) - -if __name__ == '__main__': - main() diff --git a/bridge/dm-listener-watermark.json b/bridge/dm-listener-watermark.json new file mode 100644 index 0000000..6795280 --- /dev/null +++ b/bridge/dm-listener-watermark.json @@ -0,0 +1,6 @@ +{ + "processed": [ + "muse:Test from 646, DM automation is live.", + "pip:Hello from 646. Testing DM to your main " + ] +} \ No newline at end of file diff --git a/bridge/thread-watermark.json b/bridge/thread-watermark.json new file mode 100644 index 0000000..c5db502 --- /dev/null +++ b/bridge/thread-watermark.json @@ -0,0 +1,3 @@ +{ + "last_processed": "muse:6db43088:You're idle and the P2P bridge" +} \ No newline at end of file diff --git a/docs/PHONE-OTP.md b/docs/PHONE-OTP.md new file mode 100644 index 0000000..cdb9e7e --- /dev/null +++ b/docs/PHONE-OTP.md @@ -0,0 +1,73 @@ +# Phone-OTP Login Flow + +**Status:** proven on bl (2026-10-03) +Alternative to the email-OTP path for muse.ai login; same form, same OTP +mechanics, different delivery channel (SMS instead of email). + +## When + +Fresh chrome-box profile + NetVM node, no session yet. Used when the +account's primary identifier is a mobile number rather than an email. + +## Prerequisites + +- chrome-box profile + NetVM node provisioned on bl (1:1: profile == node + == Warp identity). See `README.md` "Provisioning a node". +- A real mobile number that can receive SMS — **human-provided per login + attempt, never stored, never written to any registry, log, or memory.** + The registry records only `phone_otp: yes` as the route. + +## Flow + +1. Launch headless Chromium in the node's netns: + `netvm-chrome.sh --headless https://muse.ai` + (CDP on by default for automation.) +2. Via CDP: open muse.ai → "Log in" → inline form "Mobile number or email". +3. Enter the human-provided mobile number (E.164) and Continue. +4. If the number maps to **multiple Meta accounts**, an account-selection + UI appears (observed 2026-10-03: "Meta Account" vs "piparada" with + Instagram avatar — see `docs/meta-account-selection.png`). Select the + intended account. +5. muse.ai sends a 6-digit SMS OTP. +6. Human reads the SMS and relays the OTP (board/chat handoff, or direct). +7. Enter the OTP via CDP and submit. Session established, cached in the + profile. +8. Verify: revisit muse.ai via CDP, confirm logged-in markers, no login + wall. Flip the `ACCOUNTS.md` row to `active`. + +## Operator/human split + +- **Agent:** browser launch, CDP automation, form entry, OTP submission, + verification. Never sees the phone number beyond the single form entry. +- **Human:** provides the number per attempt, receives the SMS, relays the + OTP. The only party that ever holds the number. +- **Operator:** provisions the node, coordinates, maintains the registry. + +## PII rules + +- The phone number is PII. It goes into the login form and nowhere else — + not the registry, not logs, not chat history, not memory. +- OTP codes are relayed, used once, never stored. +- If the number is linked to other accounts (e.g. Facebook), that linkage + is managed via the separate Meta Accounts Center API + (`docs/META-ACCOUNTS-API.md`) — never conflated with the OTP flow. + +## Failure modes + +- **OTP expired:** re-request from the form (note rate limits if hit). +- **SMS delayed:** wait 60s, retry once, then escalate (possible carrier + filtering). +- **Session lost on browser restart:** observed 2026-10-03 (`pip` node) — + re-auth from step 1. If recurrent, investigate session persistence. +- **Wrong number entered:** restart from step 1. +- **Account-selection ambiguity:** confirm with the human which account + before proceeding; never guess. + +## Provenance + +- 2026-10-03: `646` — phone OTP login completed (first Meta Account + option), bl. +- 2026-10-03: `pip` (piparada) — phone OTP login completed; session lost + on browser restart, needs re-auth. +- 3 OTP codes used 2026-10-03 without rate-limit issues. +- Egress: bl Warp (104.28.195.181) accepted without bot-detection friction. diff --git a/snapshots/meta-ac/baseline.json b/snapshots/meta-ac/baseline.json new file mode 100644 index 0000000..137e623 --- /dev/null +++ b/snapshots/meta-ac/baseline.json @@ -0,0 +1,21 @@ +{ + "redirect_chain": [ + "https://accountscenter.meta.com/", + "https://auth.meta.com/?waterfall_id=d468891e-29bd-426d-9516-997753deb786&redirect_uri=https%3A%2F%2Fauth.meta.com%2Foidc%2F%3Fapp_id%3D633385687760560%26nonce%3DAdTU08Ikjcm9mTIgoaGrOVdc2Hw%26redirect_uri%3Dhttps%253A%252F%252Faccountscenter.meta.com%252Foidc%252Fcallback%252F%26response_type%3Dcode%26scope%3Dopenid%26state%3DATp-FrkXUmikeLjBdUcvg88XWZqfg28_w4CfZo0pU1jvEZhZDxHUizuDNqY3DshXdxThHi2x3FCqCt83Iu33y6UbRDBm7gK1XsydiOLSex3CeO8NHJ1vpzdAp_pc5Jiks73LDbvyaxt5sFFzmJNXZdndeOnFC--DIn_PJ2jkG1rkG7lIZc0ltpTX2PZG3wdcmPHD1BQed-tioVY%26waterfall_id%3Dd468891e-29bd-426d-9516-997753deb786&source_app_id=633385687760560&force_reauth=0&rcs=ATrhOU75HRnKLZwPMD8f-3q_cCgOxaPnUSlIEwIb_UpHqJHfYVMuMCfFZp8BGu3zv62sTRVOXqkISEKZGke18uwCrhZjg5mz_FyTEo_dh_rHrmoZMR2mKQkw8fEFYzeY_3sWXdtAo0PjePnQJG8ehIXwBRVkodPqg9N9LorT5pe-DN8X4yOty4lJ3tA" + ], + "final_url": "https://auth.meta.com/?waterfall_id=d468891e-29bd-426d-9516-997753deb786&redirect_uri=https%3A%2F%2Fauth.meta.com%2Foidc%2F%3Fapp_id%3D633385687760560%26nonce%3DAdTU08Ikjcm9mTIgoaGrOVdc2Hw%26redirect_uri%3Dhttps%253A%252F%252Faccountscenter.meta.com%252Foidc%252Fcallback%252F%26response_type%3Dcode%26scope%3Dopenid%26state%3DATp-FrkXUmikeLjBdUcvg88XWZqfg28_w4CfZo0pU1jvEZhZDxHUizuDNqY3DshXdxThHi2x3FCqCt83Iu33y6UbRDBm7gK1XsydiOLSex3CeO8NHJ1vpzdAp_pc5Jiks73LDbvyaxt5sFFzmJNXZdndeOnFC--DIn_PJ2jkG1rkG7lIZc0ltpTX2PZG3wdcmPHD1BQed-tioVY%26waterfall_id%3Dd468891e-29bd-426d-9516-997753deb786&source_app_id=633385687760560&force_reauth=0&rcs=ATrhOU75HRnKLZwPMD8f-3q_cCgOxaPnUSlIEwIb_UpHqJHfYVMuMCfFZp8BGu3zv62sTRVOXqkISEKZGke18uwCrhZjg5mz_FyTEo_dh_rHrmoZMR2mKQkw8fEFYzeY_3sWXdtAo0PjePnQJG8ehIXwBRVkodPqg9N9LorT5pe-DN8X4yOty4lJ3tA", + "final_title": "Meta", + "dom_markers": { + "forms": [], + "inputs": [], + "buttons": [ + "Continue with Facebook", + "Continue with Instagram", + "Use mobile number or email", + "English (US)" + ] + }, + "node": "phone", + "captured_at": "2026-10-03T17:42:26.963885+00:00", + "egress_ip": "104.28.195.181" +} \ No newline at end of file diff --git a/snapshots/meta-ac/snap-20261003-174227.json b/snapshots/meta-ac/snap-20261003-174227.json new file mode 100644 index 0000000..137e623 --- /dev/null +++ b/snapshots/meta-ac/snap-20261003-174227.json @@ -0,0 +1,21 @@ +{ + "redirect_chain": [ + "https://accountscenter.meta.com/", + "https://auth.meta.com/?waterfall_id=d468891e-29bd-426d-9516-997753deb786&redirect_uri=https%3A%2F%2Fauth.meta.com%2Foidc%2F%3Fapp_id%3D633385687760560%26nonce%3DAdTU08Ikjcm9mTIgoaGrOVdc2Hw%26redirect_uri%3Dhttps%253A%252F%252Faccountscenter.meta.com%252Foidc%252Fcallback%252F%26response_type%3Dcode%26scope%3Dopenid%26state%3DATp-FrkXUmikeLjBdUcvg88XWZqfg28_w4CfZo0pU1jvEZhZDxHUizuDNqY3DshXdxThHi2x3FCqCt83Iu33y6UbRDBm7gK1XsydiOLSex3CeO8NHJ1vpzdAp_pc5Jiks73LDbvyaxt5sFFzmJNXZdndeOnFC--DIn_PJ2jkG1rkG7lIZc0ltpTX2PZG3wdcmPHD1BQed-tioVY%26waterfall_id%3Dd468891e-29bd-426d-9516-997753deb786&source_app_id=633385687760560&force_reauth=0&rcs=ATrhOU75HRnKLZwPMD8f-3q_cCgOxaPnUSlIEwIb_UpHqJHfYVMuMCfFZp8BGu3zv62sTRVOXqkISEKZGke18uwCrhZjg5mz_FyTEo_dh_rHrmoZMR2mKQkw8fEFYzeY_3sWXdtAo0PjePnQJG8ehIXwBRVkodPqg9N9LorT5pe-DN8X4yOty4lJ3tA" + ], + "final_url": "https://auth.meta.com/?waterfall_id=d468891e-29bd-426d-9516-997753deb786&redirect_uri=https%3A%2F%2Fauth.meta.com%2Foidc%2F%3Fapp_id%3D633385687760560%26nonce%3DAdTU08Ikjcm9mTIgoaGrOVdc2Hw%26redirect_uri%3Dhttps%253A%252F%252Faccountscenter.meta.com%252Foidc%252Fcallback%252F%26response_type%3Dcode%26scope%3Dopenid%26state%3DATp-FrkXUmikeLjBdUcvg88XWZqfg28_w4CfZo0pU1jvEZhZDxHUizuDNqY3DshXdxThHi2x3FCqCt83Iu33y6UbRDBm7gK1XsydiOLSex3CeO8NHJ1vpzdAp_pc5Jiks73LDbvyaxt5sFFzmJNXZdndeOnFC--DIn_PJ2jkG1rkG7lIZc0ltpTX2PZG3wdcmPHD1BQed-tioVY%26waterfall_id%3Dd468891e-29bd-426d-9516-997753deb786&source_app_id=633385687760560&force_reauth=0&rcs=ATrhOU75HRnKLZwPMD8f-3q_cCgOxaPnUSlIEwIb_UpHqJHfYVMuMCfFZp8BGu3zv62sTRVOXqkISEKZGke18uwCrhZjg5mz_FyTEo_dh_rHrmoZMR2mKQkw8fEFYzeY_3sWXdtAo0PjePnQJG8ehIXwBRVkodPqg9N9LorT5pe-DN8X4yOty4lJ3tA", + "final_title": "Meta", + "dom_markers": { + "forms": [], + "inputs": [], + "buttons": [ + "Continue with Facebook", + "Continue with Instagram", + "Use mobile number or email", + "English (US)" + ] + }, + "node": "phone", + "captured_at": "2026-10-03T17:42:26.963885+00:00", + "egress_ip": "104.28.195.181" +} \ No newline at end of file diff --git a/snapshots/meta-ac/snap-20261003-174329.json b/snapshots/meta-ac/snap-20261003-174329.json new file mode 100644 index 0000000..e3db6d1 --- /dev/null +++ b/snapshots/meta-ac/snap-20261003-174329.json @@ -0,0 +1,21 @@ +{ + "redirect_chain": [ + "https://accountscenter.meta.com/", + "https://auth.meta.com/?waterfall_id=cb070318-7a4b-49e6-8fc3-d290170d43a6&redirect_uri=https%3A%2F%2Fauth.meta.com%2Foidc%2F%3Fapp_id%3D633385687760560%26nonce%3DAdTU08Ikjcm9mTIgoaGrOVdcRAw%26redirect_uri%3Dhttps%253A%252F%252Faccountscenter.meta.com%252Foidc%252Fcallback%252F%26response_type%3Dcode%26scope%3Dopenid%26state%3DATpq0JxkZwqHfWFB0jh5kR6RenDkDrmgBt5LE0KJyZfdDnriGRq9OoNdvrAZlzfXnMdVIWAqUbc2xusK10YYetA7LuvVjmBYrbh49GgzHiSRknK6ZkW1Cunv1PvlIq3rKo9vsNNrk-_HfmdRqcbal8P6msApW3B8yp_LbOC5smAQUFE-0tdRyXXuOjDfaC2Qcb_wrV5WRTzzYLM%26waterfall_id%3Dcb070318-7a4b-49e6-8fc3-d290170d43a6&source_app_id=633385687760560&force_reauth=0&rcs=ATq7O-Ly9QzkbaEB6ft9NPrYs_cEB5CoxdWH8ejoTxYwqOzsNA4rOBiSOctEtS-xvWC8YFdIq7HgGcRPk2uoM-Nrs0LujStVyK18UHX2EXj7A_Po1Bg6gv7zPq25hTzUS5fExigM8FPTl86JtcctntHqe9XU-81ZpWJjlu_s1OnO_JNtqBZb1LBqFz8" + ], + "final_url": "https://auth.meta.com/?waterfall_id=cb070318-7a4b-49e6-8fc3-d290170d43a6&redirect_uri=https%3A%2F%2Fauth.meta.com%2Foidc%2F%3Fapp_id%3D633385687760560%26nonce%3DAdTU08Ikjcm9mTIgoaGrOVdcRAw%26redirect_uri%3Dhttps%253A%252F%252Faccountscenter.meta.com%252Foidc%252Fcallback%252F%26response_type%3Dcode%26scope%3Dopenid%26state%3DATpq0JxkZwqHfWFB0jh5kR6RenDkDrmgBt5LE0KJyZfdDnriGRq9OoNdvrAZlzfXnMdVIWAqUbc2xusK10YYetA7LuvVjmBYrbh49GgzHiSRknK6ZkW1Cunv1PvlIq3rKo9vsNNrk-_HfmdRqcbal8P6msApW3B8yp_LbOC5smAQUFE-0tdRyXXuOjDfaC2Qcb_wrV5WRTzzYLM%26waterfall_id%3Dcb070318-7a4b-49e6-8fc3-d290170d43a6&source_app_id=633385687760560&force_reauth=0&rcs=ATq7O-Ly9QzkbaEB6ft9NPrYs_cEB5CoxdWH8ejoTxYwqOzsNA4rOBiSOctEtS-xvWC8YFdIq7HgGcRPk2uoM-Nrs0LujStVyK18UHX2EXj7A_Po1Bg6gv7zPq25hTzUS5fExigM8FPTl86JtcctntHqe9XU-81ZpWJjlu_s1OnO_JNtqBZb1LBqFz8", + "final_title": "Meta", + "dom_markers": { + "forms": [], + "inputs": [], + "buttons": [ + "Continue with Facebook", + "Continue with Instagram", + "Use mobile number or email", + "English (US)" + ] + }, + "node": "phone", + "captured_at": "2026-10-03T17:43:29.468850+00:00", + "egress_ip": "104.28.195.181" +} \ No newline at end of file