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