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