master
John Lauer 1.3.0: Dock Bar contract (serve --print-url, /api/status LEDs, CORS, say strip), settings modal, taskbar progress, pup vault for logins, adom-bridge + pup_* transport, dashboard overhaul per adom-ui-design, doctor for first run 83b2ae8 1mo ago
//! Vendor logins live in pup's OS-keychain vault (pup-credentials skill);
//! chip-fetcher keeps no password file of its own. This module is a thin
//! proxy over the `credential_*` / `pup_login` / `pup_import_*` verbs so the
//! CLI and the dashboard never spell them out.

use anyhow::Result;
use serde_json::{json, Value};

/// Vendors whose logins are worth importing from the user's real browser.
pub const DEFAULT_VENDOR_HOSTS: &[&str] = &[
    "componentsearchengine",
    "samacsys",
    "snapeda",
    "snapmagic",
    "ultralibrarian",
    "mouser",
    "digikey",
    "arrow.com",
    "ti.com",
    "st.com",
    "nxp.com",
    "microchip",
    "nordicsemi",
    "lcsc",
];

/// `{ok, credentials:[{host, username, ...}]}` with passwords never present.
pub fn list() -> Result<Value> {
    let v = crate::bridge::call("credential_list", &json!({}))?;
    if !crate::bridge::succeeded(&v) {
        return Ok(json!({
            "ok": false,
            "error": crate::bridge::error_text(&v),
            "_hint": "Vendor logins are stored by pup on your desktop. Open Adom Hydrogen there so the bridge is connected, then reload.",
        }));
    }
    let out = crate::bridge::output(&v);
    let mut creds: Vec<Value> = out
        .get("credentials")
        .and_then(|c| c.as_array())
        .cloned()
        .unwrap_or_default();
    for c in creds.iter_mut() {
        if let Some(o) = c.as_object_mut() {
            o.remove("password");
        }
    }
    Ok(json!({
        "ok": true,
        "source": "pup",
        "credentials": creds,
        "_hint": "Passwords stay in the OS keychain on the desktop. Add one with `adom-chip-fetcher login <source> --user <u> --pass <p>` or import from your browser with `adom-chip-fetcher logins --import`.",
    }))
}

pub fn set(host: &str, username: &str, password: &str) -> Result<Value> {
    let v = crate::bridge::call("credential_set", &json!({"host": host, "username": username, "password": password}))?;
    if crate::bridge::succeeded(&v) {
        Ok(json!({"ok": true, "host": host, "username": username}))
    } else {
        Ok(json!({"ok": false, "error": crate::bridge::error_text(&v)}))
    }
}

pub fn delete(host: &str) -> Result<Value> {
    let v = crate::bridge::call("credential_delete", &json!({"host": host}))?;
    if crate::bridge::succeeded(&v) {
        Ok(json!({"ok": true, "removed": true, "host": host}))
    } else {
        Ok(json!({"ok": false, "error": crate::bridge::error_text(&v)}))
    }
}

/// Read-only scan of the browser profiles on the desktop.
pub fn import_scan() -> Result<Value> {
    let v = crate::bridge::call("pup_import_scan", &json!({}))?;
    Ok(if crate::bridge::succeeded(&v) { crate::bridge::output(&v) } else { json!({"ok": false, "error": crate::bridge::error_text(&v)}) })
}

/// Import the user's saved browser logins (one consent dialog + one UAC on
/// the desktop). Counts only come back; plaintext never leaves pup.
pub fn import_browser_logins(host_filter: &[String]) -> Result<Value> {
    let hosts: Vec<&str> = if host_filter.is_empty() { DEFAULT_VENDOR_HOSTS.to_vec() } else { host_filter.iter().map(|s| s.as_str()).collect() };
    let v = crate::bridge::call("pup_import_browser_logins", &json!({"hostFilter": hosts}))?;
    Ok(if crate::bridge::succeeded(&v) { crate::bridge::output(&v) } else { json!({"ok": false, "error": crate::bridge::error_text(&v)}) })
}

/// Sign into the page currently open in `session` from the vault (or with
/// explicit credentials, saved for next time). Submit is reason-gated by pup.
pub fn login(session: &str, username: Option<&str>, password: Option<&str>, host: Option<&str>, reason: &str) -> Result<Value> {
    let mut args = json!({"sessionId": session, "submit": true, "submitReason": reason});
    if let (Some(u), Some(p)) = (username, password) {
        args["username"] = json!(u);
        args["password"] = json!(p);
        args["save"] = json!(true);
        if let Some(h) = host {
            args["host"] = json!(h);
        }
    }
    let v = crate::bridge::call("pup_login", &args)?;
    if crate::bridge::succeeded(&v) {
        Ok(crate::bridge::output(&v))
    } else {
        Ok(json!({
            "ok": false,
            "errorCode": v.get("errorCode").cloned().unwrap_or(Value::Null),
            "error": crate::bridge::error_text(&v),
        }))
    }
}