Skip to main content

agent_first_http/sdk/fetch/artifacts/
storage.rs

1//! `storage` artifact: localStorage, sessionStorage, and IndexedDB database
2//! names captured via `Runtime.evaluate`. Default off — sensitive data risk.
3//! Capped at 256 KiB of JSON; values truncated if the cap is exceeded.
4
5use std::path::PathBuf;
6
7use serde::Serialize;
8
9use crate::sdk::cdp::ws_client::Connection;
10use crate::sdk::fetch::writer;
11use crate::shared::artifacts::ArtifactPaths;
12use crate::shared::error::{Error, ErrorCode};
13
14/// Hard cap on the serialized JSON size. Values are truncated if exceeded.
15const SIZE_CAP_BYTES: usize = 256 * 1024;
16
17#[derive(Serialize)]
18pub struct StorageSnapshot {
19    pub schema_version: u32,
20    pub url: String,
21    pub local_storage: serde_json::Value,
22    pub session_storage: serde_json::Value,
23    /// IndexedDB database names only (no full dump — size and privacy risk).
24    pub indexed_db_names: Vec<String>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub truncated: Option<TruncationNote>,
27}
28
29#[derive(Serialize)]
30pub struct TruncationNote {
31    pub reason: String,
32    pub cap_bytes: usize,
33}
34
35/// Capture localStorage, sessionStorage, and IndexedDB names via CDP.
36pub async fn capture(
37    conn: &Connection,
38    session_id: &str,
39    url: &str,
40) -> Result<StorageSnapshot, Error> {
41    // localStorage
42    let ls = eval_json(
43        conn,
44        session_id,
45        "(function(){ var o={}; for(var i=0;i<localStorage.length;i++){ \
46         var k=localStorage.key(i); o[k]=localStorage.getItem(k); } \
47         return JSON.stringify(o); })()",
48    )
49    .await
50    .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
51
52    // sessionStorage
53    let ss = eval_json(
54        conn,
55        session_id,
56        "(function(){ var o={}; for(var i=0;i<sessionStorage.length;i++){ \
57         var k=sessionStorage.key(i); o[k]=sessionStorage.getItem(k); } \
58         return JSON.stringify(o); })()",
59    )
60    .await
61    .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
62
63    // IndexedDB names
64    let idb_names = eval_str_array(
65        conn,
66        session_id,
67        "indexedDB.databases().then(dbs => JSON.stringify(dbs.map(d => d.name || '')))",
68    )
69    .await
70    .unwrap_or_default();
71
72    let mut snapshot = StorageSnapshot {
73        schema_version: 1,
74        url: url.to_string(),
75        local_storage: ls,
76        session_storage: ss,
77        indexed_db_names: idb_names,
78        truncated: None,
79    };
80
81    // Check size cap.
82    if let Ok(json) = serde_json::to_string(&snapshot) {
83        if json.len() > SIZE_CAP_BYTES {
84            snapshot.local_storage = serde_json::Value::Null;
85            snapshot.session_storage = serde_json::Value::Null;
86            snapshot.indexed_db_names = Vec::new();
87            snapshot.truncated = Some(TruncationNote {
88                reason: format!(
89                    "serialized size {} bytes exceeded cap {}; values omitted",
90                    json.len(),
91                    SIZE_CAP_BYTES
92                ),
93                cap_bytes: SIZE_CAP_BYTES,
94            });
95        }
96    }
97
98    Ok(snapshot)
99}
100
101pub async fn write(paths: &ArtifactPaths, snapshot: &StorageSnapshot) -> Result<PathBuf, Error> {
102    let target = paths.file_for(crate::shared::artifacts::Artifact::Storage);
103    writer::ensure_dir(&paths.root).await?;
104    let json = serde_json::to_vec_pretty(snapshot)
105        .map_err(|e| Error::new(ErrorCode::InternalError, format!("storage: serialize: {e}")))?;
106    writer::write_bytes(&target, &json).await?;
107    Ok(target)
108}
109
110async fn eval_json(
111    conn: &Connection,
112    session_id: &str,
113    expr: &str,
114) -> Result<serde_json::Value, Error> {
115    let r = conn
116        .send(
117            "Runtime.evaluate",
118            &serde_json::json!({
119                "expression": expr,
120                "returnByValue": true,
121                "awaitPromise": true,
122            }),
123            Some(session_id),
124        )
125        .await?;
126    let s = r["result"]["value"].as_str().unwrap_or("{}");
127    serde_json::from_str(s).map_err(|e| {
128        Error::new(
129            ErrorCode::ArtifactCaptureFailed,
130            format!("storage: parse json: {e}"),
131        )
132    })
133}
134
135async fn eval_str_array(
136    conn: &Connection,
137    session_id: &str,
138    expr: &str,
139) -> Result<Vec<String>, Error> {
140    let r = conn
141        .send(
142            "Runtime.evaluate",
143            &serde_json::json!({
144                "expression": expr,
145                "returnByValue": true,
146                "awaitPromise": true,
147            }),
148            Some(session_id),
149        )
150        .await?;
151    let s = r["result"]["value"].as_str().unwrap_or("[]");
152    serde_json::from_str::<Vec<serde_json::Value>>(s)
153        .map(|arr| {
154            arr.into_iter()
155                .filter_map(|v| v.as_str().map(str::to_string))
156                .collect()
157        })
158        .map_err(|e| {
159            Error::new(
160                ErrorCode::ArtifactCaptureFailed,
161                format!("storage: parse idb names: {e}"),
162            )
163        })
164}