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