1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value};
3use std::collections::{HashMap, HashSet};
4use thiserror::Error;
5use uptrakit_shared_macros::wire_safe_enum;
6use uuid::Uuid;
7
8use crate::{
9 Capability, CapabilitySet, DataSourceDescriptor, DataSourceKind, FrameworkGeneration,
10 FrameworkGenerationRange, InteractionDescriptor, InteractionHttpMethod, InteractionId,
11 InteractionKind, InteractionTransport, ProviderKind, RESERVED_PARAM_KEYS, SchemaContract,
12 Scope, SlotValidationError, SurfaceDescriptor, SurfaceId, SurfaceNode, SurfaceSlotDef,
13 Targeting, validate_slot_id, validate_surface_identifier,
14};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct SurfaceRegistration {
18 pub provider: ProviderIdentity,
19 pub framework_generation: FrameworkGeneration,
20 pub capabilities: CapabilitySet,
21 pub effective_tenant_binding: EffectiveTenantBinding,
22 #[serde(default, skip_serializing_if = "Vec::is_empty")]
23 pub surfaces: Vec<RegisteredSurface>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub encryption_metadata: Option<ProviderEncryptionMetadata>,
26}
27
28impl SurfaceRegistration {
29 pub fn validate_against(
38 &self,
39 policy: &SurfaceRegistrationPolicy,
40 ) -> Result<(), SurfaceRegistrationError> {
41 validate_supported_generation(self, policy)?;
42 validate_required_registration_capabilities(self, policy)?;
43 validate_registered_surfaces(self)?;
44 Ok(())
45 }
46}
47
48fn validate_supported_generation(
49 registration: &SurfaceRegistration,
50 policy: &SurfaceRegistrationPolicy,
51) -> Result<(), SurfaceRegistrationError> {
52 if policy
53 .supported_generation
54 .includes(registration.framework_generation)
55 {
56 return Ok(());
57 }
58
59 Err(registration_error(
60 SurfaceRegistrationErrorCode::UnsupportedGeneration,
61 format!(
62 "framework generation {}.{} is outside supported range {}.{}..={}.{}",
63 registration.framework_generation.major,
64 registration.framework_generation.minor,
65 policy.supported_generation.min.major,
66 policy.supported_generation.min.minor,
67 policy.supported_generation.max.major,
68 policy.supported_generation.max.minor,
69 ),
70 ))
71}
72
73fn validate_required_registration_capabilities(
74 registration: &SurfaceRegistration,
75 policy: &SurfaceRegistrationPolicy,
76) -> Result<(), SurfaceRegistrationError> {
77 if registration
78 .capabilities
79 .contains_all(&policy.required_capabilities)
80 {
81 return Ok(());
82 }
83
84 Err(missing_capability(
85 "registration is missing one or more required capabilities",
86 ))
87}
88
89fn validate_registered_surfaces(
90 registration: &SurfaceRegistration,
91) -> Result<(), SurfaceRegistrationError> {
92 RegisteredSurfacesValidator::new(registration).validate()
93}
94
95struct RegisteredSurfacesValidator<'a> {
96 provider_kind: ProviderKind,
97 capabilities: &'a CapabilitySet,
98 surfaces: &'a [RegisteredSurface],
99 surface_ids: HashSet<&'a str>,
100 single_entry_slots: HashSet<&'static str>,
101}
102
103impl<'a> RegisteredSurfacesValidator<'a> {
104 fn new(registration: &'a SurfaceRegistration) -> Self {
105 Self {
106 provider_kind: registration.provider.provider_kind,
107 capabilities: ®istration.capabilities,
108 surfaces: ®istration.surfaces,
109 surface_ids: HashSet::new(),
110 single_entry_slots: HashSet::new(),
111 }
112 }
113
114 fn validate(mut self) -> Result<(), SurfaceRegistrationError> {
115 for surface in self.surfaces {
116 self.validate_surface(surface)?;
117 }
118
119 Ok(())
120 }
121
122 fn validate_surface(
123 &mut self,
124 surface: &'a RegisteredSurface,
125 ) -> Result<(), SurfaceRegistrationError> {
126 validate_surface_descriptor_rules(
127 surface,
128 self.provider_kind,
129 self.capabilities,
130 &mut self.surface_ids,
131 &mut self.single_entry_slots,
132 )?;
133
134 let interaction_methods = validate_surface_interaction_rules(surface)?;
135 validate_workflow_step_references(
136 &surface.descriptor.surface_id,
137 &surface.interactions,
138 &interaction_methods,
139 )?;
140
141 let data_source_ids = validate_surface_data_source_rules(surface, &interaction_methods)?;
142 validate_root_node_references(
143 &surface.descriptor.surface_id,
144 &surface.descriptor.root_node,
145 &interaction_methods,
146 &surface.interactions,
147 &data_source_ids,
148 )
149 }
150}
151
152fn validate_surface_descriptor_rules<'a>(
153 surface: &'a RegisteredSurface,
154 registration_provider_kind: ProviderKind,
155 capabilities: &CapabilitySet,
156 surface_ids: &mut HashSet<&'a str>,
157 single_entry_slots: &mut HashSet<&'static str>,
158) -> Result<(), SurfaceRegistrationError> {
159 validate_unique_surface_id(surface, surface_ids)?;
160 validate_surface_provider_kind(surface, registration_provider_kind)?;
161 let slot_def = validate_surface_slot(surface)?;
162 validate_single_entry_slot_occupancy(slot_def, single_entry_slots)?;
163 validate_surface_priority_range(surface, slot_def)?;
164 validate_surface_required_capabilities(surface, capabilities)?;
165 validate_surface_usage_capabilities(
166 &surface.descriptor.surface_id,
167 capabilities,
168 &surface.descriptor.root_node,
169 &surface.descriptor.targeting,
170 &surface.interactions,
171 &surface.data_sources,
172 )
173}
174
175fn validate_unique_surface_id<'a>(
176 surface: &'a RegisteredSurface,
177 surface_ids: &mut HashSet<&'a str>,
178) -> Result<(), SurfaceRegistrationError> {
179 if surface_ids.insert(surface.descriptor.surface_id.as_str()) {
180 return Ok(());
181 }
182
183 Err(invalid_contract(format!(
184 "duplicate surface_id `{}` within registration batch",
185 surface.descriptor.surface_id
186 )))
187}
188
189fn validate_surface_provider_kind(
190 surface: &RegisteredSurface,
191 registration_provider_kind: ProviderKind,
192) -> Result<(), SurfaceRegistrationError> {
193 if surface.descriptor.provider_kind == registration_provider_kind {
194 return Ok(());
195 }
196
197 Err(invalid_contract(format!(
198 "surface `{}` provider_kind does not match registration provider_kind",
199 surface.descriptor.surface_id
200 )))
201}
202
203fn validate_surface_slot(
204 surface: &RegisteredSurface,
205) -> Result<&'static SurfaceSlotDef, SurfaceRegistrationError> {
206 validate_slot_id(&surface.descriptor.slot).map_err(map_slot_validation_error)
207}
208
209fn validate_single_entry_slot_occupancy(
210 slot_def: &'static SurfaceSlotDef,
211 single_entry_slots: &mut HashSet<&'static str>,
212) -> Result<(), SurfaceRegistrationError> {
213 if slot_def.multi_entry || single_entry_slots.insert(slot_def.id) {
214 return Ok(());
215 }
216
217 Err(invalid_contract(format!(
218 "slot `{}` is single-entry and cannot accept multiple surfaces in one registration batch",
219 slot_def.id
220 )))
221}
222
223fn validate_surface_priority_range(
224 surface: &RegisteredSurface,
225 slot_def: &'static SurfaceSlotDef,
226) -> Result<(), SurfaceRegistrationError> {
227 if surface.descriptor.provider_kind == ProviderKind::BuiltIn {
228 return Ok(());
229 }
230
231 if surface.descriptor.priority >= slot_def.provider_priority_min
232 && surface.descriptor.priority <= slot_def.provider_priority_max
233 {
234 return Ok(());
235 }
236
237 Err(invalid_contract(format!(
238 "surface `{}` priority {} is outside slot `{}` provider range {}..={}",
239 surface.descriptor.surface_id,
240 surface.descriptor.priority,
241 slot_def.id,
242 slot_def.provider_priority_min,
243 slot_def.provider_priority_max
244 )))
245}
246
247fn validate_surface_required_capabilities(
248 surface: &RegisteredSurface,
249 capabilities: &CapabilitySet,
250) -> Result<(), SurfaceRegistrationError> {
251 if capabilities.contains_all(&surface.descriptor.required_capabilities) {
252 return Ok(());
253 }
254
255 Err(missing_capability(format!(
256 "surface `{}` requires capabilities not advertised by registration",
257 surface.descriptor.surface_id
258 )))
259}
260
261fn validate_surface_interaction_rules(
262 surface: &RegisteredSurface,
263) -> Result<HashMap<&str, Vec<InteractionHttpMethod>>, SurfaceRegistrationError> {
264 let mut interaction_methods: HashMap<&str, Vec<InteractionHttpMethod>> = HashMap::new();
265 for interaction in &surface.interactions {
266 validate_unique_interaction_id(surface, interaction, &mut interaction_methods)?;
267 validate_interaction_method(surface, interaction)?;
268 validate_interaction_params(surface, interaction)?;
269 validate_interaction_provider_rules(surface, interaction)?;
270 validate_descriptor_gated_provider_invocable(surface, interaction)?;
271 }
272
273 Ok(interaction_methods)
274}
275
276fn validate_unique_interaction_id<'a>(
282 surface: &'a RegisteredSurface,
283 interaction: &'a InteractionDescriptor,
284 interaction_methods: &mut HashMap<&'a str, Vec<InteractionHttpMethod>>,
285) -> Result<(), SurfaceRegistrationError> {
286 let method = interaction.effective_http_method();
287 let methods = interaction_methods
288 .entry(interaction.interaction_id.as_str())
289 .or_default();
290 if methods.contains(&method) {
291 return Err(invalid_contract(format!(
292 "duplicate interaction `{}` [{}] within surface `{}`",
293 interaction.interaction_id, method, surface.descriptor.surface_id
294 )));
295 }
296 methods.push(method);
297 Ok(())
298}
299
300fn validate_interaction_method(
304 surface: &RegisteredSurface,
305 interaction: &InteractionDescriptor,
306) -> Result<(), SurfaceRegistrationError> {
307 let id = &interaction.interaction_id;
308 let surface_id = &surface.descriptor.surface_id;
309 if matches!(interaction.http_method, InteractionHttpMethod::Other(_)) {
310 return Err(invalid_contract(format!(
311 "interaction `{id}` in surface `{surface_id}` declares an unknown http_method"
312 )));
313 }
314 match interaction.kind {
315 InteractionKind::DataLoad => {
318 if matches!(
319 interaction.http_method,
320 InteractionHttpMethod::Put | InteractionHttpMethod::Delete
321 ) {
322 return Err(invalid_contract(format!(
323 "data-load interaction `{id}` in surface `{surface_id}` must use GET"
324 )));
325 }
326 }
327 InteractionKind::Workflow => {
328 if interaction.http_method != InteractionHttpMethod::Post {
329 return Err(invalid_contract(format!(
330 "workflow interaction `{id}` in surface `{surface_id}` must use POST"
331 )));
332 }
333 }
334 _ => {
338 if interaction.http_method == InteractionHttpMethod::Get {
339 return Err(invalid_contract(format!(
340 "interaction `{id}` in surface `{surface_id}` is not a data-load and cannot use GET"
341 )));
342 }
343 }
344 }
345 Ok(())
346}
347
348fn validate_interaction_params(
353 surface: &RegisteredSurface,
354 interaction: &InteractionDescriptor,
355) -> Result<(), SurfaceRegistrationError> {
356 let id = &interaction.interaction_id;
357 let surface_id = &surface.descriptor.surface_id;
358 let mut seen = HashSet::new();
359 for field in &interaction.params {
360 if RESERVED_PARAM_KEYS.contains(&field.key.as_str()) {
361 return Err(invalid_contract(format!(
362 "interaction `{id}` in surface `{surface_id}` declares reserved param key `{}`",
363 field.key
364 )));
365 }
366 if !seen.insert(field.key.as_str()) {
367 return Err(invalid_contract(format!(
368 "interaction `{id}` in surface `{surface_id}` declares duplicate param key `{}`",
369 field.key
370 )));
371 }
372 if interaction.kind == InteractionKind::DataLoad
373 && !matches!(
374 field.schema,
375 SchemaContract::String
376 | SchemaContract::Integer
377 | SchemaContract::Number
378 | SchemaContract::Boolean
379 )
380 {
381 return Err(invalid_contract(format!(
382 "data-load interaction `{id}` in surface `{surface_id}` param `{}` must be a scalar schema",
383 field.key
384 )));
385 }
386 }
387 if interaction.kind == InteractionKind::DataLoad && !interaction.sensitive_fields.is_empty() {
388 return Err(invalid_contract(format!(
389 "data-load interaction `{id}` in surface `{surface_id}` must not declare sensitive_fields \
390 (GET params travel in query strings)"
391 )));
392 }
393 Ok(())
394}
395
396fn validate_interaction_provider_rules(
397 surface: &RegisteredSurface,
398 interaction: &InteractionDescriptor,
399) -> Result<(), SurfaceRegistrationError> {
400 interaction
401 .validate_for_provider(surface.descriptor.provider_kind)
402 .map_err(|err| invalid_contract(err.to_string()))
403}
404
405fn validate_descriptor_gated_provider_invocable(
421 surface: &RegisteredSurface,
422 interaction: &InteractionDescriptor,
423) -> Result<(), SurfaceRegistrationError> {
424 if interaction.provider_invocable
425 && surface.descriptor.provider_kind == ProviderKind::Service
426 && surface.descriptor.required_action.is_some()
427 {
428 return Err(invalid_contract(format!(
429 "interaction `{}` in surface `{}` sets provider_invocable under an \
430 action-gated surface descriptor — not allowed for service-registered surfaces",
431 interaction.interaction_id, surface.descriptor.surface_id
432 )));
433 }
434 Ok(())
435}
436
437fn validate_surface_data_source_rules<'a>(
438 surface: &'a RegisteredSurface,
439 interaction_methods: &HashMap<&str, Vec<InteractionHttpMethod>>,
440) -> Result<HashSet<&'a str>, SurfaceRegistrationError> {
441 let mut data_source_ids: HashSet<&str> = HashSet::new();
442 for data_source in &surface.data_sources {
443 validate_unique_data_source_id(surface, data_source, &mut data_source_ids)?;
444 validate_data_source_provider_rules(surface, data_source)?;
445 validate_data_source_reference_rules(surface, data_source, interaction_methods)?;
446 }
447
448 Ok(data_source_ids)
449}
450
451fn validate_data_source_reference_rules(
454 surface: &RegisteredSurface,
455 data_source: &DataSourceDescriptor,
456 interaction_methods: &HashMap<&str, Vec<InteractionHttpMethod>>,
457) -> Result<(), SurfaceRegistrationError> {
458 if let DataSourceKind::ProviderQuery { operation_id } = &data_source.kind {
459 require_pair_reference(
460 interaction_methods,
461 operation_id,
462 InteractionHttpMethod::Get,
463 || {
464 format!(
465 "surface `{}` data source `{}` references unknown provider_query operation_id `{}`",
466 surface.descriptor.surface_id, data_source.data_source_id, operation_id
467 )
468 },
469 )?;
470 }
471 Ok(())
472}
473
474fn validate_unique_data_source_id<'a>(
475 surface: &'a RegisteredSurface,
476 data_source: &'a DataSourceDescriptor,
477 data_source_ids: &mut HashSet<&'a str>,
478) -> Result<(), SurfaceRegistrationError> {
479 if data_source_ids.insert(data_source.data_source_id.as_str()) {
480 return Ok(());
481 }
482
483 Err(invalid_contract(format!(
484 "duplicate data_source_id `{}` within surface `{}`",
485 data_source.data_source_id, surface.descriptor.surface_id
486 )))
487}
488
489fn validate_data_source_provider_rules(
490 surface: &RegisteredSurface,
491 data_source: &DataSourceDescriptor,
492) -> Result<(), SurfaceRegistrationError> {
493 data_source
494 .validate_for_provider(surface.descriptor.provider_kind)
495 .map_err(|err| invalid_contract(err.to_string()))
496}
497
498fn map_slot_validation_error(err: SlotValidationError) -> SurfaceRegistrationError {
499 let code = match err {
500 SlotValidationError::UnknownSlot(_) => SurfaceRegistrationErrorCode::InvalidSlot,
501 SlotValidationError::InvalidIdentifier(_) => SurfaceRegistrationErrorCode::InvalidContract,
502 };
503
504 registration_error(code, err.to_string())
505}
506
507fn registration_error(
508 code: SurfaceRegistrationErrorCode,
509 message: impl Into<String>,
510) -> SurfaceRegistrationError {
511 SurfaceRegistrationError::new(code, message.into())
512}
513
514fn invalid_contract(message: impl Into<String>) -> SurfaceRegistrationError {
515 registration_error(SurfaceRegistrationErrorCode::InvalidContract, message)
516}
517
518fn missing_capability(message: impl Into<String>) -> SurfaceRegistrationError {
519 registration_error(SurfaceRegistrationErrorCode::MissingCapability, message)
520}
521
522fn validate_surface_usage_capabilities(
523 surface_id: &SurfaceId,
524 capabilities: &CapabilitySet,
525 root_node: &SurfaceNode,
526 targeting: &Targeting,
527 interactions: &[InteractionDescriptor],
528 data_sources: &[DataSourceDescriptor],
529) -> Result<(), SurfaceRegistrationError> {
530 validate_node_capabilities(surface_id, capabilities, root_node)?;
531
532 let targeting_capability = match targeting {
533 Targeting::Universal => Capability::UniversalTargeting,
534 Targeting::Targeted => Capability::TargetedTargeting,
535 };
536 require_capability(
537 capabilities,
538 targeting_capability,
539 surface_id,
540 "targeting mode",
541 )?;
542
543 for interaction in interactions {
544 let kind_capability = match interaction.kind {
545 InteractionKind::MutationAction => Capability::MutationAction,
546 InteractionKind::FormSubmit => Capability::FormSubmit,
547 InteractionKind::Workflow => Capability::Workflow,
548 InteractionKind::Navigate => Capability::Navigate,
549 InteractionKind::DataLoad => Capability::DataLoad,
550 InteractionKind::ConfirmableAction => Capability::ConfirmableAction,
551 };
552 require_capability(
553 capabilities,
554 kind_capability,
555 surface_id,
556 "interaction kind",
557 )?;
558
559 if matches!(
560 &interaction.transport,
561 InteractionTransport::ProviderProxied
562 ) {
563 require_capability(
564 capabilities,
565 Capability::ProviderInitiatedActions,
566 surface_id,
567 "interaction transport",
568 )?;
569 }
570
571 if !interaction.sensitive_fields.is_empty() {
572 require_capability(
573 capabilities,
574 Capability::SensitiveFields,
575 surface_id,
576 "sensitive fields",
577 )?;
578 }
579 }
580
581 for data_source in data_sources {
582 let kind_capability = match &data_source.kind {
583 DataSourceKind::Static { .. } => Capability::StaticDataSource,
584 DataSourceKind::ControllerQuery { .. } => Capability::ControllerQueryDataSource,
585 DataSourceKind::ProviderQuery { .. } => Capability::ProviderQueryDataSource,
586 };
587 require_capability(
588 capabilities,
589 kind_capability,
590 surface_id,
591 "data source kind",
592 )?;
593 }
594
595 Ok(())
596}
597
598fn validate_node_capabilities(
599 surface_id: &SurfaceId,
600 capabilities: &CapabilitySet,
601 node: &SurfaceNode,
602) -> Result<(), SurfaceRegistrationError> {
603 let node_capability = match node {
604 SurfaceNode::Section { children, .. } => {
605 for child in children {
606 validate_node_capabilities(surface_id, capabilities, child)?;
607 }
608 Capability::SectionNode
609 }
610 SurfaceNode::TextBlock { .. } => Capability::TextBlockNode,
611 SurfaceNode::KeyValue { .. } => Capability::KeyValueNode,
612 SurfaceNode::Table { .. } => Capability::TableNode,
613 SurfaceNode::Form { .. } => Capability::FormNode,
614 SurfaceNode::ActionBar { .. } => Capability::ActionBarNode,
615 SurfaceNode::Tabs { tabs } => {
616 for tab in tabs {
617 validate_node_capabilities(surface_id, capabilities, &tab.root)?;
618 }
619 Capability::TabsNode
620 }
621 SurfaceNode::Callout { .. } => Capability::CalloutNode,
622 SurfaceNode::EmptyState { .. } => Capability::EmptyStateNode,
623 SurfaceNode::ModalTrigger { modal_nodes, .. } => {
624 for child in modal_nodes {
625 validate_node_capabilities(surface_id, capabilities, child)?;
626 }
627 Capability::ModalTriggerNode
628 }
629 SurfaceNode::WorkflowTrigger { step_nodes, .. } => {
630 for child in step_nodes {
631 validate_node_capabilities(surface_id, capabilities, child)?;
632 }
633 Capability::WorkflowTriggerNode
634 }
635 };
636
637 require_capability(capabilities, node_capability, surface_id, "root_node kind")
638}
639
640fn require_capability(
641 capabilities: &CapabilitySet,
642 required: Capability,
643 surface_id: &SurfaceId,
644 usage: &str,
645) -> Result<(), SurfaceRegistrationError> {
646 if capabilities.0.contains(&required) {
647 return Ok(());
648 }
649
650 Err(missing_capability(format!(
651 "surface `{}` uses {} that requires capability `{}`",
652 surface_id,
653 usage,
654 serde_json::to_string(&required)
655 .unwrap_or_else(|_| "\"unknown\"".to_owned())
656 .trim_matches('"')
657 )))
658}
659
660fn validate_root_node_references(
661 surface_id: &SurfaceId,
662 node: &SurfaceNode,
663 interaction_methods: &HashMap<&str, Vec<InteractionHttpMethod>>,
664 interactions: &[InteractionDescriptor],
665 data_source_ids: &HashSet<&str>,
666) -> Result<(), SurfaceRegistrationError> {
667 RootNodeReferenceValidator::new(
668 surface_id,
669 interaction_methods,
670 interactions,
671 data_source_ids,
672 )
673 .validate(node)
674}
675
676struct RootNodeReferenceValidator<'a> {
677 surface_id: &'a SurfaceId,
678 interaction_methods: &'a HashMap<&'a str, Vec<InteractionHttpMethod>>,
679 interactions: &'a [InteractionDescriptor],
680 data_source_ids: &'a HashSet<&'a str>,
681}
682
683impl<'a> RootNodeReferenceValidator<'a> {
684 fn new(
685 surface_id: &'a SurfaceId,
686 interaction_methods: &'a HashMap<&'a str, Vec<InteractionHttpMethod>>,
687 interactions: &'a [InteractionDescriptor],
688 data_source_ids: &'a HashSet<&'a str>,
689 ) -> Self {
690 Self {
691 surface_id,
692 interaction_methods,
693 interactions,
694 data_source_ids,
695 }
696 }
697
698 fn validate(&self, node: &SurfaceNode) -> Result<(), SurfaceRegistrationError> {
699 match node {
700 SurfaceNode::Section { children, .. } => self.validate_children(children),
702 SurfaceNode::TextBlock { .. } => Ok(()),
703 SurfaceNode::KeyValue { data_source_id } => {
704 self.require_data_source_reference(data_source_id.as_str())
705 }
706 SurfaceNode::Table {
707 data_source_id,
708 row_actions,
709 ..
710 } => {
711 self.require_data_source_reference(data_source_id.as_str())?;
712 self.validate_table_row_actions(row_actions)
713 }
714 SurfaceNode::Form {
715 interaction_id,
716 http_method,
717 } => {
718 self.require_root_interaction_reference(
719 interaction_id.as_str(),
720 http_method.as_ref(),
721 )?;
722 self.require_form_pre_load_reference(interaction_id.as_str(), http_method.as_ref())
723 }
724 SurfaceNode::ActionBar { action_ids } => self.validate_action_bar(action_ids),
725 SurfaceNode::Tabs { tabs } => self.validate_tabs(tabs),
726 SurfaceNode::Callout { .. } | SurfaceNode::EmptyState { .. } => Ok(()),
727 SurfaceNode::ModalTrigger {
728 interaction_id,
729 http_method,
730 modal_nodes,
731 } => {
732 self.require_root_interaction_reference(
733 interaction_id.as_str(),
734 http_method.as_ref(),
735 )?;
736 self.validate_children(modal_nodes)
737 }
738 SurfaceNode::WorkflowTrigger {
739 interaction_id,
740 step_nodes,
741 } => {
742 self.require_root_workflow_trigger_reference(interaction_id.as_str())?;
743 self.validate_children(step_nodes)
744 }
745 }
746 }
747
748 fn validate_children(&self, nodes: &[SurfaceNode]) -> Result<(), SurfaceRegistrationError> {
749 for child in nodes {
750 self.validate(child)?;
751 }
752 Ok(())
753 }
754
755 fn validate_table_row_actions(
756 &self,
757 row_actions: &[crate::SurfaceTableRowAction],
758 ) -> Result<(), SurfaceRegistrationError> {
759 for row_action in row_actions {
760 resolve_interaction_reference(
761 self.interaction_methods,
762 row_action.interaction_id.as_str(),
763 row_action.http_method.as_ref(),
764 || {
765 format!(
766 "surface `{}` table references unknown row-action interaction_id `{}`",
767 self.surface_id, row_action.interaction_id
768 )
769 },
770 )?;
771 }
772 Ok(())
773 }
774
775 fn validate_action_bar(
776 &self,
777 action_ids: &[crate::ActionRef],
778 ) -> Result<(), SurfaceRegistrationError> {
779 for action_ref in action_ids {
780 resolve_interaction_reference(
781 self.interaction_methods,
782 action_ref.interaction_id().as_str(),
783 action_ref.http_method(),
784 || {
785 format!(
786 "surface `{}` root_node references unknown interaction_id `{}`",
787 self.surface_id,
788 action_ref.interaction_id()
789 )
790 },
791 )?;
792 }
793 Ok(())
794 }
795
796 fn validate_tabs(&self, tabs: &[crate::SurfaceTab]) -> Result<(), SurfaceRegistrationError> {
797 let mut tab_ids: HashSet<&str> = HashSet::new();
798
799 for tab in tabs {
800 validate_surface_identifier(tab.id.as_str()).map_err(|err| {
801 invalid_contract(format!(
802 "surface `{}` root_node contains invalid tab id `{}`: {}",
803 self.surface_id, tab.id, err
804 ))
805 })?;
806
807 if !tab_ids.insert(tab.id.as_str()) {
808 return Err(invalid_contract(format!(
809 "surface `{}` root_node contains duplicate tab id `{}` within one tabs node",
810 self.surface_id, tab.id
811 )));
812 }
813
814 self.validate(&tab.root)?;
815 }
816
817 Ok(())
818 }
819
820 fn require_root_interaction_reference(
821 &self,
822 interaction_id: &str,
823 declared_method: Option<&InteractionHttpMethod>,
824 ) -> Result<(), SurfaceRegistrationError> {
825 resolve_interaction_reference(
826 self.interaction_methods,
827 interaction_id,
828 declared_method,
829 || {
830 format!(
831 "surface `{}` root_node references unknown interaction_id `{}`",
832 self.surface_id, interaction_id
833 )
834 },
835 )
836 }
837
838 fn require_root_workflow_trigger_reference(
839 &self,
840 interaction_id: &str,
841 ) -> Result<(), SurfaceRegistrationError> {
842 require_pair_reference(
843 self.interaction_methods,
844 interaction_id,
845 InteractionHttpMethod::Post,
846 || {
847 format!(
848 "surface `{}` root_node references unknown interaction_id `{}`",
849 self.surface_id, interaction_id
850 )
851 },
852 )
853 }
854
855 fn require_form_pre_load_reference(
860 &self,
861 interaction_id: &str,
862 declared_method: Option<&InteractionHttpMethod>,
863 ) -> Result<(), SurfaceRegistrationError> {
864 let Some(target) = self.interactions.iter().find(|interaction| {
873 interaction.interaction_id.as_str() == interaction_id
874 && declared_method
875 .map(|method| interaction.effective_http_method() == *method)
876 .unwrap_or(true)
877 }) else {
878 return Ok(());
880 };
881 let Some(pre_load_interaction_id) = target
882 .form_ui
883 .as_ref()
884 .and_then(|form_ui| form_ui.pre_load_interaction_id.as_ref())
885 else {
886 return Ok(());
887 };
888
889 require_pair_reference(
890 self.interaction_methods,
891 pre_load_interaction_id.as_str(),
892 InteractionHttpMethod::Get,
893 || {
894 format!(
895 "surface `{}` root_node form `{}` references unknown pre_load_interaction_id `{}`",
896 self.surface_id, interaction_id, pre_load_interaction_id
897 )
898 },
899 )
900 }
901
902 fn require_data_source_reference(
903 &self,
904 data_source_id: &str,
905 ) -> Result<(), SurfaceRegistrationError> {
906 ensure_known_reference(self.data_source_ids, data_source_id, || {
907 format!(
908 "surface `{}` root_node references unknown data_source_id `{}`",
909 self.surface_id, data_source_id
910 )
911 })
912 }
913}
914
915fn validate_workflow_step_references(
916 surface_id: &SurfaceId,
917 interactions: &[InteractionDescriptor],
918 interaction_methods: &HashMap<&str, Vec<InteractionHttpMethod>>,
919) -> Result<(), SurfaceRegistrationError> {
920 WorkflowStepReferenceValidator::new(surface_id, interaction_methods).validate(interactions)
921}
922
923struct WorkflowStepReferenceValidator<'a> {
924 surface_id: &'a SurfaceId,
925 interaction_methods: &'a HashMap<&'a str, Vec<InteractionHttpMethod>>,
926}
927
928impl<'a> WorkflowStepReferenceValidator<'a> {
929 fn new(
930 surface_id: &'a SurfaceId,
931 interaction_methods: &'a HashMap<&'a str, Vec<InteractionHttpMethod>>,
932 ) -> Self {
933 Self {
934 surface_id,
935 interaction_methods,
936 }
937 }
938
939 fn validate(
940 &self,
941 interactions: &[InteractionDescriptor],
942 ) -> Result<(), SurfaceRegistrationError> {
943 for interaction in interactions {
944 if interaction.kind != InteractionKind::Workflow {
945 continue;
946 }
947 self.validate_workflow_interaction_steps(interaction)?;
948 }
949
950 Ok(())
951 }
952
953 fn validate_workflow_interaction_steps(
954 &self,
955 interaction: &InteractionDescriptor,
956 ) -> Result<(), SurfaceRegistrationError> {
957 for step in &interaction.workflow_steps {
958 if let Some(submit_interaction_id) = &step.submit_interaction_id {
959 require_pair_reference(
960 self.interaction_methods,
961 submit_interaction_id.as_str(),
962 InteractionHttpMethod::Post,
963 || {
964 format!(
965 "surface `{}` workflow interaction `{}` references unknown submit_interaction_id `{}` in step `{}`",
966 self.surface_id,
967 interaction.interaction_id,
968 submit_interaction_id,
969 step.step_id
970 )
971 },
972 )?;
973 }
974
975 if let Some(form_ui) = &step.form_ui
976 && let Some(pre_load_interaction_id) = &form_ui.pre_load_interaction_id
977 {
978 require_pair_reference(
979 self.interaction_methods,
980 pre_load_interaction_id.as_str(),
981 InteractionHttpMethod::Get,
982 || {
983 format!(
984 "surface `{}` workflow interaction `{}` references unknown pre_load_interaction_id `{}` in step `{}`",
985 self.surface_id,
986 interaction.interaction_id,
987 pre_load_interaction_id,
988 step.step_id
989 )
990 },
991 )?;
992 }
993 }
994
995 Ok(())
996 }
997}
998
999fn ensure_known_reference(
1000 known_ids: &HashSet<&str>,
1001 reference_id: &str,
1002 error_message: impl FnOnce() -> String,
1003) -> Result<(), SurfaceRegistrationError> {
1004 if known_ids.contains(reference_id) {
1005 return Ok(());
1006 }
1007
1008 Err(invalid_contract(error_message()))
1009}
1010
1011fn resolve_interaction_reference(
1017 interaction_methods: &HashMap<&str, Vec<InteractionHttpMethod>>,
1018 reference_id: &str,
1019 declared_method: Option<&InteractionHttpMethod>,
1020 error_context: impl FnOnce() -> String,
1021) -> Result<(), SurfaceRegistrationError> {
1022 let Some(methods) = interaction_methods.get(reference_id) else {
1023 return Err(invalid_contract(error_context()));
1024 };
1025 match declared_method {
1026 Some(method) if !methods.contains(method) => Err(invalid_contract(format!(
1027 "{} — `{reference_id}` is not registered under method `{method}`",
1028 error_context()
1029 ))),
1030 None if methods.len() > 1 => Err(invalid_contract(format!(
1034 "{} — `{reference_id}` is registered under multiple methods; declare http_method — ambiguous",
1035 error_context()
1036 ))),
1037 _ => Ok(()),
1038 }
1039}
1040
1041fn require_pair_reference(
1046 interaction_methods: &HashMap<&str, Vec<InteractionHttpMethod>>,
1047 reference_id: &str,
1048 required: InteractionHttpMethod,
1049 error_context: impl FnOnce() -> String,
1050) -> Result<(), SurfaceRegistrationError> {
1051 match interaction_methods.get(reference_id) {
1052 Some(methods) if methods.contains(&required) => Ok(()),
1053 _ => Err(invalid_contract(format!(
1054 "{} — `{reference_id}` must be registered under method `{required}`",
1055 error_context()
1056 ))),
1057 }
1058}
1059
1060#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1061pub struct ProviderIdentity {
1062 pub provider_id: String,
1063 pub provider_kind: ProviderKind,
1064 pub provider_namespace: String,
1065}
1066
1067#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1068pub struct EffectiveTenantBinding {
1069 pub scope: Scope,
1070 #[serde(default, skip_serializing_if = "Option::is_none")]
1071 pub tenant_id: Option<String>,
1072}
1073
1074#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1075pub struct ProviderEncryptionMetadata {
1076 pub key_id: String,
1077 pub algorithm: ProviderEncryptionAlgorithm,
1078 pub public_key: String,
1079}
1080
1081wire_safe_enum! {
1082 #[derive(Debug, Clone, PartialEq, Eq)]
1084 pub enum ProviderEncryptionAlgorithm {
1085 EciesP256 => "ecies_p256",
1086 }
1087 parse_error = ParseProviderEncryptionAlgorithmError("invalid provider encryption algorithm");
1088}
1089
1090impl ProviderEncryptionAlgorithm {
1091 pub const KNOWN_VARIANTS: &'static [ProviderEncryptionAlgorithm] =
1094 &[ProviderEncryptionAlgorithm::EciesP256];
1095}
1096
1097#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1098pub struct RegisteredSurface {
1099 pub descriptor: SurfaceDescriptor,
1100 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1101 pub interactions: Vec<InteractionDescriptor>,
1102 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1103 pub data_sources: Vec<DataSourceDescriptor>,
1104}
1105
1106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1107pub struct SurfaceRegistrationPolicy {
1108 pub supported_generation: FrameworkGenerationRange,
1109 pub required_capabilities: CapabilitySet,
1110}
1111
1112#[derive(Debug, Clone, PartialEq, Eq, Error)]
1113#[error("{code:?}: {message}")]
1114pub struct SurfaceRegistrationError {
1115 pub code: SurfaceRegistrationErrorCode,
1116 pub message: String,
1117}
1118
1119impl SurfaceRegistrationError {
1120 pub fn new(code: SurfaceRegistrationErrorCode, message: String) -> Self {
1121 Self { code, message }
1122 }
1123}
1124
1125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1126#[serde(rename_all = "snake_case")]
1127pub enum SurfaceRegistrationErrorCode {
1128 UnsupportedGeneration,
1129 MissingCapability,
1130 InvalidSlot,
1131 InvalidContract,
1132}
1133
1134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1135pub struct SurfaceActionRequest {
1136 pub request_id: Uuid,
1137 pub tenant_id: String,
1138 pub surface_id: SurfaceId,
1139 pub interaction_id: InteractionId,
1140 #[serde(default)]
1143 pub method: InteractionHttpMethod,
1144 pub idempotency_key: String,
1145 #[serde(default, skip_serializing_if = "Option::is_none")]
1146 pub target_provider_id: Option<String>,
1147 pub caller_origin: CallerOrigin,
1148 #[serde(default, skip_serializing_if = "Map::is_empty")]
1149 pub params: Map<String, Value>,
1150 #[serde(default, skip_serializing_if = "Option::is_none")]
1151 pub encrypted_sensitive_params: Option<EncryptedSensitiveParams>,
1152}
1153
1154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1155#[serde(rename_all = "snake_case", tag = "kind")]
1156pub enum CallerOrigin {
1157 UserSession { user_id: String, session_id: String },
1158 BuiltInSystem { principal: String },
1159 Provider { provider_id: String },
1160}
1161
1162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1163pub struct EncryptedSensitiveParams {
1164 pub key_id: String,
1165 pub algorithm: ProviderEncryptionAlgorithm,
1166 pub ciphertext_b64: String,
1167}
1168
1169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1170pub struct SurfaceActionCancel {
1171 pub request_id: Uuid,
1172 pub target_provider_id: String,
1173 pub reason: SurfaceActionCancelReason,
1174}
1175
1176#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1177#[serde(rename_all = "snake_case")]
1178pub enum SurfaceActionCancelReason {
1179 Timeout,
1180 RequestCancelled,
1181 ProviderDisconnected,
1182}
1183
1184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1185pub struct SurfaceActionResponse {
1186 pub request_id: Uuid,
1187 pub success: bool,
1188 #[serde(default, skip_serializing_if = "Option::is_none")]
1189 pub result: Option<Value>,
1190 #[serde(default, skip_serializing_if = "Option::is_none")]
1191 pub error: Option<SurfaceActionError>,
1192}
1193
1194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1195pub struct SurfaceActionError {
1196 pub code: SurfaceActionErrorCode,
1197 pub message: String,
1198 #[serde(default, skip_serializing_if = "Option::is_none")]
1199 pub details: Option<Value>,
1200}
1201
1202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1203#[serde(rename_all = "snake_case")]
1204pub enum SurfaceActionErrorCode {
1205 PermissionDenied,
1206 InvalidRequest,
1207 SchemaValidationFailed,
1208 UnsupportedCapability,
1209 ProviderUnavailable,
1210 Timeout,
1211 DuplicateRequest,
1212 InternalError,
1213}
1214
1215#[cfg(test)]
1216mod tests {
1217 use super::*;
1218
1219 #[test]
1220 fn surface_action_request_without_method_deserializes_to_post() {
1221 let json = serde_json::json!({
1223 "request_id": "018f0000-0000-7000-8000-000000000000",
1224 "tenant_id": "018f0000-0000-7000-8000-000000000001",
1225 "surface_id": "test.surface",
1226 "interaction_id": "save",
1227 "idempotency_key": "k1",
1228 "caller_origin": { "kind": "built_in_system", "principal": "test" }
1229 });
1230 let request: SurfaceActionRequest = serde_json::from_value(json).expect("old-peer shape");
1231 assert_eq!(request.method, InteractionHttpMethod::Post);
1232 }
1233}