Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068fc74a9c | ||
|
|
4e5ff20078 | ||
|
|
03e266ee38 | ||
|
|
8c412197ad |
@@ -293,6 +293,7 @@ agent-browser snapshot -i -c -d 5 # Combine options
|
|||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |
|
| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |
|
||||||
|
| `--headers <json>` | Set HTTP headers scoped to the URL's origin |
|
||||||
| `--executable-path <path>` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |
|
| `--executable-path <path>` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |
|
||||||
| `--json` | JSON output (for agents) |
|
| `--json` | JSON output (for agents) |
|
||||||
| `--full, -f` | Full page screenshot |
|
| `--full, -f` | Full page screenshot |
|
||||||
@@ -388,6 +389,41 @@ agent-browser open example.com --headed
|
|||||||
|
|
||||||
This opens a visible browser window instead of running headless.
|
This opens a visible browser window instead of running headless.
|
||||||
|
|
||||||
|
## Authenticated Sessions
|
||||||
|
|
||||||
|
Use `--headers` to set HTTP headers for a specific origin, enabling authentication without login flows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Headers are scoped to api.example.com only
|
||||||
|
agent-browser open api.example.com --headers '{"Authorization": "Bearer <token>"}'
|
||||||
|
|
||||||
|
# Requests to api.example.com include the auth header
|
||||||
|
agent-browser snapshot -i --json
|
||||||
|
agent-browser click @e2
|
||||||
|
|
||||||
|
# Navigate to another domain - headers are NOT sent (safe!)
|
||||||
|
agent-browser open other-site.com
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
- **Skipping login flows** - Authenticate via headers instead of UI
|
||||||
|
- **Switching users** - Start new sessions with different auth tokens
|
||||||
|
- **API testing** - Access protected endpoints directly
|
||||||
|
- **Security** - Headers are scoped to the origin, not leaked to other domains
|
||||||
|
|
||||||
|
To set headers for multiple origins, use `--headers` with each `open` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
agent-browser open api.example.com --headers '{"Authorization": "Bearer token1"}'
|
||||||
|
agent-browser open api.acme.com --headers '{"Authorization": "Bearer token2"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
For global headers (all domains), use `set headers`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
agent-browser set headers '{"X-Custom-Header": "value"}'
|
||||||
|
```
|
||||||
|
|
||||||
## Custom Browser Executable
|
## Custom Browser Executable
|
||||||
|
|
||||||
Use a custom browser executable instead of the bundled Chromium. This is useful for:
|
Use a custom browser executable instead of the bundled Chromium. This is useful for:
|
||||||
|
|||||||
+91
-2
@@ -80,7 +80,14 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
} else {
|
} else {
|
||||||
format!("https://{}", url)
|
format!("https://{}", url)
|
||||||
};
|
};
|
||||||
Ok(json!({ "id": id, "action": "navigate", "url": url }))
|
let mut nav_cmd = json!({ "id": id, "action": "navigate", "url": url });
|
||||||
|
// If --headers flag is set, include headers (scoped to this origin)
|
||||||
|
if let Some(ref headers_json) = flags.headers {
|
||||||
|
if let Ok(headers) = serde_json::from_str::<serde_json::Value>(headers_json) {
|
||||||
|
nav_cmd["headers"] = headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(nav_cmd)
|
||||||
}
|
}
|
||||||
"back" => Ok(json!({ "id": id, "action": "back" })),
|
"back" => Ok(json!({ "id": id, "action": "back" })),
|
||||||
"forward" => Ok(json!({ "id": id, "action": "forward" })),
|
"forward" => Ok(json!({ "id": id, "action": "forward" })),
|
||||||
@@ -766,7 +773,13 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
context: "set headers".to_string(),
|
context: "set headers".to_string(),
|
||||||
usage: "set headers <json>",
|
usage: "set headers <json>",
|
||||||
})?;
|
})?;
|
||||||
Ok(json!({ "id": id, "action": "headers", "headers": headers_json }))
|
// Parse the JSON string into an object
|
||||||
|
let headers: serde_json::Value = serde_json::from_str(headers_json)
|
||||||
|
.map_err(|_| ParseError::MissingArguments {
|
||||||
|
context: "set headers".to_string(),
|
||||||
|
usage: "set headers <json> (must be valid JSON object)",
|
||||||
|
})?;
|
||||||
|
Ok(json!({ "id": id, "action": "headers", "headers": headers }))
|
||||||
}
|
}
|
||||||
Some("credentials") | Some("auth") => {
|
Some("credentials") | Some("auth") => {
|
||||||
let user = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
let user = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||||
@@ -886,6 +899,7 @@ mod tests {
|
|||||||
full: false,
|
full: false,
|
||||||
headed: false,
|
headed: false,
|
||||||
debug: false,
|
debug: false,
|
||||||
|
headers: None,
|
||||||
executable_path: None,
|
executable_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1013,6 +1027,81 @@ mod tests {
|
|||||||
assert_eq!(cmd["url"], "https://example.com");
|
assert_eq!(cmd["url"], "https://example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_navigate_with_headers() {
|
||||||
|
let mut flags = default_flags();
|
||||||
|
flags.headers = Some(r#"{"Authorization": "Bearer token"}"#.to_string());
|
||||||
|
let cmd = parse_command(&args("open api.example.com"), &flags).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "navigate");
|
||||||
|
assert_eq!(cmd["url"], "https://api.example.com");
|
||||||
|
assert_eq!(cmd["headers"]["Authorization"], "Bearer token");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_navigate_with_multiple_headers() {
|
||||||
|
let mut flags = default_flags();
|
||||||
|
flags.headers = Some(r#"{"Authorization": "Bearer token", "X-Custom": "value"}"#.to_string());
|
||||||
|
let cmd = parse_command(&args("open api.example.com"), &flags).unwrap();
|
||||||
|
assert_eq!(cmd["headers"]["Authorization"], "Bearer token");
|
||||||
|
assert_eq!(cmd["headers"]["X-Custom"], "value");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_navigate_without_headers_flag() {
|
||||||
|
let cmd = parse_command(&args("open example.com"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "navigate");
|
||||||
|
// headers should not be present when flag is not set
|
||||||
|
assert!(cmd.get("headers").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_navigate_with_invalid_headers_json() {
|
||||||
|
let mut flags = default_flags();
|
||||||
|
flags.headers = Some("not valid json".to_string());
|
||||||
|
let cmd = parse_command(&args("open api.example.com"), &flags).unwrap();
|
||||||
|
// Invalid JSON should result in no headers field (graceful handling)
|
||||||
|
assert!(cmd.get("headers").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Set Headers Tests ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_headers_parses_json() {
|
||||||
|
let input: Vec<String> = vec![
|
||||||
|
"set".to_string(),
|
||||||
|
"headers".to_string(),
|
||||||
|
r#"{"Authorization":"Bearer token"}"#.to_string(),
|
||||||
|
];
|
||||||
|
let cmd = parse_command(&input, &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "headers");
|
||||||
|
// Headers should be an object, not a string
|
||||||
|
assert!(cmd["headers"].is_object());
|
||||||
|
assert_eq!(cmd["headers"]["Authorization"], "Bearer token");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_headers_with_multiple_values() {
|
||||||
|
let input: Vec<String> = vec![
|
||||||
|
"set".to_string(),
|
||||||
|
"headers".to_string(),
|
||||||
|
r#"{"Authorization": "Bearer token", "X-Custom": "value"}"#.to_string(),
|
||||||
|
];
|
||||||
|
let cmd = parse_command(&input, &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["headers"]["Authorization"], "Bearer token");
|
||||||
|
assert_eq!(cmd["headers"]["X-Custom"], "value");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_headers_invalid_json_error() {
|
||||||
|
let input: Vec<String> = vec![
|
||||||
|
"set".to_string(),
|
||||||
|
"headers".to_string(),
|
||||||
|
"not-valid-json".to_string(),
|
||||||
|
];
|
||||||
|
let result = parse_command(&input, &default_flags());
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_back() {
|
fn test_back() {
|
||||||
let cmd = parse_command(&args("back"), &default_flags()).unwrap();
|
let cmd = parse_command(&args("back"), &default_flags()).unwrap();
|
||||||
|
|||||||
+79
-3
@@ -6,6 +6,7 @@ pub struct Flags {
|
|||||||
pub headed: bool,
|
pub headed: bool,
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
pub session: String,
|
pub session: String,
|
||||||
|
pub headers: Option<String>,
|
||||||
pub executable_path: Option<String>,
|
pub executable_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
headed: false,
|
headed: false,
|
||||||
debug: false,
|
debug: false,
|
||||||
session: env::var("AGENT_BROWSER_SESSION").unwrap_or_else(|_| "default".to_string()),
|
session: env::var("AGENT_BROWSER_SESSION").unwrap_or_else(|_| "default".to_string()),
|
||||||
|
headers: None,
|
||||||
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(),
|
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,6 +34,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"--headers" => {
|
||||||
|
if let Some(h) = args.get(i + 1) {
|
||||||
|
flags.headers = Some(h.clone());
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
"--executable-path" => {
|
"--executable-path" => {
|
||||||
if let Some(s) = args.get(i + 1) {
|
if let Some(s) = args.get(i + 1) {
|
||||||
flags.executable_path = Some(s.clone());
|
flags.executable_path = Some(s.clone());
|
||||||
@@ -51,15 +59,15 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
|||||||
|
|
||||||
// Global flags that should be stripped from command args
|
// Global flags that should be stripped from command args
|
||||||
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"];
|
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"];
|
||||||
// Flags that take a value (skip both the flag and the next arg)
|
// Global flags that take a value (need to skip the next arg too)
|
||||||
const VALUE_FLAGS: &[&str] = &["--session", "--executable-path"];
|
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path"];
|
||||||
|
|
||||||
for arg in args.iter() {
|
for arg in args.iter() {
|
||||||
if skip_next {
|
if skip_next {
|
||||||
skip_next = false;
|
skip_next = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if VALUE_FLAGS.contains(&arg.as_str()) {
|
if GLOBAL_FLAGS_WITH_VALUE.contains(&arg.as_str()) {
|
||||||
skip_next = true;
|
skip_next = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -80,6 +88,74 @@ mod tests {
|
|||||||
s.split_whitespace().map(String::from).collect()
|
s.split_whitespace().map(String::from).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_headers_flag() {
|
||||||
|
let flags = parse_flags(&args(r#"open example.com --headers {"Auth":"token"}"#));
|
||||||
|
assert_eq!(flags.headers, Some(r#"{"Auth":"token"}"#.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_headers_flag_with_spaces() {
|
||||||
|
// Headers JSON is passed as a single quoted argument in shell
|
||||||
|
let input: Vec<String> = vec![
|
||||||
|
"open".to_string(),
|
||||||
|
"example.com".to_string(),
|
||||||
|
"--headers".to_string(),
|
||||||
|
r#"{"Authorization": "Bearer token"}"#.to_string(),
|
||||||
|
];
|
||||||
|
let flags = parse_flags(&input);
|
||||||
|
assert_eq!(flags.headers, Some(r#"{"Authorization": "Bearer token"}"#.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_no_headers_flag() {
|
||||||
|
let flags = parse_flags(&args("open example.com"));
|
||||||
|
assert!(flags.headers.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clean_args_removes_headers() {
|
||||||
|
let input: Vec<String> = vec![
|
||||||
|
"open".to_string(),
|
||||||
|
"example.com".to_string(),
|
||||||
|
"--headers".to_string(),
|
||||||
|
r#"{"Auth":"token"}"#.to_string(),
|
||||||
|
];
|
||||||
|
let clean = clean_args(&input);
|
||||||
|
assert_eq!(clean, vec!["open", "example.com"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clean_args_removes_headers_at_start() {
|
||||||
|
let input: Vec<String> = vec![
|
||||||
|
"--headers".to_string(),
|
||||||
|
r#"{"Auth":"token"}"#.to_string(),
|
||||||
|
"open".to_string(),
|
||||||
|
"example.com".to_string(),
|
||||||
|
];
|
||||||
|
let clean = clean_args(&input);
|
||||||
|
assert_eq!(clean, vec!["open", "example.com"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_headers_with_other_flags() {
|
||||||
|
let input: Vec<String> = vec![
|
||||||
|
"open".to_string(),
|
||||||
|
"example.com".to_string(),
|
||||||
|
"--headers".to_string(),
|
||||||
|
r#"{"Auth":"token"}"#.to_string(),
|
||||||
|
"--json".to_string(),
|
||||||
|
"--headed".to_string(),
|
||||||
|
];
|
||||||
|
let flags = parse_flags(&input);
|
||||||
|
assert_eq!(flags.headers, Some(r#"{"Auth":"token"}"#.to_string()));
|
||||||
|
assert!(flags.json);
|
||||||
|
assert!(flags.headed);
|
||||||
|
|
||||||
|
let clean = clean_args(&input);
|
||||||
|
assert_eq!(clean, vec!["open", "example.com"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_executable_path_flag() {
|
fn test_parse_executable_path_flag() {
|
||||||
let flags = parse_flags(&args("--executable-path /path/to/chromium open example.com"));
|
let flags = parse_flags(&args("--executable-path /path/to/chromium open example.com"));
|
||||||
|
|||||||
@@ -162,12 +162,15 @@ Aliases: goto, navigate
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--headers <json> Set HTTP headers (scoped to this origin)
|
||||||
--headed Show browser window
|
--headed Show browser window
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser open example.com
|
agent-browser open example.com
|
||||||
agent-browser open https://github.com
|
agent-browser open https://github.com
|
||||||
agent-browser open localhost:3000
|
agent-browser open localhost:3000
|
||||||
|
agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}'
|
||||||
|
# ^ Headers only sent to api.example.com, not other domains
|
||||||
"##,
|
"##,
|
||||||
"back" => r##"
|
"back" => r##"
|
||||||
agent-browser back - Navigate back in history
|
agent-browser back - Navigate back in history
|
||||||
@@ -1186,6 +1189,7 @@ Snapshot Options:
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
|
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
|
||||||
|
--headers <json> HTTP headers scoped to URL's origin (for auth)
|
||||||
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
|
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
|
||||||
--json JSON output
|
--json JSON output
|
||||||
--full, -f Full page screenshot
|
--full, -f Full page screenshot
|
||||||
|
|||||||
@@ -411,6 +411,12 @@ async function handleNavigate(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response<NavigateData>> {
|
): Promise<Response<NavigateData>> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
|
// If headers are provided, set up scoped headers for this origin
|
||||||
|
if (command.headers && Object.keys(command.headers).length > 0) {
|
||||||
|
await browser.setScopedHeaders(command.url, command.headers);
|
||||||
|
}
|
||||||
|
|
||||||
await page.goto(command.url, {
|
await page.goto(command.url, {
|
||||||
waitUntil: command.waitUntil ?? 'load',
|
waitUntil: command.waitUntil ?? 'load',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -304,4 +304,59 @@ describe('BrowserManager', () => {
|
|||||||
expect(h1).toBe('Example Domain');
|
expect(h1).toBe('Example Domain');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('scoped headers', () => {
|
||||||
|
it('should register route for scoped headers', async () => {
|
||||||
|
// Test that setScopedHeaders doesn't throw and completes successfully
|
||||||
|
await browser.clearScopedHeaders();
|
||||||
|
await expect(
|
||||||
|
browser.setScopedHeaders('https://example.com', { 'X-Test': 'value' })
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
await browser.clearScopedHeaders();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle full URL origin', async () => {
|
||||||
|
await browser.clearScopedHeaders();
|
||||||
|
await expect(
|
||||||
|
browser.setScopedHeaders('https://api.example.com/path', { Authorization: 'Bearer token' })
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
await browser.clearScopedHeaders();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle hostname-only origin', async () => {
|
||||||
|
await browser.clearScopedHeaders();
|
||||||
|
await expect(
|
||||||
|
browser.setScopedHeaders('example.com', { 'X-Custom': 'value' })
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
await browser.clearScopedHeaders();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear scoped headers for specific origin', async () => {
|
||||||
|
await browser.clearScopedHeaders();
|
||||||
|
await browser.setScopedHeaders('https://example.com', { 'X-Test': 'value' });
|
||||||
|
await expect(browser.clearScopedHeaders('https://example.com')).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear all scoped headers', async () => {
|
||||||
|
await browser.setScopedHeaders('https://example.com', { 'X-Test-1': 'value1' });
|
||||||
|
await browser.setScopedHeaders('https://example.org', { 'X-Test-2': 'value2' });
|
||||||
|
await expect(browser.clearScopedHeaders()).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace headers when called twice for same origin', async () => {
|
||||||
|
await browser.clearScopedHeaders();
|
||||||
|
await browser.setScopedHeaders('https://example.com', { 'X-First': 'first' });
|
||||||
|
// Second call should replace, not add
|
||||||
|
await expect(
|
||||||
|
browser.setScopedHeaders('https://example.com', { 'X-Second': 'second' })
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
await browser.clearScopedHeaders();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle clearing non-existent origin gracefully', async () => {
|
||||||
|
await browser.clearScopedHeaders();
|
||||||
|
// Should not throw when clearing headers that were never set
|
||||||
|
await expect(browser.clearScopedHeaders('https://never-set.com')).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+74
-2
@@ -51,6 +51,7 @@ export class BrowserManager {
|
|||||||
private isRecordingHar: boolean = false;
|
private isRecordingHar: boolean = false;
|
||||||
private refMap: RefMap = {};
|
private refMap: RefMap = {};
|
||||||
private lastSnapshot: string = '';
|
private lastSnapshot: string = '';
|
||||||
|
private scopedHeaderRoutes: Map<string, (route: Route) => Promise<void>> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if browser is launched
|
* Check if browser is launched
|
||||||
@@ -439,7 +440,7 @@ export class BrowserManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set extra HTTP headers
|
* Set extra HTTP headers (global - all requests)
|
||||||
*/
|
*/
|
||||||
async setExtraHeaders(headers: Record<string, string>): Promise<void> {
|
async setExtraHeaders(headers: Record<string, string>): Promise<void> {
|
||||||
const context = this.contexts[0];
|
const context = this.contexts[0];
|
||||||
@@ -448,6 +449,76 @@ export class BrowserManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set scoped HTTP headers (only for requests matching the origin)
|
||||||
|
* Uses route interception to add headers only to matching requests
|
||||||
|
*/
|
||||||
|
async setScopedHeaders(origin: string, headers: Record<string, string>): Promise<void> {
|
||||||
|
const page = this.getPage();
|
||||||
|
|
||||||
|
// Build URL pattern from origin (e.g., "api.example.com" -> "**://api.example.com/**")
|
||||||
|
// Handle both full URLs and just hostnames
|
||||||
|
let urlPattern: string;
|
||||||
|
try {
|
||||||
|
const url = new URL(origin.startsWith('http') ? origin : `https://${origin}`);
|
||||||
|
// Match any protocol, the host, and any path
|
||||||
|
urlPattern = `**://${url.host}/**`;
|
||||||
|
} catch {
|
||||||
|
// If parsing fails, treat as hostname pattern
|
||||||
|
urlPattern = `**://${origin}/**`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove existing route for this origin if any
|
||||||
|
const existingHandler = this.scopedHeaderRoutes.get(urlPattern);
|
||||||
|
if (existingHandler) {
|
||||||
|
await page.unroute(urlPattern, existingHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create handler that adds headers to matching requests
|
||||||
|
const handler = async (route: Route) => {
|
||||||
|
const requestHeaders = route.request().headers();
|
||||||
|
await route.continue({
|
||||||
|
headers: {
|
||||||
|
...requestHeaders,
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store and register the route
|
||||||
|
this.scopedHeaderRoutes.set(urlPattern, handler);
|
||||||
|
await page.route(urlPattern, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear scoped headers for an origin (or all if no origin specified)
|
||||||
|
*/
|
||||||
|
async clearScopedHeaders(origin?: string): Promise<void> {
|
||||||
|
const page = this.getPage();
|
||||||
|
|
||||||
|
if (origin) {
|
||||||
|
let urlPattern: string;
|
||||||
|
try {
|
||||||
|
const url = new URL(origin.startsWith('http') ? origin : `https://${origin}`);
|
||||||
|
urlPattern = `**://${url.host}/**`;
|
||||||
|
} catch {
|
||||||
|
urlPattern = `**://${origin}/**`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = this.scopedHeaderRoutes.get(urlPattern);
|
||||||
|
if (handler) {
|
||||||
|
await page.unroute(urlPattern, handler);
|
||||||
|
this.scopedHeaderRoutes.delete(urlPattern);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Clear all scoped header routes
|
||||||
|
for (const [pattern, handler] of this.scopedHeaderRoutes) {
|
||||||
|
await page.unroute(pattern, handler);
|
||||||
|
}
|
||||||
|
this.scopedHeaderRoutes.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start tracing
|
* Start tracing
|
||||||
*/
|
*/
|
||||||
@@ -523,9 +594,10 @@ export class BrowserManager {
|
|||||||
executablePath: options.executablePath,
|
executablePath: options.executablePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create context with viewport
|
// Create context with viewport and optional headers
|
||||||
const context = await this.browser.newContext({
|
const context = await this.browser.newContext({
|
||||||
viewport: options.viewport ?? { width: 1280, height: 720 },
|
viewport: options.viewport ?? { width: 1280, height: 720 },
|
||||||
|
extraHTTPHeaders: options.headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set default timeout to 10 seconds (Playwright default is 30s)
|
// Set default timeout to 10 seconds (Playwright default is 30s)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface LaunchCommand extends BaseCommand {
|
|||||||
headless?: boolean;
|
headless?: boolean;
|
||||||
viewport?: { width: number; height: number };
|
viewport?: { width: number; height: number };
|
||||||
browser?: 'chromium' | 'firefox' | 'webkit';
|
browser?: 'chromium' | 'firefox' | 'webkit';
|
||||||
|
headers?: Record<string, string>;
|
||||||
executablePath?: string;
|
executablePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ export interface NavigateCommand extends BaseCommand {
|
|||||||
action: 'navigate';
|
action: 'navigate';
|
||||||
url: string;
|
url: string;
|
||||||
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
|
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
|
||||||
|
headers?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClickCommand extends BaseCommand {
|
export interface ClickCommand extends BaseCommand {
|
||||||
|
|||||||
Reference in New Issue
Block a user