app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
master
John Lauer
v2.0.658 (insiders): extension lane closes the smoke gaps (download fields, wiki view + auto-sign, owner chip); adom-de…
841e718
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
//! Wiki verbs and the wiki sign-in: pup_wiki_set_view (authed | public | toggle, the server-side
//! ?view=public render), the auth probe (/auth/me with the page's own cookies), and the one-shot
//! auto sign-in from the desktop token (~/.adom/session.json -> wiki browser-session magic URL).
//! Ports checkWikiAuth / readDesktopWikiToken / mintWikiMagicUrl / autoSignWikiFromDesktop.
use crate::input::resolve_tab;
use crate::{browser_for, touch, Ctx};
use pup_core::{err, ok, Caller};
use serde_json::{json, Value};
use std::time::Duration;
const T: Duration = Duration::from_secs(10);
pub fn public_url(u: &str) -> String { if u.contains("view=public") { u.into() } else if u.contains('?') { format!("{u}&view=public") } else { format!("{u}?view=public") } }
pub fn strip_public(u: &str) -> String { u.replace("?view=public&", "?").replace("&view=public", "").replace("?view=public", "") }
/// {authed, user?, unknown?}: an indeterminate probe is never reported as logged out.
pub fn check_auth(cdp: &pup_cdp::Cdp, sid: &str) -> Value {
let js = r#"(async () => { try { const r = await fetch('/auth/me', { credentials: 'same-origin' }); if (!r.ok) return (r.status === 401 || r.status === 403) ? { authed: false } : { authed: false, unknown: true }; const j = await r.json(); return { authed: !!(j && j.user), user: j && j.user && (j.user.displayName || j.user.username) }; } catch (e) { return { authed: false, unknown: true }; } })()"#;
cdp.evaluate(sid, js, Duration::from_secs(5)).ok().filter(|v| v.is_object()).unwrap_or(json!({ "authed": false, "unknown": true }))
}
pub fn desktop_token(ctx: &Ctx) -> Option<(String, Option<String>)> {
let v: Value = serde_json::from_str(&std::fs::read_to_string(ctx.host.state_dir().join("session.json")).ok()?).ok()?;
let tok = v["session_token"].as_str()?.to_string();
Some((tok, v["user_name"].as_str().or(v["user_email"].as_str()).map(|s| s.to_string())))
}
pub fn mint_magic_url(ctx: &Ctx, next_path: &str) -> Option<String> {
let (tok, _) = desktop_token(ctx)?;
let r = ureq::AgentBuilder::new().timeout(Duration::from_secs(12)).build().post("https://wiki.adom.inc/api/v1/auth/browser-session").set("content-type", "application/json").set("authorization", &format!("Bearer {tok}")).set("user-agent", "adom-pup-bridge").send_json(json!({})).ok()?;
let j: Value = r.into_json().ok()?;
let url = j["url"].as_str()?;
Some(format!("{url}{}next={}", if url.contains('?') { "&" } else { "?" }, urlencode(next_path)))
}
fn urlencode(s: &str) -> String { let mut o = String::new(); for b in s.bytes() { match b { b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => o.push(b as char), _ => o.push_str(&format!("%{b:02X}")) } } o }
/// Sign the page in from the desktop token, then confirm with the auth probe. Returns who.
pub fn auto_sign(ctx: &Ctx, cdp: &pup_cdp::Cdp, sid: &str, next_path: &str) -> Option<String> {
let magic = mint_magic_url(ctx, next_path)?;
let _ = cdp.navigate(sid, &magic, Duration::from_secs(25));
for _ in 0..6 { let a = check_auth(cdp, sid); if a["authed"] == json!(true) { return Some(a["user"].as_str().unwrap_or("authed").to_string()); } std::thread::sleep(Duration::from_millis(500)); }
desktop_token(ctx).and_then(|(_, u)| u).or(Some("authed".into()))
}
/// After an open/navigate onto an Adom URL: probe, auto-sign when the desktop has a token, and
/// return the wiki hint fields for the response (Node: the open path's _wikiAuth block).
pub fn after_navigate(ctx: &Ctx, s: &pup_session::Session, cdp: &pup_cdp::Cdp, sid: &str, url: &str, view: Option<&str>, isolated: bool) -> Value {
if !pup_identity::is_adom_url(url) { return json!({}); }
if view == Some("public") { return json!({ "wikiView": "public", "_wikiHint": "Wiki PUBLIC view: the SERVER rendered this page exactly as a signed-out visitor sees it (?view=public). Reopen without wikiView (or view:\"authed\") for the logged-in view." }); }
let mut auth = check_auth(cdp, sid);
if auth["authed"] == json!(false) && auth["unknown"] != json!(true) && !isolated && desktop_token(ctx).is_some() {
let next = url.split("//").nth(1).and_then(|r| r.find('/').map(|i| r[i..].to_string())).unwrap_or_else(|| "/adom".into());
if let Some(who) = auto_sign(ctx, cdp, sid, &next) { auth = json!({ "authed": true, "user": who, "autoSigned": true }); pup_core::log(format!("[wikiview] \"{}\" AUTO-SIGNED from the desktop token ({who})", s.id)); }
}
let authed = auth["authed"] == json!(true);
ctx.sessions.with_mut(&s.id, |x| { x.wiki_authed = Some(authed); x.wiki_view = Some(if authed { "authed" } else { "public" }.into()); });
json!({ "wikiView": if authed { "authed" } else { "public" }, "wikiAuthed": authed, "wikiUser": auth["user"], "wikiLoginNeeded": if !authed && auth["unknown"] != json!(true) { json!(true) } else { Value::Null },
"_wikiHint": if authed { format!("Wiki opened LOGGED IN (as {}): private source, drafts, owner-only cards. For the anonymous view reopen with wikiView:\"public\" (same window, server-side render).", auth["user"].as_str().unwrap_or("the user")) } else if auth["unknown"] == json!(true) { "Wiki auth state could not be probed on this page (not evidence of logout).".to_string() } else { "Wiki is LOGGED OUT in the shared profile and no desktop token could sign it in. One-time login: pup_raise_os_window {foregroundReason:\"sign into the Adom wiki\"} and let the user complete the SSO; every pup window is then signed in.".to_string() } })
}
pub fn check_auth_ext(lane: &crate::transport::Lane, tab: &pup_session::Tab) -> Value {
let js = r#"(async () => { try { const r = await fetch('/auth/me', { credentials: 'same-origin' }); if (!r.ok) return (r.status === 401 || r.status === 403) ? { authed: false } : { authed: false, unknown: true }; const j = await r.json(); return { authed: true, user: (j.user && (j.user.name || j.user.email)) || j.name || j.email || 'authed' }; } catch (e) { return { authed: false, unknown: true }; } })()"#;
crate::transport::evaluate(lane, tab, js).ok().and_then(|v| v.as_str().and_then(|x| serde_json::from_str::<Value>(x).ok()).or_else(|| if v.is_object() { Some(v) } else { None })).unwrap_or(json!({ "authed": false, "unknown": true }))
}
pub fn auto_sign_ext(ctx: &Ctx, lane: &crate::transport::Lane, tab: &pup_session::Tab, next_path: &str) -> Option<String> {
let magic = mint_magic_url(ctx, next_path)?;
let _ = crate::transport::navigate(lane, tab, &magic);
for _ in 0..6 { let a = check_auth_ext(lane, tab); if a["authed"] == json!(true) { return Some(a["user"].as_str().unwrap_or("authed").to_string()); } std::thread::sleep(Duration::from_millis(500)); }
desktop_token(ctx).and_then(|(_, u)| u).or(Some("authed".into()))
}
/// 2.0.653: the extension-lane twin of after_navigate (the smoke found wikiView missing on that lane).
pub fn after_navigate_ext(ctx: &Ctx, s: &pup_session::Session, lane: &crate::transport::Lane, tab: &pup_session::Tab, url: &str, view: Option<&str>, isolated: bool) -> Value {
if !pup_identity::is_adom_url(url) { return json!({}); }
let _ = crate::transport::wait_ready(lane, tab, url, Duration::from_secs(8));
if view == Some("public") { return json!({ "wikiView": "public", "_wikiHint": "Wiki PUBLIC view: the SERVER rendered this page exactly as a signed-out visitor sees it (?view=public). Reopen without wikiView (or view:\"authed\") for the logged-in view." }); }
let mut auth = check_auth_ext(lane, tab);
if auth["authed"] == json!(false) && auth["unknown"] != json!(true) && !isolated && desktop_token(ctx).is_some() {
let next = url.split("//").nth(1).and_then(|r| r.find('/').map(|i| r[i..].to_string())).unwrap_or_else(|| "/adom".into());
if let Some(who) = auto_sign_ext(ctx, lane, tab, &next) { auth = json!({ "authed": true, "user": who, "autoSigned": true }); pup_core::log(format!("[wikiview] \"{}\" AUTO-SIGNED from the desktop token ({who})", s.id)); }
}
let authed = auth["authed"] == json!(true);
ctx.sessions.with_mut(&s.id, |x| { x.wiki_authed = Some(authed); x.wiki_view = Some(if authed { "authed" } else { "public" }.into()); });
json!({ "wikiView": if authed { "authed" } else { "public" }, "wikiAuthed": authed, "wikiUser": auth["user"], "wikiLoginNeeded": if !authed && auth["unknown"] != json!(true) { json!(true) } else { Value::Null },
"_wikiHint": if authed { format!("Wiki opened LOGGED IN (as {}): private source, drafts, owner-only cards. For the anonymous view reopen with wikiView:\"public\" (same window, server-side render).", auth["user"].as_str().unwrap_or("the user")) } else if auth["unknown"] == json!(true) { "Wiki auth state could not be probed on this page (not evidence of logout).".to_string() } else { "Wiki is LOGGED OUT in the shared profile and no desktop token could sign it in. One-time login: pup_raise_os_window {foregroundReason:\"sign into the Adom wiki\"} and let the user complete the SSO; every pup window is then signed in.".to_string() } })
}
pub fn set_view(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {
let (s, t, b) = match resolve_tab(ctx, caller, args) { Ok(x) => x, Err(e) => return e };
let url = t.url.clone();
if !pup_identity::is_adom_url(&url) { return err("not_wiki", "wiki view toggle applies only to Adom (adom.inc) URLs", None); }
let cur = if url.contains("view=public") { "public" } else { "authed" };
let want = match args["view"].as_str().unwrap_or("toggle") { "toggle" => if cur == "public" { "authed" } else { "public" }, "authed" | "public" => args["view"].as_str().unwrap(), other => return err("bad_view", format!("view must be one of authed|public|toggle (got \"{other}\")"), None) };
if want == cur { return ok(json!({ "success": true, "output": json!({ "ok": true, "view": want, "unchanged": true, "url": url }).to_string(), "view": want, "unchanged": true })); }
let target = if want == "public" { public_url(&url) } else { strip_public(&url) };
let loaded = b.cdp.navigate(&t.cdp_session, &target, Duration::from_secs(25)).map(|(_, l)| l).unwrap_or(false);
let extra = after_navigate(ctx, &s, &b.cdp, &t.cdp_session, &target, Some(want), false);
// 2.0.656: the flip loads a new document; on the extension lane give it the page scripts (owner chip, floaty)
// like every navigate does (the smoke checks the chip right after this flip).
if crate::transport::is_ext(&s) { if let Ok(lane) = crate::transport::lane_for(ctx, &s) { let mut tt = t.clone(); tt.url = target.clone(); crate::inject_page_scripts_ext(ctx, &s, &lane, &tt); } }
let _ = crate::park::bottom_unless_foreground(ctx, &s.id, true);
touch(ctx, &s.id, None);
if let Some(a) = ctx.arc() { crate::identity::schedule(a, s.id.clone(), caller.clone(), true); }
pup_core::merge(ok(json!({ "success": true, "output": json!({ "ok": true, "view": want, "url": target, "loaded": loaded }).to_string(), "view": want, "url": target, "sessionId": s.id, "window": s.id, "aiThread": s.owner })), extra)
}
#[allow(dead_code)]
fn _unused(ctx: &Ctx) { let _ = browser_for(ctx, "x"); let _ = T; }