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
//! 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; }