1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::{BTreeMap, BTreeSet};
6
7use crate::{
8 AttemptId, CommandId, FormControlTarget, PageId, SessionId, WorkflowId, MAX_FORM_REFERENCES,
9 MAX_FORM_VALUE_BYTES,
10};
11
12pub const MAX_INTENT_PURPOSE_BYTES: usize = 256;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18#[serde(rename_all = "camelCase")]
19pub struct CommandEnvelope {
20 pub schema_version: u16,
21 pub command_id: CommandId,
22 pub workflow_id: WorkflowId,
23 pub attempt_id: AttemptId,
24 pub session_id: SessionId,
25 pub page_id: Option<PageId>,
26 pub deadline: DateTime<Utc>,
27 pub command: RuntimeCommand,
28}
29
30impl CommandEnvelope {
31 pub const SCHEMA_VERSION: u16 = 2;
32
33 pub fn journal_safe(&self) -> Self {
36 let mut safe = self.clone();
37 safe.command.sanitize_urls();
38 safe
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[serde(tag = "kind", content = "input", rename_all = "camelCase")]
46pub enum RuntimeCommand {
47 Primitive(PrimitiveCommand),
48 Intent(IntentCommand),
49}
50
51impl RuntimeCommand {
52 pub fn class(&self) -> CommandClass {
53 match self {
54 Self::Primitive(command) => command.class(),
55 Self::Intent(command) => command.class(),
56 }
57 }
58
59 pub fn sanitize_urls(&mut self) {
60 match self {
61 Self::Primitive(command) => command.sanitize_urls(),
62 Self::Intent(_) => {}
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
69#[serde(tag = "kind", content = "input", rename_all = "camelCase")]
70pub enum IntentCommand {
71 Locate(LocateIntent),
72 Fill(FillIntent),
73 CompleteForm(CompleteFormIntent),
74 SubmitAndVerify(SubmitAndVerifyIntent),
75 WaitForState(WaitForStateIntent),
76 Follow(FollowIntent),
77 DismissObstruction(DismissObstructionIntent),
78 Extract(ExtractIntent),
79 SolveChallenge(crate::challenges::SolveChallengeIntent),
84}
85
86impl IntentCommand {
87 pub fn class(&self) -> CommandClass {
88 match self {
89 Self::Locate(_) | Self::WaitForState(_) | Self::Extract(_) => CommandClass::Replayable,
90 Self::Fill(_) | Self::CompleteForm(_) | Self::DismissObstruction(_) => {
91 CommandClass::Reconciliable
92 }
93 Self::SolveChallenge(_) => CommandClass::Reconciliable,
94 Self::SubmitAndVerify(_) => CommandClass::Boundary,
95 Self::Follow(intent) => {
96 if intent.boundary {
97 CommandClass::Boundary
98 } else {
99 CommandClass::Reconciliable
100 }
101 }
102 }
103 }
104}
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
109#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
110#[serde(rename_all = "camelCase", default)]
111pub struct IntentHints {
112 pub role: Option<String>,
113 pub near_text: Option<TextMatch>,
114 pub accessible_name: Option<String>,
121 pub ordinal: Option<usize>,
122 pub frame_path: Vec<TargetSpec>,
123 pub shadow_path: Vec<TargetSpec>,
124 pub allow_best_match: bool,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
129#[serde(rename_all = "camelCase")]
130pub struct LocateIntent {
131 pub purpose: String,
132 #[serde(default)]
133 pub hints: IntentHints,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
138#[serde(rename_all = "camelCase")]
139pub struct FillIntent {
140 pub purpose: String,
141 #[serde(default)]
142 pub hints: IntentHints,
143 pub value: ControlAction,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
148#[serde(rename_all = "camelCase")]
149pub struct CompleteFormField {
150 pub name: String,
151 pub purpose: String,
152 #[serde(default)]
153 pub hints: IntentHints,
154 pub value: ControlAction,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
158#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
159#[serde(rename_all = "camelCase")]
160pub struct CompleteFormIntent {
161 pub purpose: String,
162 pub fields: Vec<CompleteFormField>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
167#[serde(rename_all = "camelCase")]
168pub struct SubmitAndVerifyIntent {
169 pub purpose: String,
170 #[serde(default)]
171 pub hints: IntentHints,
172 pub expected_state: WaitForCommand,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
176#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
177#[serde(rename_all = "camelCase")]
178pub struct WaitForStateIntent {
179 pub condition: WaitCondition,
180 pub timeout_ms: u64,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
188#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
189#[serde(rename_all = "camelCase")]
190pub struct FollowIntent {
191 pub purpose: String,
192 #[serde(default)]
193 pub hints: IntentHints,
194 pub expected_destination: WaitForCommand,
195 #[serde(default)]
196 pub boundary: bool,
197}
198
199pub const DEFAULT_DISMISS_OBSTRUCTION_TIMEOUT_MS: u64 = 5_000;
200
201fn default_dismiss_timeout_ms() -> u64 {
202 DEFAULT_DISMISS_OBSTRUCTION_TIMEOUT_MS
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
210#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
211#[serde(rename_all = "camelCase")]
212pub struct DismissObstructionIntent {
213 pub purpose: String,
214 #[serde(default)]
215 pub hints: IntentHints,
216 #[serde(default = "default_dismiss_timeout_ms")]
217 pub timeout_ms: u64,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
223#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
224#[serde(tag = "kind", rename_all = "camelCase")]
225pub enum ExtractValueKind {
226 Text,
227 Attribute { attribute: String },
228 Href,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
234#[serde(rename_all = "camelCase")]
235pub struct ExtractField {
236 pub name: String,
237 pub purpose: String,
238 #[serde(default)]
239 pub hints: IntentHints,
240 pub value: ExtractValueKind,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249#[serde(rename_all = "camelCase")]
250pub struct ExtractIntent {
251 pub purpose: String,
252 pub fields: Vec<ExtractField>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
256#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
257#[serde(tag = "kind", content = "input", rename_all = "camelCase")]
258pub enum PrimitiveCommand {
259 Navigate(NavigateCommand),
260 DownloadUrl(DownloadUrlCommand),
261 Inspect(InspectCommand),
262 Click(ClickCommand),
263 TypeText(TypeTextCommand),
264 UploadFiles(UploadFilesCommand),
265 OpenPage(OpenPageCommand),
266 ListPages(ListPagesCommand),
267 ClosePage(ClosePageCommand),
268 ActivatePage(ActivatePageCommand),
269 AccessibilitySnapshot(AccessibilitySnapshotCommand),
270 ExtractStructured(ExtractStructuredCommand),
271 GetCookies(GetCookiesCommand),
272 PrintToPdf(PrintToPdfCommand),
273 HandleDialog(HandleDialogCommand),
274 Emulate(EmulateCommand),
275 NetworkLog(NetworkLogCommand),
276 SetCookies(SetCookiesCommand),
277 DeleteCookies(DeleteCookiesCommand),
278 ClickAndWaitForPopup(ClickAndWaitForPopupCommand),
279 ClickAndWaitForDownload(ClickAndWaitForDownloadCommand),
280 WaitFor(WaitForCommand),
281 CaptureScreenshot(CaptureScreenshotCommand),
282 ControlAction(ControlActionCommand),
283 SetFocusEmulation(SetFocusEmulationCommand),
284 SetEmulatedMedia(SetEmulatedMediaCommand),
285 EvaluateJavaScript(EvaluateJavaScriptCommand),
286}
287
288impl PrimitiveCommand {
289 fn sanitize_urls(&mut self) {
290 fn sanitize(value: &mut String) {
291 let Ok(mut url) = url::Url::parse(value) else {
292 *value = "[redacted-invalid-url]".into();
293 return;
294 };
295 let _ = url.set_username("");
296 let _ = url.set_password(None);
297 url.set_query(None);
298 url.set_fragment(None);
299 *value = url.to_string();
300 }
301 match self {
302 Self::Navigate(command) => sanitize(&mut command.url),
303 Self::DownloadUrl(command) => {
304 sanitize(&mut command.url);
305 if let Some(save_as) = &mut command.save_as {
306 *save_as = "[redacted-download-path]".into();
307 }
308 }
309 Self::UploadFiles(command) => {
310 for (index, path) in command.paths.iter_mut().enumerate() {
311 *path = format!("upload://input/{index}");
312 }
313 }
314 Self::OpenPage(command) => {
315 if let Some(url) = &mut command.url {
316 sanitize(url);
317 }
318 }
319 Self::Click(command) => {
320 if let Some(url) = &mut command.expected_url {
321 sanitize(url);
322 }
323 }
324 Self::ControlAction(command) => {
325 if let ControlAction::SetFiles { paths } = &mut command.action {
326 for (index, path) in paths.iter_mut().enumerate() {
327 *path = format!("upload://input/{index}");
328 }
329 }
330 }
331 _ => {}
332 }
333 }
334
335 pub fn class(&self) -> CommandClass {
336 match self {
337 Self::Navigate(_)
338 | Self::Inspect(_)
339 | Self::OpenPage(_)
340 | Self::ListPages(_)
341 | Self::WaitFor(_)
342 | Self::ActivatePage(_)
343 | Self::Emulate(_)
344 | Self::NetworkLog(_)
345 | Self::AccessibilitySnapshot(_)
346 | Self::ExtractStructured(_)
347 | Self::GetCookies(_)
348 | Self::PrintToPdf(_)
349 | Self::CaptureScreenshot(_) => CommandClass::Replayable,
350 Self::SetCookies(_) | Self::DeleteCookies(_) | Self::HandleDialog(_) => {
351 CommandClass::Reconciliable
352 }
353 Self::DownloadUrl(_)
354 | Self::TypeText(_)
355 | Self::UploadFiles(_)
356 | Self::ClosePage(_)
357 | Self::EvaluateJavaScript(_) => CommandClass::Reconciliable,
358 Self::ControlAction(_) => CommandClass::Reconciliable,
359 Self::ClickAndWaitForPopup(_) | Self::ClickAndWaitForDownload(_) => {
360 CommandClass::Boundary
361 }
362 Self::Click(command) if command.boundary => CommandClass::Boundary,
363 Self::Click(_) => CommandClass::Reconciliable,
364 Self::SetFocusEmulation(_) => CommandClass::Reconciliable,
365 Self::SetEmulatedMedia(_) => CommandClass::Reconciliable,
366 }
367 }
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
371#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
372#[serde(rename_all = "camelCase", deny_unknown_fields)]
373pub struct ControlActionCommand {
374 pub target: FormControlTarget,
375 pub action: ControlAction,
376}
377
378fn default_clear_first() -> bool {
379 true
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
384#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
385pub enum ControlAction {
386 SetText {
387 value: String,
388 #[serde(default = "default_clear_first", rename = "clearFirst")]
389 clear_first: bool,
390 },
391 SetChecked {
392 checked: bool,
393 },
394 SelectOne {
395 value: String,
396 },
397 SelectMany {
398 values: Vec<String>,
399 },
400 SetFiles {
401 paths: Vec<String>,
402 },
403 Clear,
404 Activate,
405}
406
407impl ControlAction {
408 pub fn validate(&self) -> Result<(), String> {
409 fn bounded(value: &str, field: &str) -> Result<(), String> {
410 if value.len() > MAX_FORM_VALUE_BYTES {
411 return Err(format!("{field} exceeds {MAX_FORM_VALUE_BYTES} bytes"));
412 }
413 Ok(())
414 }
415
416 match self {
417 Self::SetText { value, .. } | Self::SelectOne { value } => bounded(value, "value"),
418 Self::SelectMany { values } => {
419 if values.is_empty() || values.len() > MAX_FORM_REFERENCES {
420 return Err(format!(
421 "values must contain between 1 and {MAX_FORM_REFERENCES} items"
422 ));
423 }
424 let mut unique = BTreeSet::new();
425 for value in values {
426 bounded(value, "selection value")?;
427 if !unique.insert(value) {
428 return Err("selection values must be unique".into());
429 }
430 }
431 Ok(())
432 }
433 Self::SetFiles { paths } => {
434 if paths.is_empty() || paths.len() > MAX_FORM_REFERENCES {
435 return Err(format!(
436 "paths must contain between 1 and {MAX_FORM_REFERENCES} items"
437 ));
438 }
439 for path in paths {
440 bounded(path, "file path")?;
441 }
442 Ok(())
443 }
444 Self::SetChecked { .. } | Self::Clear | Self::Activate => Ok(()),
445 }
446 }
447
448 pub fn operation(&self) -> crate::FormControlOperation {
449 match self {
450 Self::SetText { .. } => crate::FormControlOperation::SetText,
451 Self::SetChecked { .. } => crate::FormControlOperation::SetChecked,
452 Self::SelectOne { .. } => crate::FormControlOperation::SelectOne,
453 Self::SelectMany { .. } => crate::FormControlOperation::SelectMany,
454 Self::SetFiles { .. } => crate::FormControlOperation::SetFiles,
455 Self::Clear => crate::FormControlOperation::Clear,
456 Self::Activate => crate::FormControlOperation::Activate,
457 }
458 }
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
462#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
463#[serde(rename_all = "camelCase")]
464pub struct DownloadUrlCommand {
465 pub url: String,
466 pub expected_content_type: Option<String>,
467 pub max_bytes: u64,
468 pub save_as: Option<String>,
469}
470
471#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
472#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
473#[serde(rename_all = "camelCase")]
474pub enum CommandClass {
475 Replayable,
476 Reconciliable,
477 Boundary,
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize)]
481#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
482#[serde(rename_all = "camelCase")]
483pub struct NavigateCommand {
484 pub url: String,
485 pub wait_until: WaitUntil,
486 pub timeout_ms: u64,
487}
488
489#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
490#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
491#[serde(rename_all = "camelCase")]
492pub enum WaitUntil {
493 Commit,
494 DomContentLoaded,
495 Interactive,
496 NetworkIdle,
497}
498
499#[derive(Debug, Clone, Default, Serialize, Deserialize)]
500#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
501#[serde(rename_all = "camelCase")]
502pub struct InspectCommand {
503 pub selector: Option<String>,
504 pub target: Option<TargetSpec>,
505 pub include_html: bool,
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
509#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
510#[serde(rename_all = "camelCase")]
511pub enum ClickModifier {
512 Shift,
513 Ctrl,
514 Alt,
515 Meta,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
519#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
520#[serde(rename_all = "camelCase")]
521pub struct ClickCommand {
522 pub selector: String,
523 pub target: Option<TargetSpec>,
524 pub boundary: bool,
525 pub expected_url: Option<String>,
526 #[serde(default, skip_serializing_if = "Vec::is_empty")]
527 pub modifiers: Vec<ClickModifier>,
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
531#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
532#[serde(rename_all = "camelCase")]
533pub struct TypeTextCommand {
534 pub selector: String,
535 pub target: Option<TargetSpec>,
536 pub value: String,
537 pub clear_first: bool,
538 #[serde(default)]
541 pub expected_url: Option<String>,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
545#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
546#[serde(rename_all = "camelCase")]
547pub struct UploadFilesCommand {
548 pub selector: String,
549 pub target: Option<TargetSpec>,
550 pub paths: Vec<String>,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
554#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
555#[serde(rename_all = "camelCase")]
556pub struct OpenPageCommand {
557 pub url: Option<String>,
558}
559
560#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
561#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
562pub struct ListPagesCommand;
563
564#[derive(Debug, Clone, Serialize, Deserialize)]
565#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
566#[serde(rename_all = "camelCase")]
567pub struct ClosePageCommand {
568 pub page_id: PageId,
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize)]
572#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
573#[serde(rename_all = "camelCase")]
574pub struct ActivatePageCommand {
575 pub page_id: PageId,
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize)]
579#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
580#[serde(rename_all = "camelCase")]
581pub struct AccessibilitySnapshotCommand {
582 pub max_nodes: Option<u32>,
583 #[serde(default)]
587 pub target: Option<TargetSpec>,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
591#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
592#[serde(rename_all = "camelCase")]
593pub struct ExtractStructuredCommand {
594 pub schema: serde_json::Value,
595 pub purpose: Option<String>,
596}
597
598#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
600#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
601#[serde(rename_all = "camelCase", deny_unknown_fields)]
602pub struct CookieRecord {
603 pub name: String,
604 pub value: String,
605 pub domain: String,
606 pub path: String,
607 pub secure: bool,
608 pub http_only: bool,
609 #[serde(default, skip_serializing_if = "Option::is_none")]
610 pub same_site: Option<String>,
611 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub expires_unix: Option<f64>,
613}
614
615#[derive(Debug, Clone, Serialize, Deserialize)]
618#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
619#[serde(rename_all = "camelCase", deny_unknown_fields)]
620pub struct SetCookieParam {
621 pub name: String,
622 pub value: String,
623 pub url: String,
624 #[serde(default)]
625 pub path: Option<String>,
626 #[serde(default)]
627 pub secure: bool,
628 #[serde(default)]
629 pub http_only: bool,
630 #[serde(default)]
631 pub same_site: Option<String>,
632 #[serde(default)]
633 pub expires_unix: Option<f64>,
634}
635
636#[derive(Debug, Clone, Default, Serialize, Deserialize)]
637#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
638#[serde(rename_all = "camelCase", deny_unknown_fields)]
639pub struct NetworkLogCommand {
640 #[serde(default = "default_true")]
642 pub clear: bool,
643}
644
645fn default_true() -> bool {
646 true
647}
648
649#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
650#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
651#[serde(rename_all = "camelCase", deny_unknown_fields)]
652pub struct ViewportSize {
653 pub width: u32,
654 pub height: u32,
655}
656
657#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
658#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
659#[serde(rename_all = "camelCase", deny_unknown_fields)]
660pub struct GeolocationCoordinates {
661 pub latitude: f64,
662 pub longitude: f64,
663 #[serde(default)]
664 pub accuracy: Option<f64>,
665}
666
667#[derive(Debug, Clone, Default, Serialize, Deserialize)]
668#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
669#[serde(rename_all = "camelCase", deny_unknown_fields)]
670pub struct EmulateCommand {
671 #[serde(default)]
672 pub viewport: Option<ViewportSize>,
673 #[serde(default)]
674 pub geolocation: Option<GeolocationCoordinates>,
675 #[serde(default)]
677 pub mobile: Option<bool>,
678}
679
680#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
681#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
682#[serde(rename_all = "camelCase")]
683pub enum DialogAction {
684 Accept,
685 Dismiss,
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize)]
689#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
690#[serde(rename_all = "camelCase", deny_unknown_fields)]
691pub struct HandleDialogCommand {
692 pub action: DialogAction,
693 #[serde(default)]
694 pub timeout_ms: Option<u64>,
695}
696
697#[derive(Debug, Clone, Serialize, Deserialize)]
698#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
699#[serde(rename_all = "camelCase", deny_unknown_fields)]
700pub struct PrintToPdfCommand {
701 #[serde(default)]
702 pub landscape: bool,
703 #[serde(default = "default_print_background")]
704 pub print_background: bool,
705 #[serde(default)]
706 pub scale: Option<f64>,
707 #[serde(default)]
708 pub page_ranges: Option<String>,
709}
710
711fn default_print_background() -> bool {
712 true
713}
714
715#[derive(Debug, Clone, Default, Serialize, Deserialize)]
716#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
717#[serde(rename_all = "camelCase", deny_unknown_fields)]
718pub struct GetCookiesCommand {
719 #[serde(default)]
721 pub urls: Vec<String>,
722}
723
724#[derive(Debug, Clone, Serialize, Deserialize)]
725#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
726#[serde(rename_all = "camelCase", deny_unknown_fields)]
727pub struct SetCookiesCommand {
728 pub cookies: Vec<SetCookieParam>,
729}
730
731#[derive(Debug, Clone, Default, Serialize, Deserialize)]
732#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
733#[serde(rename_all = "camelCase", deny_unknown_fields)]
734pub struct DeleteCookiesCommand {
735 #[serde(default)]
737 pub urls: Vec<String>,
738 #[serde(default)]
740 pub names: Vec<String>,
741}
742
743#[derive(Debug, Clone, Serialize, Deserialize)]
744#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
745#[serde(rename_all = "camelCase")]
746pub struct ClickAndWaitForPopupCommand {
747 pub selector: String,
748 pub target: Option<TargetSpec>,
749 pub timeout_ms: u64,
750}
751
752#[derive(Debug, Clone, Serialize, Deserialize)]
753#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
754#[serde(rename_all = "camelCase")]
755pub struct ClickAndWaitForDownloadCommand {
756 pub selector: String,
757 pub target: Option<TargetSpec>,
758 pub timeout_ms: u64,
759}
760
761#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
762#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
763#[serde(rename_all = "camelCase")]
764pub struct TargetSpec {
765 pub css: Option<String>,
766 pub test_id: Option<String>,
767 pub role: Option<String>,
768 pub accessible_name: Option<String>,
769 pub label: Option<String>,
770 pub text: Option<TextMatch>,
771 #[serde(default)]
772 pub attributes: BTreeMap<String, String>,
773 #[serde(default)]
774 pub frame_path: Vec<Box<TargetSpec>>,
775 #[serde(default)]
776 pub shadow_path: Vec<Box<TargetSpec>>,
777 pub ordinal: Option<usize>,
778 #[serde(default)]
779 pub allow_best_match: bool,
780}
781
782#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
783#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
784#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
785pub enum TextMatch {
786 Exact(String),
787 Contains(String),
788 Regex(String),
789}
790
791#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
792#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
793#[serde(rename_all = "camelCase")]
794pub enum ElementState {
795 Attached,
796 Detached,
797 Visible,
798 Hidden,
799 Enabled,
800 Disabled,
801}
802
803#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
804#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
805#[serde(rename_all = "PascalCase")]
806pub enum NetworkResourceType {
807 Document,
808 Stylesheet,
809 Image,
810 Media,
811 Font,
812 Script,
813 TextTrack,
814 #[serde(rename = "XHR", alias = "Xhr")]
815 Xhr,
816 Fetch,
817 Prefetch,
818 EventSource,
819 WebSocket,
820 Manifest,
821 SignedExchange,
822 Ping,
823 #[serde(rename = "CSPViolationReport")]
824 CspViolationReport,
825 Preflight,
826 #[serde(rename = "FedCM")]
827 FedCm,
828 Other,
829}
830
831impl NetworkResourceType {
832 pub fn as_str(&self) -> &'static str {
833 match self {
834 Self::Document => "Document",
835 Self::Stylesheet => "Stylesheet",
836 Self::Image => "Image",
837 Self::Media => "Media",
838 Self::Font => "Font",
839 Self::Script => "Script",
840 Self::TextTrack => "TextTrack",
841 Self::Xhr => "XHR",
842 Self::Fetch => "Fetch",
843 Self::Prefetch => "Prefetch",
844 Self::EventSource => "EventSource",
845 Self::WebSocket => "WebSocket",
846 Self::Manifest => "Manifest",
847 Self::SignedExchange => "SignedExchange",
848 Self::Ping => "Ping",
849 Self::CspViolationReport => "CSPViolationReport",
850 Self::Preflight => "Preflight",
851 Self::FedCm => "FedCM",
852 Self::Other => "Other",
853 }
854 }
855}
856
857impl std::str::FromStr for NetworkResourceType {
858 type Err = String;
859
860 fn from_str(s: &str) -> Result<Self, Self::Err> {
861 match s {
862 "Document" | "document" => Ok(Self::Document),
863 "Stylesheet" | "stylesheet" => Ok(Self::Stylesheet),
864 "Image" | "image" => Ok(Self::Image),
865 "Media" | "media" => Ok(Self::Media),
866 "Font" | "font" => Ok(Self::Font),
867 "Script" | "script" => Ok(Self::Script),
868 "TextTrack" | "textTrack" | "texttrack" => Ok(Self::TextTrack),
869 "XHR" | "Xhr" | "xhr" => Ok(Self::Xhr),
870 "Fetch" | "fetch" => Ok(Self::Fetch),
871 "Prefetch" | "prefetch" => Ok(Self::Prefetch),
872 "EventSource" | "eventSource" | "eventsource" => Ok(Self::EventSource),
873 "WebSocket" | "webSocket" | "websocket" => Ok(Self::WebSocket),
874 "Manifest" | "manifest" => Ok(Self::Manifest),
875 "SignedExchange" | "signedExchange" => Ok(Self::SignedExchange),
876 "Ping" | "ping" => Ok(Self::Ping),
877 "CSPViolationReport" | "cspViolationReport" => Ok(Self::CspViolationReport),
878 "Preflight" | "preflight" => Ok(Self::Preflight),
879 "FedCM" | "FedCm" | "fedCM" => Ok(Self::FedCm),
880 "Other" | "other" => Ok(Self::Other),
881 other => Err(format!("unknown network resource type: {other}")),
882 }
883 }
884}
885
886#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
887#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
888#[serde(tag = "kind", rename_all = "camelCase")]
889pub enum WaitCondition {
890 Element {
891 target: Box<TargetSpec>,
892 state: ElementState,
893 },
894 Text {
895 target: Box<TargetSpec>,
896 matcher: TextMatch,
897 },
898 Value {
899 target: Box<TargetSpec>,
900 matcher: TextMatch,
901 },
902 Url {
903 matcher: TextMatch,
904 },
905 Document {
906 ready: WaitUntil,
907 },
908 NetworkQuiet {
909 #[serde(rename = "idleMs", alias = "idle_ms")]
910 idle_ms: u64,
911 #[serde(rename = "maxInFlight", alias = "max_in_flight")]
912 max_in_flight: usize,
913 #[serde(
914 default,
915 rename = "ignoreUrlSubstrings",
916 alias = "ignore_url_substrings",
917 skip_serializing_if = "Vec::is_empty"
918 )]
919 ignore_url_substrings: Vec<String>,
920 #[serde(
921 default,
922 rename = "ignoreResourceTypes",
923 alias = "ignore_resource_types",
924 skip_serializing_if = "Vec::is_empty"
925 )]
926 ignore_resource_types: Vec<NetworkResourceType>,
927 #[serde(
928 default,
929 rename = "ignoreLongLived",
930 alias = "ignore_long_lived",
931 skip_serializing_if = "std::ops::Not::not"
932 )]
933 ignore_long_lived: bool,
934 },
935}
936
937#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
938#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
939#[serde(rename_all = "camelCase")]
940pub struct WaitForCommand {
941 pub condition: WaitCondition,
942 pub timeout_ms: u64,
943}
944
945#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
946#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
947#[serde(tag = "kind", rename_all = "camelCase")]
948pub enum ScreenshotMode {
949 Viewport,
950 FullPage,
951 Element {
952 target: Box<TargetSpec>,
953 },
954 Clip {
955 x: f64,
956 y: f64,
957 width: f64,
958 height: f64,
959 },
960}
961
962#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
963#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
964#[serde(rename_all = "camelCase")]
965pub struct CaptureScreenshotCommand {
966 pub mode: ScreenshotMode,
967}
968
969#[derive(Debug, Clone, Serialize, Deserialize)]
970#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
971#[serde(rename_all = "camelCase")]
972pub struct SetFocusEmulationCommand {
973 pub enabled: bool,
974}
975
976#[derive(Debug, Clone, Serialize, Deserialize)]
977#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
978#[serde(rename_all = "camelCase")]
979pub struct SetEmulatedMediaCommand {
980 pub media: String,
981 pub features: BTreeMap<String, String>,
982}
983
984#[derive(Debug, Clone, Serialize, Deserialize)]
985#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
986#[serde(rename_all = "camelCase")]
987pub struct EvaluateJavaScriptCommand {
988 pub expression: String,
989 pub timeout_ms: u64,
990 #[serde(default)]
991 pub await_promise: bool,
992}
993
994#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
997#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
998#[serde(rename_all = "camelCase")]
999pub struct ExecutionPolicy {
1000 #[serde(default)]
1001 pub javascript_evaluation: bool,
1002 #[serde(default)]
1003 pub vision_assist: bool,
1004 #[serde(default)]
1009 pub fingerprint: bool,
1010 #[serde(default)]
1017 pub humanize: bool,
1018 #[serde(default, skip_serializing_if = "Option::is_none")]
1023 pub vision_node: Option<String>,
1024}
1025
1026#[derive(Debug, Clone, Serialize, Deserialize)]
1028#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1029#[serde(rename_all = "camelCase")]
1030pub struct CreateSessionRequest {
1031 pub profile: String,
1032 pub proxy: Option<String>,
1033 #[serde(default)]
1034 pub execution_policy: ExecutionPolicy,
1035}
1036
1037#[derive(Debug, Clone, Serialize, Deserialize)]
1039#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1040pub struct OpenPageRequest {
1041 pub session_id: SessionId,
1042}
1043
1044#[derive(Debug, Clone, Serialize, Deserialize)]
1045#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1046pub struct NavigationRequest {
1047 pub page_id: PageId,
1048 pub url: String,
1049 pub wait_until: Option<String>,
1050 pub timeout_ms: Option<u64>,
1051}
1052
1053#[derive(Debug, Clone, Serialize, Deserialize)]
1054#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1055pub struct ExtractRequest {
1056 pub page_id: PageId,
1057 pub fields: serde_json::Value,
1058}