Skip to content

Commit 62b2f6d

Browse files
committed
feat(browser): inject_cookies() + user_data_dir builder with auto tempdir (#11, #12)
1 parent 52138f9 commit 62b2f6d

File tree

6 files changed

+116
-6
lines changed

6 files changed

+116
-6
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.1.10] - 2026-03-03
11+
12+
### Added
13+
14+
- `stygian-browser`: `PageHandle::inject_cookies()` — seed session cookies on a page without a full `SessionSnapshot` round-trip and without a direct `chromiumoxide` dependency in calling code (closes [#11](https://github.com/greysquirr3l/stygian/issues/11))
15+
- `stygian-browser`: `BrowserConfig::builder().user_data_dir(path)` builder method — set a custom Chrome profile directory; when omitted each browser instance now auto-generates a unique temporary directory (`$TMPDIR/stygian-<id>`) so concurrent pools no longer race on `SingletonLock` (closes [#12](https://github.com/greysquirr3l/stygian/issues/12))
16+
1017
## [0.1.9] - 2026-03-02
1118

1219
### Fixed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ members = [
66
]
77

88
[workspace.package]
9-
version = "0.1.9"
9+
version = "0.1.10"
1010
edition = "2024"
1111
rust-version = "1.93.1"
1212
authors = ["Nick Campbell <s0ma@protonmail.com>"]

crates/stygian-browser/src/browser.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,13 @@ impl BrowserInstance {
9393
builder = builder.chrome_executable(path);
9494
}
9595

96-
if let Some(dir) = &config.user_data_dir {
97-
builder = builder.user_data_dir(dir);
98-
}
96+
// Use the caller-supplied profile dir, or generate a unique temp dir
97+
// per instance so concurrent pools never race on SingletonLock.
98+
let data_dir = config
99+
.user_data_dir
100+
.clone()
101+
.unwrap_or_else(|| std::env::temp_dir().join(format!("stygian-{id}")));
102+
builder = builder.user_data_dir(&data_dir);
99103

100104
for arg in &args {
101105
// chromiumoxide's ArgsBuilder prepends "--" when formatting args, so

crates/stygian-browser/src/config.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,27 @@ impl BrowserConfigBuilder {
424424
self
425425
}
426426

427+
/// Set a custom user profile directory.
428+
///
429+
/// When not set, each browser instance automatically uses a unique
430+
/// temporary directory derived from its instance ID, preventing
431+
/// `SingletonLock` races between concurrent pools or instances.
432+
///
433+
/// # Example
434+
///
435+
/// ```
436+
/// use stygian_browser::BrowserConfig;
437+
/// let cfg = BrowserConfig::builder()
438+
/// .user_data_dir("/tmp/my-profile")
439+
/// .build();
440+
/// assert!(cfg.user_data_dir.is_some());
441+
/// ```
442+
#[must_use]
443+
pub fn user_data_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
444+
self.config.user_data_dir = Some(path.into());
445+
self
446+
}
447+
427448
/// Set headless mode.
428449
#[must_use]
429450
pub const fn headless(mut self, headless: bool) -> Self {

crates/stygian-browser/src/page.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,84 @@ impl PageHandle {
685685
.map(|r| r.cookies.clone())
686686
}
687687

688+
/// Inject cookies into the current page.
689+
///
690+
/// Seeds session tokens or other state without needing a full
691+
/// [`SessionSnapshot`][crate::session::SessionSnapshot] and without
692+
/// requiring a direct `chromiumoxide` dependency in calling code.
693+
///
694+
/// Individual cookie failures are logged as warnings and do not abort the
695+
/// remaining cookies.
696+
///
697+
/// # Errors
698+
///
699+
/// Returns [`BrowserError::Timeout`] if a single `Network.setCookie` CDP
700+
/// call exceeds `cdp_timeout`.
701+
///
702+
/// # Example
703+
///
704+
/// ```no_run
705+
/// use stygian_browser::{BrowserPool, BrowserConfig};
706+
/// use stygian_browser::session::SessionCookie;
707+
/// use std::time::Duration;
708+
///
709+
/// # async fn run() -> stygian_browser::error::Result<()> {
710+
/// let pool = BrowserPool::new(BrowserConfig::default()).await?;
711+
/// let handle = pool.acquire().await?;
712+
/// let page = handle.browser().expect("valid browser").new_page().await?;
713+
/// let cookies = vec![SessionCookie {
714+
/// name: "session".to_string(),
715+
/// value: "abc123".to_string(),
716+
/// domain: ".example.com".to_string(),
717+
/// path: "/".to_string(),
718+
/// expires: -1.0,
719+
/// http_only: true,
720+
/// secure: true,
721+
/// same_site: "Lax".to_string(),
722+
/// }];
723+
/// page.inject_cookies(&cookies).await?;
724+
/// # Ok(())
725+
/// # }
726+
/// ```
727+
pub async fn inject_cookies(&self, cookies: &[crate::session::SessionCookie]) -> Result<()> {
728+
use chromiumoxide::cdp::browser_protocol::network::SetCookieParams;
729+
730+
for cookie in cookies {
731+
let params = match SetCookieParams::builder()
732+
.name(cookie.name.clone())
733+
.value(cookie.value.clone())
734+
.domain(cookie.domain.clone())
735+
.path(cookie.path.clone())
736+
.http_only(cookie.http_only)
737+
.secure(cookie.secure)
738+
.build()
739+
{
740+
Ok(p) => p,
741+
Err(e) => {
742+
warn!(cookie = %cookie.name, error = %e, "Failed to build cookie params");
743+
continue;
744+
}
745+
};
746+
747+
match timeout(self.cdp_timeout, self.page.execute(params)).await {
748+
Err(_) => {
749+
warn!(
750+
cookie = %cookie.name,
751+
timeout_ms = self.cdp_timeout.as_millis(),
752+
"Timed out injecting cookie"
753+
);
754+
}
755+
Ok(Err(e)) => {
756+
warn!(cookie = %cookie.name, error = %e, "Failed to inject cookie");
757+
}
758+
Ok(Ok(_)) => {}
759+
}
760+
}
761+
762+
debug!(count = cookies.len(), "Cookies injected");
763+
Ok(())
764+
}
765+
688766
/// Capture a screenshot of the current page as PNG bytes.
689767
///
690768
/// The screenshot is full-page by default (viewport clipped to the rendered

0 commit comments

Comments
 (0)