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