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