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