1use std::collections::BTreeSet;
2use std::error::Error;
3use std::fmt;
4
5use crate::{
6 AdapterConformanceManifestHash, ApplicationExportManifestHash, ApplicationExportReceiptHash,
7 ApplicationInstallationPlanHash, ApplicationLockHash, ApplicationManifestHash,
8 ApplicationPortabilityManifestHash, ApplicationRoleHash, ApplicationSourceHash, CapabilityId,
9 ContractBundleHash, ContractLineage, ContractVersion, DatabaseAlias, Environment,
10 GeneratedArtifactHash, MigrationBundleHash, hash_application_installation_plan,
11};
12use serde::{Deserialize, Serialize};
13
14pub const APPLICATION_INSTALLATION_PLAN_SCHEMA_V1: &str = "riffdb.application-installation-plan/v1";
16pub const APPLICATION_INSTALLATION_PLAN_SCHEMA_V2: &str = "riffdb.application-installation-plan/v2";
18pub const APPLICATION_INSTALLATION_PLAN_SCHEMA_V3: &str = "riffdb.application-installation-plan/v3";
20pub const MAX_INSTALLATION_PLAN_BYTES: usize = 4 * 1_024 * 1_024;
22pub const MAX_INSTALLATION_ARTIFACTS: usize = 256;
24pub const MAX_INSTALLATION_ROLES: usize = 128;
26pub const MAX_CREDENTIAL_DESTINATIONS: usize = 256;
28pub const MAX_INSTALLATION_SEEDS: usize = 256;
30pub const MAX_SEED_BATCH_ITEMS: u64 = 1_000_000;
32pub const MAX_ROLE_OPERATIONS: usize = 1_024;
34pub const MAX_ROLE_SECRET_OUTPUTS: usize = 1_024;
36pub const MAX_ROLE_AUTHORITY_ATOMS: usize = 2_048;
38pub const MAX_INSTALLATION_SYMBOL_BYTES: usize = 256;
40
41#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
43pub struct InstallationSymbol(String);
44
45impl InstallationSymbol {
46 pub fn new(value: impl Into<String>) -> Result<Self, InstallationPlanError> {
48 let value = value.into();
49 if value.is_empty() || value.len() > MAX_INSTALLATION_SYMBOL_BYTES {
50 return Err(InstallationPlanError::new(
51 InstallationPlanErrorKind::LimitExceeded,
52 ));
53 }
54 if let Some(index) = value.bytes().enumerate().find_map(|(index, byte)| {
55 (!(byte == b'_'
56 || byte == b'-'
57 || byte == b'.'
58 || byte.is_ascii_alphabetic()
59 || index > 0 && byte.is_ascii_digit()))
60 .then_some(index)
61 }) {
62 let _ = index;
63 return Err(InstallationPlanError::new(
64 InstallationPlanErrorKind::InvalidIdentifier,
65 ));
66 }
67 Ok(Self(value))
68 }
69
70 #[must_use]
72 pub fn as_str(&self) -> &str {
73 &self.0
74 }
75}
76
77impl fmt::Debug for InstallationSymbol {
78 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79 formatter
80 .debug_tuple("InstallationSymbol")
81 .field(&self.0)
82 .finish()
83 }
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct InstallationTarget {
89 database: DatabaseAlias,
90 environment: Environment,
91 lineage: ContractLineage,
92}
93
94impl InstallationTarget {
95 #[must_use]
97 pub const fn new(
98 database: DatabaseAlias,
99 environment: Environment,
100 lineage: ContractLineage,
101 ) -> Self {
102 Self {
103 database,
104 environment,
105 lineage,
106 }
107 }
108
109 #[must_use]
111 pub const fn database(&self) -> &DatabaseAlias {
112 &self.database
113 }
114
115 #[must_use]
117 pub const fn environment(&self) -> &Environment {
118 &self.environment
119 }
120
121 #[must_use]
123 pub const fn lineage(&self) -> &ContractLineage {
124 &self.lineage
125 }
126}
127
128#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
130pub struct InstallationContract {
131 version: ContractVersion,
132 bundle_hash: ContractBundleHash,
133}
134
135impl InstallationContract {
136 #[must_use]
138 pub const fn new(version: ContractVersion, bundle_hash: ContractBundleHash) -> Self {
139 Self {
140 version,
141 bundle_hash,
142 }
143 }
144
145 #[must_use]
147 pub const fn version(self) -> ContractVersion {
148 self.version
149 }
150
151 #[must_use]
153 pub const fn bundle_hash(self) -> ContractBundleHash {
154 self.bundle_hash
155 }
156}
157
158#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
160pub enum InstallationArtifactKind {
161 Manifest,
163 ContractBundle,
165 QueryModule,
167 ReactiveModule,
169 MigrationBundle,
171 Rust,
173 TypeScript,
175 Go,
177 Python,
179 Mcp,
181 SeedInput,
183 AdapterManifest,
185}
186
187impl InstallationArtifactKind {
188 #[doc(hidden)]
189 pub const fn tag(self) -> &'static str {
190 match self {
191 Self::Manifest => "manifest",
192 Self::ContractBundle => "contract_bundle",
193 Self::QueryModule => "query_module",
194 Self::ReactiveModule => "reactive_module",
195 Self::MigrationBundle => "migration_bundle",
196 Self::Rust => "rust",
197 Self::TypeScript => "typescript",
198 Self::Go => "go",
199 Self::Python => "python",
200 Self::Mcp => "mcp",
201 Self::SeedInput => "seed_input",
202 Self::AdapterManifest => "adapter_manifest",
203 }
204 }
205
206 #[doc(hidden)]
207 pub fn parse(value: &str) -> Option<Self> {
208 Some(match value {
209 "manifest" => Self::Manifest,
210 "contract_bundle" => Self::ContractBundle,
211 "query_module" => Self::QueryModule,
212 "reactive_module" => Self::ReactiveModule,
213 "migration_bundle" => Self::MigrationBundle,
214 "rust" => Self::Rust,
215 "typescript" => Self::TypeScript,
216 "go" => Self::Go,
217 "python" => Self::Python,
218 "mcp" => Self::Mcp,
219 "seed_input" => Self::SeedInput,
220 "adapter_manifest" => Self::AdapterManifest,
221 _ => return None,
222 })
223 }
224}
225
226#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
228pub struct InstallationArtifact {
229 kind: InstallationArtifactKind,
230 name: InstallationSymbol,
231 content_hash: GeneratedArtifactHash,
232}
233
234impl InstallationArtifact {
235 #[must_use]
237 pub const fn new(
238 kind: InstallationArtifactKind,
239 name: InstallationSymbol,
240 content_hash: GeneratedArtifactHash,
241 ) -> Self {
242 Self {
243 kind,
244 name,
245 content_hash,
246 }
247 }
248
249 #[must_use]
251 pub const fn kind(&self) -> InstallationArtifactKind {
252 self.kind
253 }
254
255 #[must_use]
257 pub const fn name(&self) -> &InstallationSymbol {
258 &self.name
259 }
260
261 #[must_use]
263 pub const fn content_hash(&self) -> GeneratedArtifactHash {
264 self.content_hash
265 }
266}
267
268#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
270pub enum RoleOperationKind {
271 Query,
273 Command,
275 EventStream,
277 QueryWatch,
279 AgentSubscription,
281}
282
283impl RoleOperationKind {
284 #[doc(hidden)]
285 pub const fn tag(self) -> &'static str {
286 match self {
287 Self::Query => "query",
288 Self::Command => "command",
289 Self::EventStream => "event_stream",
290 Self::QueryWatch => "query_watch",
291 Self::AgentSubscription => "agent_subscription",
292 }
293 }
294
295 #[doc(hidden)]
296 pub fn parse(value: &str) -> Option<Self> {
297 Some(match value {
298 "query" => Self::Query,
299 "command" => Self::Command,
300 "event_stream" => Self::EventStream,
301 "query_watch" => Self::QueryWatch,
302 "agent_subscription" => Self::AgentSubscription,
303 _ => return None,
304 })
305 }
306}
307
308#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
310pub struct RoleOperation {
311 kind: RoleOperationKind,
312 name: InstallationSymbol,
313}
314
315#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
317pub struct QuerySecretOutput {
318 query: InstallationSymbol,
319 entity: InstallationSymbol,
320 field: InstallationSymbol,
321}
322
323impl QuerySecretOutput {
324 #[must_use]
326 pub const fn new(
327 query: InstallationSymbol,
328 entity: InstallationSymbol,
329 field: InstallationSymbol,
330 ) -> Self {
331 Self {
332 query,
333 entity,
334 field,
335 }
336 }
337
338 #[must_use]
340 pub const fn query(&self) -> &InstallationSymbol {
341 &self.query
342 }
343
344 #[must_use]
346 pub const fn entity(&self) -> &InstallationSymbol {
347 &self.entity
348 }
349
350 #[must_use]
352 pub const fn field(&self) -> &InstallationSymbol {
353 &self.field
354 }
355}
356
357#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
359pub enum RoleAuthorityAtom {
360 Operation(RoleOperation),
362 QuerySecretOutput(QuerySecretOutput),
364}
365
366#[derive(Clone, Debug, Eq, PartialEq)]
368pub struct RoleAuthoritySet {
369 operations: Vec<RoleOperation>,
370 secret_outputs: Vec<QuerySecretOutput>,
371}
372
373impl RoleAuthoritySet {
374 pub fn new(
376 mut operations: Vec<RoleOperation>,
377 mut secret_outputs: Vec<QuerySecretOutput>,
378 ) -> Result<Self, InstallationPlanError> {
379 sort_unique(&mut operations)?;
380 sort_unique(&mut secret_outputs)?;
381 if operations.len() > MAX_ROLE_OPERATIONS
382 || secret_outputs.len() > MAX_ROLE_SECRET_OUTPUTS
383 || operations.len() + secret_outputs.len() > MAX_ROLE_AUTHORITY_ATOMS
384 {
385 return Err(InstallationPlanError::new(
386 InstallationPlanErrorKind::LimitExceeded,
387 ));
388 }
389 Ok(Self {
390 operations,
391 secret_outputs,
392 })
393 }
394
395 #[must_use]
397 pub fn operations(&self) -> &[RoleOperation] {
398 &self.operations
399 }
400
401 #[must_use]
403 pub fn secret_outputs(&self) -> &[QuerySecretOutput] {
404 &self.secret_outputs
405 }
406
407 #[must_use]
409 pub fn atoms(&self) -> Vec<RoleAuthorityAtom> {
410 self.operations
411 .iter()
412 .cloned()
413 .map(RoleAuthorityAtom::Operation)
414 .chain(
415 self.secret_outputs
416 .iter()
417 .cloned()
418 .map(RoleAuthorityAtom::QuerySecretOutput),
419 )
420 .collect()
421 }
422
423 const fn is_empty(&self) -> bool {
424 self.operations.is_empty() && self.secret_outputs.is_empty()
425 }
426}
427
428impl RoleOperation {
429 #[must_use]
431 pub const fn new(kind: RoleOperationKind, name: InstallationSymbol) -> Self {
432 Self { kind, name }
433 }
434
435 #[must_use]
437 pub const fn kind(&self) -> RoleOperationKind {
438 self.kind
439 }
440
441 #[must_use]
443 pub const fn name(&self) -> &InstallationSymbol {
444 &self.name
445 }
446}
447
448#[derive(Clone, Debug, Eq, PartialEq)]
450pub struct RoleWideningApproval {
451 expected_previous_role: ApplicationRoleHash,
452 additions: Vec<RoleAuthorityAtom>,
453}
454
455impl RoleWideningApproval {
456 pub fn new(
458 expected_previous_role: ApplicationRoleHash,
459 mut additions: Vec<RoleOperation>,
460 ) -> Result<Self, InstallationPlanError> {
461 sort_unique(&mut additions)?;
462 if additions.is_empty() || additions.len() > MAX_ROLE_OPERATIONS {
463 return Err(InstallationPlanError::new(
464 InstallationPlanErrorKind::InvalidApproval,
465 ));
466 }
467 Ok(Self {
468 expected_previous_role,
469 additions: additions
470 .into_iter()
471 .map(RoleAuthorityAtom::Operation)
472 .collect(),
473 })
474 }
475
476 pub fn new_authority(
478 expected_previous_role: ApplicationRoleHash,
479 additions: Vec<RoleAuthorityAtom>,
480 ) -> Result<Self, InstallationPlanError> {
481 let operation_count = additions
482 .iter()
483 .filter(|atom| matches!(atom, RoleAuthorityAtom::Operation(_)))
484 .count();
485 let secret_output_count = additions.len() - operation_count;
486 if additions.is_empty()
487 || operation_count > MAX_ROLE_OPERATIONS
488 || secret_output_count > MAX_ROLE_SECRET_OUTPUTS
489 || additions.len() > MAX_ROLE_AUTHORITY_ATOMS
490 || additions.windows(2).any(|pair| pair[0] >= pair[1])
491 {
492 return Err(InstallationPlanError::new(
493 InstallationPlanErrorKind::InvalidApproval,
494 ));
495 }
496 Ok(Self {
497 expected_previous_role,
498 additions,
499 })
500 }
501
502 #[must_use]
504 pub const fn expected_previous_role(&self) -> ApplicationRoleHash {
505 self.expected_previous_role
506 }
507
508 #[must_use]
510 pub fn additions(&self) -> &[RoleAuthorityAtom] {
511 &self.additions
512 }
513}
514
515#[derive(Clone, Debug, Eq, PartialEq)]
517pub struct InstallationRole {
518 name: InstallationSymbol,
519 role_hash: ApplicationRoleHash,
520 previous_role_hash: Option<ApplicationRoleHash>,
521 desired_authority: RoleAuthoritySet,
522 previous_authority: RoleAuthoritySet,
523 widening_approval: Option<RoleWideningApproval>,
524}
525
526impl InstallationRole {
527 pub fn new(
529 name: InstallationSymbol,
530 role_hash: ApplicationRoleHash,
531 previous_role_hash: Option<ApplicationRoleHash>,
532 desired_operations: Vec<RoleOperation>,
533 previous_operations: Vec<RoleOperation>,
534 widening_approval: Option<RoleWideningApproval>,
535 ) -> Result<Self, InstallationPlanError> {
536 Self::new_with_authority(
537 name,
538 role_hash,
539 previous_role_hash,
540 RoleAuthoritySet::new(desired_operations, vec![])?,
541 RoleAuthoritySet::new(previous_operations, vec![])?,
542 widening_approval,
543 )
544 }
545
546 pub fn new_with_authority(
548 name: InstallationSymbol,
549 role_hash: ApplicationRoleHash,
550 previous_role_hash: Option<ApplicationRoleHash>,
551 desired_authority: RoleAuthoritySet,
552 previous_authority: RoleAuthoritySet,
553 widening_approval: Option<RoleWideningApproval>,
554 ) -> Result<Self, InstallationPlanError> {
555 if desired_authority.is_empty()
556 || (previous_role_hash.is_none() && !previous_authority.is_empty())
557 {
558 return Err(InstallationPlanError::new(
559 InstallationPlanErrorKind::InvalidShape,
560 ));
561 }
562 let desired_atoms = desired_authority.atoms();
563 let previous_atoms = previous_authority.atoms();
564 let additions = desired_atoms
565 .iter()
566 .filter(|atom| previous_atoms.binary_search(atom).is_err())
567 .cloned()
568 .collect::<Vec<_>>();
569 match (
570 previous_role_hash,
571 additions.is_empty(),
572 widening_approval.as_ref(),
573 ) {
574 (None, _, None) | (Some(_), true, None) => {}
575 (Some(previous), false, Some(approval))
576 if approval.expected_previous_role == previous
577 && approval.additions == additions => {}
578 (Some(_), false, None) => {
579 return Err(InstallationPlanError::new(
580 InstallationPlanErrorKind::ApprovalRequired,
581 ));
582 }
583 _ => {
584 return Err(InstallationPlanError::new(
585 InstallationPlanErrorKind::InvalidApproval,
586 ));
587 }
588 }
589 Ok(Self {
590 name,
591 role_hash,
592 previous_role_hash,
593 desired_authority,
594 previous_authority,
595 widening_approval,
596 })
597 }
598
599 #[must_use]
601 pub const fn name(&self) -> &InstallationSymbol {
602 &self.name
603 }
604
605 #[must_use]
607 pub const fn role_hash(&self) -> ApplicationRoleHash {
608 self.role_hash
609 }
610
611 #[must_use]
613 pub const fn previous_role_hash(&self) -> Option<ApplicationRoleHash> {
614 self.previous_role_hash
615 }
616
617 #[must_use]
619 pub fn desired_operations(&self) -> &[RoleOperation] {
620 self.desired_authority.operations()
621 }
622
623 #[must_use]
625 pub fn previous_operations(&self) -> &[RoleOperation] {
626 self.previous_authority.operations()
627 }
628
629 #[must_use]
631 pub fn desired_secret_outputs(&self) -> &[QuerySecretOutput] {
632 self.desired_authority.secret_outputs()
633 }
634
635 #[must_use]
637 pub fn previous_secret_outputs(&self) -> &[QuerySecretOutput] {
638 self.previous_authority.secret_outputs()
639 }
640
641 #[must_use]
643 pub const fn desired_authority(&self) -> &RoleAuthoritySet {
644 &self.desired_authority
645 }
646
647 #[must_use]
649 pub const fn previous_authority(&self) -> &RoleAuthoritySet {
650 &self.previous_authority
651 }
652
653 #[must_use]
655 pub const fn widening_approval(&self) -> Option<&RoleWideningApproval> {
656 self.widening_approval.as_ref()
657 }
658}
659
660#[derive(Clone, Debug, Eq, PartialEq)]
662pub struct CredentialDestination {
663 name: InstallationSymbol,
664 role: InstallationSymbol,
665 expected_current: Option<CapabilityId>,
666 successor: CapabilityId,
667}
668
669impl CredentialDestination {
670 pub fn new(
672 name: InstallationSymbol,
673 role: InstallationSymbol,
674 expected_current: Option<CapabilityId>,
675 successor: CapabilityId,
676 ) -> Result<Self, InstallationPlanError> {
677 if expected_current == Some(successor) {
678 return Err(InstallationPlanError::new(
679 InstallationPlanErrorKind::CredentialConflict,
680 ));
681 }
682 Ok(Self {
683 name,
684 role,
685 expected_current,
686 successor,
687 })
688 }
689
690 #[must_use]
692 pub const fn name(&self) -> &InstallationSymbol {
693 &self.name
694 }
695
696 #[must_use]
698 pub const fn role(&self) -> &InstallationSymbol {
699 &self.role
700 }
701
702 #[must_use]
704 pub const fn expected_current(&self) -> Option<CapabilityId> {
705 self.expected_current
706 }
707
708 #[must_use]
710 pub const fn successor(&self) -> CapabilityId {
711 self.successor
712 }
713}
714
715#[derive(Clone, Copy, Debug, Eq, PartialEq)]
717pub enum MigrationBackupPolicy {
718 RequiredVerified,
720}
721
722#[derive(Clone, Copy, Debug, Eq, PartialEq)]
724pub enum MigrationDowntimeClass {
725 OfflineExclusive,
727}
728
729#[derive(Clone, Copy, Debug, Eq, PartialEq)]
731pub struct InstallationMigration {
732 parent_version: ContractVersion,
733 parent_bundle_hash: ContractBundleHash,
734 successor_version: ContractVersion,
735 successor_bundle_hash: ContractBundleHash,
736 migration_hash: MigrationBundleHash,
737 confirmation: MigrationBundleHash,
738 backup_policy: MigrationBackupPolicy,
739 downtime: MigrationDowntimeClass,
740}
741
742impl InstallationMigration {
743 pub fn new(
745 parent_version: ContractVersion,
746 parent_bundle_hash: ContractBundleHash,
747 successor_version: ContractVersion,
748 successor_bundle_hash: ContractBundleHash,
749 migration_hash: MigrationBundleHash,
750 confirmation: MigrationBundleHash,
751 ) -> Result<Self, InstallationPlanError> {
752 if parent_version >= successor_version || migration_hash != confirmation {
753 return Err(InstallationPlanError::new(
754 InstallationPlanErrorKind::UnsafeMigration,
755 ));
756 }
757 Ok(Self {
758 parent_version,
759 parent_bundle_hash,
760 successor_version,
761 successor_bundle_hash,
762 migration_hash,
763 confirmation,
764 backup_policy: MigrationBackupPolicy::RequiredVerified,
765 downtime: MigrationDowntimeClass::OfflineExclusive,
766 })
767 }
768
769 #[must_use]
771 pub const fn parent_version(self) -> ContractVersion {
772 self.parent_version
773 }
774
775 #[must_use]
777 pub const fn parent_bundle_hash(self) -> ContractBundleHash {
778 self.parent_bundle_hash
779 }
780
781 #[must_use]
783 pub const fn successor_version(self) -> ContractVersion {
784 self.successor_version
785 }
786
787 #[must_use]
789 pub const fn successor_bundle_hash(self) -> ContractBundleHash {
790 self.successor_bundle_hash
791 }
792
793 #[must_use]
795 pub const fn migration_hash(self) -> MigrationBundleHash {
796 self.migration_hash
797 }
798
799 #[must_use]
801 pub const fn confirmation(self) -> MigrationBundleHash {
802 self.confirmation
803 }
804
805 #[must_use]
807 pub const fn backup_policy(self) -> MigrationBackupPolicy {
808 self.backup_policy
809 }
810
811 #[must_use]
813 pub const fn downtime(self) -> MigrationDowntimeClass {
814 self.downtime
815 }
816}
817
818#[derive(Clone, Copy, Debug, Eq, PartialEq)]
820pub struct InstallationReimport {
821 export_manifest_hash: ApplicationExportManifestHash,
822 export_receipt_hash: ApplicationExportReceiptHash,
823 portability_manifest_hash: ApplicationPortabilityManifestHash,
824}
825
826impl InstallationReimport {
827 #[must_use]
829 pub const fn new(
830 export_manifest_hash: ApplicationExportManifestHash,
831 export_receipt_hash: ApplicationExportReceiptHash,
832 portability_manifest_hash: ApplicationPortabilityManifestHash,
833 ) -> Self {
834 Self {
835 export_manifest_hash,
836 export_receipt_hash,
837 portability_manifest_hash,
838 }
839 }
840
841 #[must_use]
843 pub const fn export_manifest_hash(self) -> ApplicationExportManifestHash {
844 self.export_manifest_hash
845 }
846
847 #[must_use]
849 pub const fn export_receipt_hash(self) -> ApplicationExportReceiptHash {
850 self.export_receipt_hash
851 }
852
853 #[must_use]
855 pub const fn portability_manifest_hash(self) -> ApplicationPortabilityManifestHash {
856 self.portability_manifest_hash
857 }
858}
859
860#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
862pub enum InstallationDriver {
863 Rust,
865 TypeScript,
867 Go,
869 Python,
871}
872
873impl InstallationDriver {
874 #[doc(hidden)]
875 pub const fn tag(self) -> &'static str {
876 match self {
877 Self::Rust => "rust",
878 Self::TypeScript => "typescript",
879 Self::Go => "go",
880 Self::Python => "python",
881 }
882 }
883
884 #[doc(hidden)]
885 pub fn parse(value: &str) -> Option<Self> {
886 Some(match value {
887 "rust" => Self::Rust,
888 "typescript" => Self::TypeScript,
889 "go" => Self::Go,
890 "python" => Self::Python,
891 _ => return None,
892 })
893 }
894}
895
896#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
898pub enum InstallationFeature {
899 RemoteTls,
901 BulkCommands,
903 OperationalQueries,
905 WorkflowConcurrency,
907 RowPolicies,
909 InstallationCampaigns,
911 DataLifecycle,
913 QuerySecretOutputs,
915}
916
917impl InstallationFeature {
918 pub const ALL: [Self; 8] = [
920 Self::RemoteTls,
921 Self::BulkCommands,
922 Self::OperationalQueries,
923 Self::WorkflowConcurrency,
924 Self::RowPolicies,
925 Self::InstallationCampaigns,
926 Self::DataLifecycle,
927 Self::QuerySecretOutputs,
928 ];
929
930 pub const ADAPTER_CONFORMANCE: [Self; 7] = [
932 Self::RemoteTls,
933 Self::BulkCommands,
934 Self::OperationalQueries,
935 Self::WorkflowConcurrency,
936 Self::RowPolicies,
937 Self::InstallationCampaigns,
938 Self::DataLifecycle,
939 ];
940
941 #[doc(hidden)]
942 pub const fn tag(self) -> &'static str {
943 match self {
944 Self::RemoteTls => "remote_tls",
945 Self::BulkCommands => "bulk_commands",
946 Self::OperationalQueries => "operational_queries",
947 Self::WorkflowConcurrency => "workflow_concurrency",
948 Self::RowPolicies => "row_policies",
949 Self::InstallationCampaigns => "installation_campaigns",
950 Self::DataLifecycle => "data_lifecycle",
951 Self::QuerySecretOutputs => "query_secret_outputs",
952 }
953 }
954
955 #[doc(hidden)]
956 pub fn parse(value: &str) -> Option<Self> {
957 Some(match value {
958 "remote_tls" => Self::RemoteTls,
959 "bulk_commands" => Self::BulkCommands,
960 "operational_queries" => Self::OperationalQueries,
961 "workflow_concurrency" => Self::WorkflowConcurrency,
962 "row_policies" => Self::RowPolicies,
963 "installation_campaigns" => Self::InstallationCampaigns,
964 "data_lifecycle" => Self::DataLifecycle,
965 "query_secret_outputs" => Self::QuerySecretOutputs,
966 _ => return None,
967 })
968 }
969}
970
971#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
973pub struct InstallationSeed {
974 name: InstallationSymbol,
975 content_hash: GeneratedArtifactHash,
976 item_count: u64,
977}
978
979#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
981pub struct InstalledSeedEvidence {
982 name: InstallationSymbol,
983 content_hash: GeneratedArtifactHash,
984 succeeded: u64,
985 replayed: u64,
986}
987
988impl InstalledSeedEvidence {
989 #[must_use]
991 pub const fn new(
992 name: InstallationSymbol,
993 content_hash: GeneratedArtifactHash,
994 succeeded: u64,
995 replayed: u64,
996 ) -> Self {
997 Self {
998 name,
999 content_hash,
1000 succeeded,
1001 replayed,
1002 }
1003 }
1004
1005 #[must_use]
1007 pub const fn name(&self) -> &InstallationSymbol {
1008 &self.name
1009 }
1010
1011 #[must_use]
1013 pub const fn content_hash(&self) -> GeneratedArtifactHash {
1014 self.content_hash
1015 }
1016
1017 #[must_use]
1019 pub const fn succeeded(&self) -> u64 {
1020 self.succeeded
1021 }
1022
1023 #[must_use]
1025 pub const fn replayed(&self) -> u64 {
1026 self.replayed
1027 }
1028}
1029
1030impl InstallationSeed {
1031 pub fn new(
1033 name: InstallationSymbol,
1034 content_hash: GeneratedArtifactHash,
1035 item_count: u64,
1036 ) -> Result<Self, InstallationPlanError> {
1037 if item_count == 0 || item_count > MAX_SEED_BATCH_ITEMS {
1038 return Err(InstallationPlanError::new(
1039 InstallationPlanErrorKind::LimitExceeded,
1040 ));
1041 }
1042 Ok(Self {
1043 name,
1044 content_hash,
1045 item_count,
1046 })
1047 }
1048
1049 #[must_use]
1051 pub const fn name(&self) -> &InstallationSymbol {
1052 &self.name
1053 }
1054
1055 #[must_use]
1057 pub const fn content_hash(&self) -> GeneratedArtifactHash {
1058 self.content_hash
1059 }
1060
1061 #[must_use]
1063 pub const fn item_count(&self) -> u64 {
1064 self.item_count
1065 }
1066}
1067
1068#[derive(Clone, Debug, Eq, PartialEq)]
1070pub struct ApplicationInstallationPlanInput {
1071 pub application: InstallationSymbol,
1073 pub source_hash: ApplicationSourceHash,
1075 pub lock_hash: ApplicationLockHash,
1077 pub manifest_hash: ApplicationManifestHash,
1079 pub target: InstallationTarget,
1081 pub contract: InstallationContract,
1083 pub artifacts: Vec<InstallationArtifact>,
1085 pub migration: Option<InstallationMigration>,
1087 pub roles: Vec<InstallationRole>,
1089 pub reimport: Option<InstallationReimport>,
1091 pub credential_destinations: Vec<CredentialDestination>,
1093 pub drivers: Vec<InstallationDriver>,
1095 pub seeds: Vec<InstallationSeed>,
1097 pub required_features: Vec<InstallationFeature>,
1099 pub adapter_manifest_hash: Option<AdapterConformanceManifestHash>,
1101}
1102
1103#[derive(Clone, Eq, PartialEq)]
1105pub struct ApplicationInstallationPlan {
1106 schema: ApplicationInstallationPlanSchema,
1107 input: ApplicationInstallationPlanInput,
1108 identity: ApplicationInstallationPlanHash,
1109 canonical_bytes: Vec<u8>,
1110}
1111
1112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1113enum ApplicationInstallationPlanSchema {
1114 V1,
1115 V2,
1116 V3,
1117}
1118
1119impl ApplicationInstallationPlanSchema {
1120 fn for_input(input: &ApplicationInstallationPlanInput) -> Self {
1121 if has_secret_output_authority(input) {
1122 Self::V3
1123 } else if input.reimport.is_some() {
1124 Self::V2
1125 } else {
1126 Self::V1
1127 }
1128 }
1129
1130 const fn name(self) -> &'static str {
1131 match self {
1132 Self::V1 => APPLICATION_INSTALLATION_PLAN_SCHEMA_V1,
1133 Self::V2 => APPLICATION_INSTALLATION_PLAN_SCHEMA_V2,
1134 Self::V3 => APPLICATION_INSTALLATION_PLAN_SCHEMA_V3,
1135 }
1136 }
1137}
1138
1139impl ApplicationInstallationPlan {
1140 pub fn compile(
1142 mut input: ApplicationInstallationPlanInput,
1143 ) -> Result<Self, InstallationPlanError> {
1144 validate_and_sort(&mut input)?;
1145 let schema = ApplicationInstallationPlanSchema::for_input(&input);
1146 let dto = PlanDto::from_input(&input, schema);
1147 let mut canonical_bytes = serde_json::to_vec(&dto)
1148 .map_err(|_| InstallationPlanError::new(InstallationPlanErrorKind::InvalidEncoding))?;
1149 canonical_bytes.push(b'\n');
1150 if canonical_bytes.len() > MAX_INSTALLATION_PLAN_BYTES {
1151 return Err(InstallationPlanError::new(
1152 InstallationPlanErrorKind::LimitExceeded,
1153 ));
1154 }
1155 let identity = hash_application_installation_plan(&canonical_bytes);
1156 Ok(Self {
1157 schema,
1158 input,
1159 identity,
1160 canonical_bytes,
1161 })
1162 }
1163
1164 pub fn decode_canonical(bytes: &[u8]) -> Result<Self, InstallationPlanError> {
1166 if bytes.is_empty() || bytes.len() > MAX_INSTALLATION_PLAN_BYTES {
1167 return Err(InstallationPlanError::new(
1168 InstallationPlanErrorKind::LimitExceeded,
1169 ));
1170 }
1171 let dto: PlanDto = serde_json::from_slice(bytes)
1172 .map_err(|_| InstallationPlanError::new(InstallationPlanErrorKind::InvalidEncoding))?;
1173 let schema = dto.schema()?;
1174 let plan = Self::compile(dto.into_input(schema)?)?;
1175 if plan.schema != schema {
1176 return Err(InstallationPlanError::new(
1177 InstallationPlanErrorKind::InvalidShape,
1178 ));
1179 }
1180 if plan.canonical_bytes != bytes {
1181 return Err(InstallationPlanError::new(
1182 InstallationPlanErrorKind::NonCanonical,
1183 ));
1184 }
1185 Ok(plan)
1186 }
1187
1188 #[must_use]
1190 pub const fn identity(&self) -> ApplicationInstallationPlanHash {
1191 self.identity
1192 }
1193
1194 #[must_use]
1196 pub const fn input(&self) -> &ApplicationInstallationPlanInput {
1197 &self.input
1198 }
1199
1200 #[must_use]
1202 pub fn canonical_bytes(&self) -> &[u8] {
1203 &self.canonical_bytes
1204 }
1205
1206 #[must_use]
1208 pub const fn schema(&self) -> &'static str {
1209 self.schema.name()
1210 }
1211
1212 pub fn validate_driver_proof(
1214 &self,
1215 drivers: &[InstallationDriver],
1216 ) -> Result<(), InstallationCampaignError> {
1217 if drivers == self.input.drivers {
1218 Ok(())
1219 } else {
1220 Err(InstallationCampaignError::new(
1221 InstallationCampaignErrorKind::EvidenceMismatch,
1222 ))
1223 }
1224 }
1225
1226 pub fn validate_seed_receipts(
1228 &self,
1229 seeds: &[InstalledSeedEvidence],
1230 ) -> Result<(), InstallationCampaignError> {
1231 let valid = seeds.len() == self.input.seeds.len()
1232 && seeds
1233 .iter()
1234 .zip(&self.input.seeds)
1235 .all(|(observed, expected)| {
1236 observed.name() == expected.name()
1237 && observed.content_hash() == expected.content_hash()
1238 && observed.succeeded().checked_add(observed.replayed())
1239 == Some(expected.item_count())
1240 });
1241 if valid {
1242 Ok(())
1243 } else {
1244 Err(InstallationCampaignError::new(
1245 InstallationCampaignErrorKind::EvidenceMismatch,
1246 ))
1247 }
1248 }
1249}
1250
1251impl fmt::Debug for ApplicationInstallationPlan {
1252 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1253 formatter
1254 .debug_struct("ApplicationInstallationPlan")
1255 .field("identity", &self.identity)
1256 .field("application", &self.input.application)
1257 .field("target", &self.input.target)
1258 .field("artifacts", &self.input.artifacts.len())
1259 .field("roles", &self.input.roles.len())
1260 .field("credentials", &self.input.credential_destinations.len())
1261 .field("seeds", &self.input.seeds.len())
1262 .finish_non_exhaustive()
1263 }
1264}
1265
1266#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1268pub enum InstallationPlanErrorKind {
1269 InvalidIdentifier,
1271 LimitExceeded,
1273 Duplicate,
1275 IdentityMismatch,
1277 ApprovalRequired,
1279 InvalidApproval,
1281 UnsafeMigration,
1283 CredentialConflict,
1285 UnsupportedVersion,
1287 InvalidEncoding,
1289 NonCanonical,
1291 InvalidShape,
1293}
1294
1295#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1297pub struct InstallationPlanError {
1298 kind: InstallationPlanErrorKind,
1299}
1300
1301impl InstallationPlanError {
1302 const fn new(kind: InstallationPlanErrorKind) -> Self {
1303 Self { kind }
1304 }
1305
1306 #[must_use]
1308 pub const fn kind(self) -> InstallationPlanErrorKind {
1309 self.kind
1310 }
1311
1312 #[doc(hidden)]
1313 pub const fn from_kind_for_diff(kind: InstallationPlanErrorKind) -> Self {
1314 Self::new(kind)
1315 }
1316}
1317
1318impl fmt::Display for InstallationPlanError {
1319 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1320 formatter.write_str(match self.kind {
1321 InstallationPlanErrorKind::InvalidIdentifier => {
1322 "application installation symbol is invalid"
1323 }
1324 InstallationPlanErrorKind::LimitExceeded => {
1325 "application installation input exceeds a hard bound"
1326 }
1327 InstallationPlanErrorKind::Duplicate => {
1328 "application installation input contains a duplicate symbolic key"
1329 }
1330 InstallationPlanErrorKind::IdentityMismatch => {
1331 "application installation exact identities disagree"
1332 }
1333 InstallationPlanErrorKind::ApprovalRequired => {
1334 "application role widening requires explicit exact approval"
1335 }
1336 InstallationPlanErrorKind::InvalidApproval => {
1337 "application role widening approval is stale or inexact"
1338 }
1339 InstallationPlanErrorKind::UnsafeMigration => {
1340 "application migration gate is absent, stale, or unsafe"
1341 }
1342 InstallationPlanErrorKind::CredentialConflict => {
1343 "application credential destination cannot be replaced safely"
1344 }
1345 InstallationPlanErrorKind::UnsupportedVersion => {
1346 "application installation plan version is unsupported"
1347 }
1348 InstallationPlanErrorKind::InvalidEncoding => {
1349 "application installation plan encoding is invalid"
1350 }
1351 InstallationPlanErrorKind::NonCanonical => {
1352 "application installation plan encoding is not canonical"
1353 }
1354 InstallationPlanErrorKind::InvalidShape => {
1355 "application installation plan shape is invalid"
1356 }
1357 })
1358 }
1359}
1360
1361impl Error for InstallationPlanError {}
1362
1363#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1365pub enum InstallationCampaignErrorKind {
1366 CampaignIdentityMismatch,
1368 PlanIdentityMismatch,
1370 StageOutOfOrder,
1372 EvidenceMismatch,
1374 AlreadyTerminal,
1376 LimitExceeded,
1378 InvalidEncoding,
1380 NonCanonical,
1382 UnsupportedVersion,
1384}
1385
1386#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1388pub struct InstallationCampaignError {
1389 kind: InstallationCampaignErrorKind,
1390}
1391
1392impl InstallationCampaignError {
1393 #[doc(hidden)]
1395 #[must_use]
1396 pub const fn new(kind: InstallationCampaignErrorKind) -> Self {
1397 Self { kind }
1398 }
1399
1400 #[doc(hidden)]
1402 #[must_use]
1403 pub fn from_plan_error(error: InstallationPlanError) -> Self {
1404 match error.kind() {
1405 InstallationPlanErrorKind::LimitExceeded => {
1406 Self::new(InstallationCampaignErrorKind::LimitExceeded)
1407 }
1408 InstallationPlanErrorKind::UnsupportedVersion => {
1409 Self::new(InstallationCampaignErrorKind::UnsupportedVersion)
1410 }
1411 InstallationPlanErrorKind::NonCanonical => {
1412 Self::new(InstallationCampaignErrorKind::NonCanonical)
1413 }
1414 _ => Self::new(InstallationCampaignErrorKind::InvalidEncoding),
1415 }
1416 }
1417
1418 #[must_use]
1420 pub const fn kind(self) -> InstallationCampaignErrorKind {
1421 self.kind
1422 }
1423}
1424
1425impl fmt::Display for InstallationCampaignError {
1426 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1427 formatter.write_str(match self.kind {
1428 InstallationCampaignErrorKind::CampaignIdentityMismatch => {
1429 "application installation campaign identity does not match retained state"
1430 }
1431 InstallationCampaignErrorKind::PlanIdentityMismatch => {
1432 "application installation campaign identity is bound to another exact plan"
1433 }
1434 InstallationCampaignErrorKind::StageOutOfOrder => {
1435 "application installation stage is not the exact next stage"
1436 }
1437 InstallationCampaignErrorKind::EvidenceMismatch => {
1438 "application installation stage evidence differs from the exact plan"
1439 }
1440 InstallationCampaignErrorKind::AlreadyTerminal => {
1441 "application installation campaign already has a terminal receipt"
1442 }
1443 InstallationCampaignErrorKind::LimitExceeded => {
1444 "application installation campaign artifact exceeds a hard bound"
1445 }
1446 InstallationCampaignErrorKind::InvalidEncoding => {
1447 "application installation campaign artifact encoding is invalid"
1448 }
1449 InstallationCampaignErrorKind::NonCanonical => {
1450 "application installation campaign artifact encoding is not canonical"
1451 }
1452 InstallationCampaignErrorKind::UnsupportedVersion => {
1453 "application installation campaign artifact version is unsupported"
1454 }
1455 })
1456 }
1457}
1458
1459impl Error for InstallationCampaignError {}
1460
1461fn validate_and_sort(
1462 input: &mut ApplicationInstallationPlanInput,
1463) -> Result<(), InstallationPlanError> {
1464 if input.artifacts.is_empty()
1465 || input.artifacts.len() > MAX_INSTALLATION_ARTIFACTS
1466 || input.roles.is_empty()
1467 || input.roles.len() > MAX_INSTALLATION_ROLES
1468 || input.credential_destinations.len() > MAX_CREDENTIAL_DESTINATIONS
1469 || input.seeds.len() > MAX_INSTALLATION_SEEDS
1470 {
1471 return Err(InstallationPlanError::new(
1472 InstallationPlanErrorKind::LimitExceeded,
1473 ));
1474 }
1475 input.artifacts.sort();
1476 if input
1477 .artifacts
1478 .windows(2)
1479 .any(|pair| (pair[0].kind, &pair[0].name) == (pair[1].kind, &pair[1].name))
1480 {
1481 return Err(InstallationPlanError::new(
1482 InstallationPlanErrorKind::Duplicate,
1483 ));
1484 }
1485 if !input
1486 .artifacts
1487 .iter()
1488 .any(|artifact| artifact.kind == InstallationArtifactKind::Manifest)
1489 || !input
1490 .artifacts
1491 .iter()
1492 .any(|artifact| artifact.kind == InstallationArtifactKind::ContractBundle)
1493 {
1494 return Err(InstallationPlanError::new(
1495 InstallationPlanErrorKind::InvalidShape,
1496 ));
1497 }
1498 input
1499 .roles
1500 .sort_by(|left, right| left.name.cmp(&right.name));
1501 ensure_unique_by(input.roles.iter().map(|role| &role.name))?;
1502 input
1503 .credential_destinations
1504 .sort_by(|left, right| left.name.cmp(&right.name));
1505 ensure_unique_by(
1506 input
1507 .credential_destinations
1508 .iter()
1509 .map(|destination| &destination.name),
1510 )?;
1511 for destination in &input.credential_destinations {
1512 let role = input
1513 .roles
1514 .binary_search_by(|role| role.name.cmp(&destination.role))
1515 .ok()
1516 .map(|index| &input.roles[index])
1517 .ok_or_else(|| {
1518 InstallationPlanError::new(InstallationPlanErrorKind::IdentityMismatch)
1519 })?;
1520 if role.previous_role_hash().is_some() != destination.expected_current().is_some() {
1521 return Err(InstallationPlanError::new(
1522 InstallationPlanErrorKind::CredentialConflict,
1523 ));
1524 }
1525 }
1526 input.seeds.sort();
1527 ensure_unique_by(input.seeds.iter().map(|seed| &seed.name))?;
1528 sort_unique(&mut input.drivers)?;
1529 let has_secret_outputs = has_secret_output_authority(input);
1530 if has_secret_outputs
1531 && !input
1532 .required_features
1533 .contains(&InstallationFeature::QuerySecretOutputs)
1534 {
1535 input
1536 .required_features
1537 .push(InstallationFeature::QuerySecretOutputs);
1538 } else if !has_secret_outputs
1539 && input
1540 .required_features
1541 .contains(&InstallationFeature::QuerySecretOutputs)
1542 {
1543 return Err(InstallationPlanError::new(
1544 InstallationPlanErrorKind::InvalidShape,
1545 ));
1546 }
1547 sort_unique(&mut input.required_features)?;
1548 if input.drivers.is_empty()
1549 || !input
1550 .required_features
1551 .contains(&InstallationFeature::InstallationCampaigns)
1552 {
1553 return Err(InstallationPlanError::new(
1554 InstallationPlanErrorKind::InvalidShape,
1555 ));
1556 }
1557 if let Some(migration) = input.migration
1558 && (migration.successor_version != input.contract.version
1559 || migration.successor_bundle_hash != input.contract.bundle_hash
1560 || migration.migration_hash != migration.confirmation
1561 || !input
1562 .artifacts
1563 .iter()
1564 .any(|artifact| artifact.kind == InstallationArtifactKind::MigrationBundle))
1565 {
1566 return Err(InstallationPlanError::new(
1567 InstallationPlanErrorKind::IdentityMismatch,
1568 ));
1569 }
1570 let adapter_artifact = input
1571 .artifacts
1572 .iter()
1573 .find(|artifact| artifact.kind == InstallationArtifactKind::AdapterManifest);
1574 if input.adapter_manifest_hash.is_some() != adapter_artifact.is_some()
1575 || input.adapter_manifest_hash.is_some_and(|manifest_hash| {
1576 adapter_artifact
1577 .is_none_or(|artifact| artifact.content_hash.as_bytes() != manifest_hash.as_bytes())
1578 })
1579 {
1580 return Err(InstallationPlanError::new(
1581 InstallationPlanErrorKind::IdentityMismatch,
1582 ));
1583 }
1584 Ok(())
1585}
1586
1587fn has_secret_output_authority(input: &ApplicationInstallationPlanInput) -> bool {
1588 input.roles.iter().any(|role| {
1589 !role.desired_secret_outputs().is_empty() || !role.previous_secret_outputs().is_empty()
1590 })
1591}
1592
1593fn sort_unique<T: Ord>(values: &mut [T]) -> Result<(), InstallationPlanError> {
1594 values.sort();
1595 if values.windows(2).any(|pair| pair[0] == pair[1]) {
1596 return Err(InstallationPlanError::new(
1597 InstallationPlanErrorKind::Duplicate,
1598 ));
1599 }
1600 Ok(())
1601}
1602
1603fn ensure_unique_by<'a, T: Ord + 'a>(
1604 values: impl Iterator<Item = &'a T>,
1605) -> Result<(), InstallationPlanError> {
1606 let mut seen = BTreeSet::new();
1607 if values.into_iter().any(|value| !seen.insert(value)) {
1608 return Err(InstallationPlanError::new(
1609 InstallationPlanErrorKind::Duplicate,
1610 ));
1611 }
1612 Ok(())
1613}
1614
1615#[derive(Serialize, Deserialize)]
1616#[serde(deny_unknown_fields)]
1617struct PlanDto {
1618 schema: String,
1619 application: String,
1620 source_hash: String,
1621 lock_hash: String,
1622 manifest_hash: String,
1623 target: TargetDto,
1624 contract: ContractDto,
1625 artifacts: Vec<ArtifactDto>,
1626 migration: Option<MigrationDto>,
1627 roles: Vec<RoleDto>,
1628 #[serde(default, skip_serializing_if = "Option::is_none")]
1629 reimport: Option<ReimportDto>,
1630 credential_destinations: Vec<CredentialDto>,
1631 drivers: Vec<String>,
1632 seeds: Vec<SeedDto>,
1633 required_features: Vec<String>,
1634 adapter_manifest_hash: Option<String>,
1635}
1636
1637#[derive(Serialize, Deserialize)]
1638#[serde(deny_unknown_fields)]
1639struct TargetDto {
1640 database: String,
1641 environment: String,
1642 lineage: String,
1643}
1644
1645#[derive(Serialize, Deserialize)]
1646#[serde(deny_unknown_fields)]
1647struct ContractDto {
1648 version: u64,
1649 bundle_hash: String,
1650}
1651
1652#[derive(Serialize, Deserialize)]
1653#[serde(deny_unknown_fields)]
1654struct ArtifactDto {
1655 kind: String,
1656 name: String,
1657 content_hash: String,
1658}
1659
1660#[derive(Serialize, Deserialize)]
1661#[serde(deny_unknown_fields)]
1662struct OperationDto {
1663 kind: String,
1664 name: String,
1665}
1666
1667#[derive(Serialize, Deserialize)]
1668#[serde(deny_unknown_fields)]
1669struct ApprovalDto {
1670 expected_previous_role: String,
1671 additions: Vec<AuthorityAtomDto>,
1672}
1673
1674#[derive(Serialize, Deserialize)]
1675#[serde(deny_unknown_fields)]
1676struct RoleDto {
1677 name: String,
1678 role_hash: String,
1679 previous_role_hash: Option<String>,
1680 #[serde(default, skip_serializing_if = "Option::is_none")]
1681 desired_operations: Option<Vec<OperationDto>>,
1682 #[serde(default, skip_serializing_if = "Option::is_none")]
1683 previous_operations: Option<Vec<OperationDto>>,
1684 #[serde(default, skip_serializing_if = "Option::is_none")]
1685 desired_authority: Option<Vec<AuthorityAtomDto>>,
1686 #[serde(default, skip_serializing_if = "Option::is_none")]
1687 previous_authority: Option<Vec<AuthorityAtomDto>>,
1688 widening_approval: Option<ApprovalDto>,
1689}
1690
1691#[derive(Serialize, Deserialize)]
1692#[serde(untagged)]
1693enum AuthorityAtomDto {
1694 Operation(OperationDto),
1695 QuerySecretOutput(SecretOutputDto),
1696}
1697
1698#[derive(Serialize, Deserialize)]
1699#[serde(deny_unknown_fields)]
1700struct SecretOutputDto {
1701 query: String,
1702 entity: String,
1703 field: String,
1704}
1705
1706#[derive(Serialize, Deserialize)]
1707#[serde(deny_unknown_fields)]
1708struct CredentialDto {
1709 name: String,
1710 role: String,
1711 expected_current: Option<String>,
1712 successor: String,
1713}
1714
1715#[derive(Serialize, Deserialize)]
1716#[serde(deny_unknown_fields)]
1717struct MigrationDto {
1718 parent_version: u64,
1719 parent_bundle_hash: String,
1720 successor_version: u64,
1721 successor_bundle_hash: String,
1722 migration_hash: String,
1723 confirmation: String,
1724 backup_policy: String,
1725 downtime: String,
1726}
1727
1728#[derive(Serialize, Deserialize)]
1729#[serde(deny_unknown_fields)]
1730struct ReimportDto {
1731 export_manifest_hash: String,
1732 export_receipt_hash: String,
1733 portability_manifest_hash: String,
1734}
1735
1736#[derive(Serialize, Deserialize)]
1737#[serde(deny_unknown_fields)]
1738struct SeedDto {
1739 name: String,
1740 content_hash: String,
1741 item_count: u64,
1742}
1743
1744impl PlanDto {
1745 fn from_input(
1746 input: &ApplicationInstallationPlanInput,
1747 schema: ApplicationInstallationPlanSchema,
1748 ) -> Self {
1749 Self {
1750 schema: schema.name().to_owned(),
1751 application: input.application.as_str().to_owned(),
1752 source_hash: hex32(input.source_hash.as_bytes()),
1753 lock_hash: hex32(input.lock_hash.as_bytes()),
1754 manifest_hash: hex32(input.manifest_hash.as_bytes()),
1755 target: TargetDto {
1756 database: input.target.database.as_str().to_owned(),
1757 environment: input.target.environment.as_str().to_owned(),
1758 lineage: input.target.lineage.as_str().to_owned(),
1759 },
1760 contract: ContractDto {
1761 version: input.contract.version.get(),
1762 bundle_hash: hex32(input.contract.bundle_hash.as_bytes()),
1763 },
1764 artifacts: input
1765 .artifacts
1766 .iter()
1767 .map(|artifact| ArtifactDto {
1768 kind: artifact.kind.tag().to_owned(),
1769 name: artifact.name.as_str().to_owned(),
1770 content_hash: hex32(artifact.content_hash.as_bytes()),
1771 })
1772 .collect(),
1773 migration: input.migration.map(|migration| MigrationDto {
1774 parent_version: migration.parent_version.get(),
1775 parent_bundle_hash: hex32(migration.parent_bundle_hash.as_bytes()),
1776 successor_version: migration.successor_version.get(),
1777 successor_bundle_hash: hex32(migration.successor_bundle_hash.as_bytes()),
1778 migration_hash: hex32(migration.migration_hash.as_bytes()),
1779 confirmation: hex32(migration.confirmation.as_bytes()),
1780 backup_policy: "required_verified".to_owned(),
1781 downtime: "offline_exclusive".to_owned(),
1782 }),
1783 roles: input
1784 .roles
1785 .iter()
1786 .map(|role| RoleDto::from_role(role, schema))
1787 .collect(),
1788 reimport: input.reimport.map(|reimport| ReimportDto {
1789 export_manifest_hash: hex32(reimport.export_manifest_hash.as_bytes()),
1790 export_receipt_hash: hex32(reimport.export_receipt_hash.as_bytes()),
1791 portability_manifest_hash: hex32(reimport.portability_manifest_hash.as_bytes()),
1792 }),
1793 credential_destinations: input
1794 .credential_destinations
1795 .iter()
1796 .map(|destination| CredentialDto {
1797 name: destination.name.as_str().to_owned(),
1798 role: destination.role.as_str().to_owned(),
1799 expected_current: destination
1800 .expected_current
1801 .map(|value| hex16(value.as_bytes())),
1802 successor: hex16(destination.successor.as_bytes()),
1803 })
1804 .collect(),
1805 drivers: input
1806 .drivers
1807 .iter()
1808 .map(|driver| driver.tag().to_owned())
1809 .collect(),
1810 seeds: input
1811 .seeds
1812 .iter()
1813 .map(|seed| SeedDto {
1814 name: seed.name.as_str().to_owned(),
1815 content_hash: hex32(seed.content_hash.as_bytes()),
1816 item_count: seed.item_count,
1817 })
1818 .collect(),
1819 required_features: input
1820 .required_features
1821 .iter()
1822 .map(|feature| feature.tag().to_owned())
1823 .collect(),
1824 adapter_manifest_hash: input
1825 .adapter_manifest_hash
1826 .map(|value| hex32(value.as_bytes())),
1827 }
1828 }
1829
1830 fn schema(&self) -> Result<ApplicationInstallationPlanSchema, InstallationPlanError> {
1831 match self.schema.as_str() {
1832 APPLICATION_INSTALLATION_PLAN_SCHEMA_V1 => Ok(ApplicationInstallationPlanSchema::V1),
1833 APPLICATION_INSTALLATION_PLAN_SCHEMA_V2 => Ok(ApplicationInstallationPlanSchema::V2),
1834 APPLICATION_INSTALLATION_PLAN_SCHEMA_V3 => Ok(ApplicationInstallationPlanSchema::V3),
1835 _ => Err(InstallationPlanError::new(
1836 InstallationPlanErrorKind::UnsupportedVersion,
1837 )),
1838 }
1839 }
1840
1841 fn into_input(
1842 self,
1843 schema: ApplicationInstallationPlanSchema,
1844 ) -> Result<ApplicationInstallationPlanInput, InstallationPlanError> {
1845 if matches!(schema, ApplicationInstallationPlanSchema::V1) && self.reimport.is_some()
1846 || matches!(schema, ApplicationInstallationPlanSchema::V2) && self.reimport.is_none()
1847 {
1848 return Err(InstallationPlanError::new(
1849 InstallationPlanErrorKind::InvalidShape,
1850 ));
1851 }
1852 Ok(ApplicationInstallationPlanInput {
1853 application: InstallationSymbol::new(self.application)?,
1854 source_hash: ApplicationSourceHash::from_bytes(parse_hex32(&self.source_hash)?),
1855 lock_hash: ApplicationLockHash::from_bytes(parse_hex32(&self.lock_hash)?),
1856 manifest_hash: ApplicationManifestHash::from_bytes(parse_hex32(&self.manifest_hash)?),
1857 target: InstallationTarget::new(
1858 DatabaseAlias::new(self.target.database).map_err(|_| {
1859 InstallationPlanError::new(InstallationPlanErrorKind::InvalidIdentifier)
1860 })?,
1861 Environment::new(self.target.environment).map_err(|_| {
1862 InstallationPlanError::new(InstallationPlanErrorKind::InvalidIdentifier)
1863 })?,
1864 ContractLineage::new(self.target.lineage).map_err(|_| {
1865 InstallationPlanError::new(InstallationPlanErrorKind::InvalidIdentifier)
1866 })?,
1867 ),
1868 contract: InstallationContract::new(
1869 ContractVersion::new(self.contract.version).ok_or_else(|| {
1870 InstallationPlanError::new(InstallationPlanErrorKind::InvalidShape)
1871 })?,
1872 ContractBundleHash::from_bytes(parse_hex32(&self.contract.bundle_hash)?),
1873 ),
1874 artifacts: self
1875 .artifacts
1876 .into_iter()
1877 .map(ArtifactDto::into_artifact)
1878 .collect::<Result<Vec<_>, _>>()?,
1879 migration: self
1880 .migration
1881 .map(MigrationDto::into_migration)
1882 .transpose()?,
1883 roles: self
1884 .roles
1885 .into_iter()
1886 .map(|role| role.into_role(schema))
1887 .collect::<Result<Vec<_>, _>>()?,
1888 reimport: self.reimport.map(ReimportDto::into_reimport).transpose()?,
1889 credential_destinations: self
1890 .credential_destinations
1891 .into_iter()
1892 .map(CredentialDto::into_destination)
1893 .collect::<Result<Vec<_>, _>>()?,
1894 drivers: self
1895 .drivers
1896 .into_iter()
1897 .map(|driver| {
1898 InstallationDriver::parse(&driver).ok_or_else(|| {
1899 InstallationPlanError::new(InstallationPlanErrorKind::InvalidShape)
1900 })
1901 })
1902 .collect::<Result<Vec<_>, _>>()?,
1903 seeds: self
1904 .seeds
1905 .into_iter()
1906 .map(SeedDto::into_seed)
1907 .collect::<Result<Vec<_>, _>>()?,
1908 required_features: self
1909 .required_features
1910 .into_iter()
1911 .map(|feature| {
1912 InstallationFeature::parse(&feature).ok_or_else(|| {
1913 InstallationPlanError::new(InstallationPlanErrorKind::InvalidShape)
1914 })
1915 })
1916 .collect::<Result<Vec<_>, _>>()?,
1917 adapter_manifest_hash: self
1918 .adapter_manifest_hash
1919 .map(|hash| parse_hex32(&hash).map(AdapterConformanceManifestHash::from_bytes))
1920 .transpose()?,
1921 })
1922 }
1923}
1924
1925impl ArtifactDto {
1926 fn into_artifact(self) -> Result<InstallationArtifact, InstallationPlanError> {
1927 Ok(InstallationArtifact::new(
1928 InstallationArtifactKind::parse(&self.kind).ok_or_else(|| {
1929 InstallationPlanError::new(InstallationPlanErrorKind::InvalidShape)
1930 })?,
1931 InstallationSymbol::new(self.name)?,
1932 GeneratedArtifactHash::from_bytes(parse_hex32(&self.content_hash)?),
1933 ))
1934 }
1935}
1936
1937impl ReimportDto {
1938 fn into_reimport(self) -> Result<InstallationReimport, InstallationPlanError> {
1939 Ok(InstallationReimport::new(
1940 ApplicationExportManifestHash::from_bytes(parse_hex32(&self.export_manifest_hash)?),
1941 ApplicationExportReceiptHash::from_bytes(parse_hex32(&self.export_receipt_hash)?),
1942 ApplicationPortabilityManifestHash::from_bytes(parse_hex32(
1943 &self.portability_manifest_hash,
1944 )?),
1945 ))
1946 }
1947}
1948
1949impl OperationDto {
1950 fn from_operation(operation: &RoleOperation) -> Self {
1951 Self {
1952 kind: operation.kind.tag().to_owned(),
1953 name: operation.name.as_str().to_owned(),
1954 }
1955 }
1956
1957 fn into_operation(self) -> Result<RoleOperation, InstallationPlanError> {
1958 Ok(RoleOperation::new(
1959 RoleOperationKind::parse(&self.kind).ok_or_else(|| {
1960 InstallationPlanError::new(InstallationPlanErrorKind::InvalidShape)
1961 })?,
1962 InstallationSymbol::new(self.name)?,
1963 ))
1964 }
1965}
1966
1967impl AuthorityAtomDto {
1968 fn from_atom(atom: &RoleAuthorityAtom) -> Self {
1969 match atom {
1970 RoleAuthorityAtom::Operation(operation) => {
1971 Self::Operation(OperationDto::from_operation(operation))
1972 }
1973 RoleAuthorityAtom::QuerySecretOutput(output) => {
1974 Self::QuerySecretOutput(SecretOutputDto {
1975 query: output.query.as_str().to_owned(),
1976 entity: output.entity.as_str().to_owned(),
1977 field: output.field.as_str().to_owned(),
1978 })
1979 }
1980 }
1981 }
1982
1983 fn into_atom(self) -> Result<RoleAuthorityAtom, InstallationPlanError> {
1984 Ok(match self {
1985 Self::Operation(operation) => RoleAuthorityAtom::Operation(operation.into_operation()?),
1986 Self::QuerySecretOutput(output) => {
1987 RoleAuthorityAtom::QuerySecretOutput(QuerySecretOutput::new(
1988 InstallationSymbol::new(output.query)?,
1989 InstallationSymbol::new(output.entity)?,
1990 InstallationSymbol::new(output.field)?,
1991 ))
1992 }
1993 })
1994 }
1995}
1996
1997impl RoleDto {
1998 fn from_role(role: &InstallationRole, schema: ApplicationInstallationPlanSchema) -> Self {
1999 let uses_authority = schema == ApplicationInstallationPlanSchema::V3;
2000 Self {
2001 name: role.name.as_str().to_owned(),
2002 role_hash: hex32(role.role_hash.as_bytes()),
2003 previous_role_hash: role.previous_role_hash.map(|hash| hex32(hash.as_bytes())),
2004 desired_operations: (!uses_authority).then(|| {
2005 role.desired_operations()
2006 .iter()
2007 .map(OperationDto::from_operation)
2008 .collect()
2009 }),
2010 previous_operations: (!uses_authority).then(|| {
2011 role.previous_operations()
2012 .iter()
2013 .map(OperationDto::from_operation)
2014 .collect()
2015 }),
2016 desired_authority: uses_authority.then(|| {
2017 role.desired_authority()
2018 .atoms()
2019 .iter()
2020 .map(AuthorityAtomDto::from_atom)
2021 .collect()
2022 }),
2023 previous_authority: uses_authority.then(|| {
2024 role.previous_authority()
2025 .atoms()
2026 .iter()
2027 .map(AuthorityAtomDto::from_atom)
2028 .collect()
2029 }),
2030 widening_approval: role.widening_approval.as_ref().map(|approval| ApprovalDto {
2031 expected_previous_role: hex32(approval.expected_previous_role.as_bytes()),
2032 additions: approval
2033 .additions
2034 .iter()
2035 .map(AuthorityAtomDto::from_atom)
2036 .collect(),
2037 }),
2038 }
2039 }
2040
2041 fn into_role(
2042 self,
2043 schema: ApplicationInstallationPlanSchema,
2044 ) -> Result<InstallationRole, InstallationPlanError> {
2045 let uses_authority = schema == ApplicationInstallationPlanSchema::V3;
2046 let shape_is_exact = if uses_authority {
2047 self.desired_authority.is_some()
2048 && self.previous_authority.is_some()
2049 && self.desired_operations.is_none()
2050 && self.previous_operations.is_none()
2051 } else {
2052 self.desired_authority.is_none()
2053 && self.previous_authority.is_none()
2054 && self.desired_operations.is_some()
2055 && self.previous_operations.is_some()
2056 };
2057 if !shape_is_exact {
2058 return Err(InstallationPlanError::new(
2059 InstallationPlanErrorKind::InvalidShape,
2060 ));
2061 }
2062 let approval = self
2063 .widening_approval
2064 .map(|approval| {
2065 RoleWideningApproval::new_authority(
2066 ApplicationRoleHash::from_bytes(parse_hex32(&approval.expected_previous_role)?),
2067 approval
2068 .additions
2069 .into_iter()
2070 .map(AuthorityAtomDto::into_atom)
2071 .collect::<Result<Vec<_>, _>>()?,
2072 )
2073 })
2074 .transpose()?;
2075 let authority = |atoms: Vec<AuthorityAtomDto>| {
2076 let mut operations = Vec::new();
2077 let mut secret_outputs = Vec::new();
2078 for atom in atoms {
2079 match atom.into_atom()? {
2080 RoleAuthorityAtom::Operation(operation) => operations.push(operation),
2081 RoleAuthorityAtom::QuerySecretOutput(output) => secret_outputs.push(output),
2082 }
2083 }
2084 RoleAuthoritySet::new(operations, secret_outputs)
2085 };
2086 let desired_authority = if uses_authority {
2087 authority(self.desired_authority.ok_or_else(|| {
2088 InstallationPlanError::new(InstallationPlanErrorKind::InvalidShape)
2089 })?)?
2090 } else {
2091 RoleAuthoritySet::new(
2092 self.desired_operations
2093 .ok_or_else(|| {
2094 InstallationPlanError::new(InstallationPlanErrorKind::InvalidShape)
2095 })?
2096 .into_iter()
2097 .map(OperationDto::into_operation)
2098 .collect::<Result<Vec<_>, _>>()?,
2099 vec![],
2100 )?
2101 };
2102 let previous_authority = if uses_authority {
2103 authority(self.previous_authority.ok_or_else(|| {
2104 InstallationPlanError::new(InstallationPlanErrorKind::InvalidShape)
2105 })?)?
2106 } else {
2107 RoleAuthoritySet::new(
2108 self.previous_operations
2109 .ok_or_else(|| {
2110 InstallationPlanError::new(InstallationPlanErrorKind::InvalidShape)
2111 })?
2112 .into_iter()
2113 .map(OperationDto::into_operation)
2114 .collect::<Result<Vec<_>, _>>()?,
2115 vec![],
2116 )?
2117 };
2118 InstallationRole::new_with_authority(
2119 InstallationSymbol::new(self.name)?,
2120 ApplicationRoleHash::from_bytes(parse_hex32(&self.role_hash)?),
2121 self.previous_role_hash
2122 .map(|hash| parse_hex32(&hash).map(ApplicationRoleHash::from_bytes))
2123 .transpose()?,
2124 desired_authority,
2125 previous_authority,
2126 approval,
2127 )
2128 }
2129}
2130
2131impl CredentialDto {
2132 fn into_destination(self) -> Result<CredentialDestination, InstallationPlanError> {
2133 CredentialDestination::new(
2134 InstallationSymbol::new(self.name)?,
2135 InstallationSymbol::new(self.role)?,
2136 self.expected_current
2137 .map(|value| parse_capability_id(&value))
2138 .transpose()?,
2139 parse_capability_id(&self.successor)?,
2140 )
2141 }
2142}
2143
2144impl MigrationDto {
2145 fn into_migration(self) -> Result<InstallationMigration, InstallationPlanError> {
2146 if self.backup_policy != "required_verified" || self.downtime != "offline_exclusive" {
2147 return Err(InstallationPlanError::new(
2148 InstallationPlanErrorKind::UnsafeMigration,
2149 ));
2150 }
2151 InstallationMigration::new(
2152 ContractVersion::new(self.parent_version).ok_or_else(|| {
2153 InstallationPlanError::new(InstallationPlanErrorKind::InvalidShape)
2154 })?,
2155 ContractBundleHash::from_bytes(parse_hex32(&self.parent_bundle_hash)?),
2156 ContractVersion::new(self.successor_version).ok_or_else(|| {
2157 InstallationPlanError::new(InstallationPlanErrorKind::InvalidShape)
2158 })?,
2159 ContractBundleHash::from_bytes(parse_hex32(&self.successor_bundle_hash)?),
2160 MigrationBundleHash::from_bytes(parse_hex32(&self.migration_hash)?),
2161 MigrationBundleHash::from_bytes(parse_hex32(&self.confirmation)?),
2162 )
2163 }
2164}
2165
2166impl SeedDto {
2167 fn into_seed(self) -> Result<InstallationSeed, InstallationPlanError> {
2168 InstallationSeed::new(
2169 InstallationSymbol::new(self.name)?,
2170 GeneratedArtifactHash::from_bytes(parse_hex32(&self.content_hash)?),
2171 self.item_count,
2172 )
2173 }
2174}
2175
2176#[doc(hidden)]
2177pub fn hex32(bytes: &[u8; 32]) -> String {
2178 hex(bytes)
2179}
2180
2181#[doc(hidden)]
2182pub fn hex16(bytes: &[u8; 16]) -> String {
2183 hex(bytes)
2184}
2185
2186fn hex(bytes: &[u8]) -> String {
2187 const DIGITS: &[u8; 16] = b"0123456789abcdef";
2188 let mut output = String::with_capacity(bytes.len() * 2);
2189 for byte in bytes {
2190 output.push(char::from(DIGITS[usize::from(byte >> 4)]));
2191 output.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
2192 }
2193 output
2194}
2195
2196#[doc(hidden)]
2197pub fn parse_hex32(value: &str) -> Result<[u8; 32], InstallationPlanError> {
2198 parse_hex(value)
2199}
2200
2201#[doc(hidden)]
2202pub fn parse_hex16(value: &str) -> Result<[u8; 16], InstallationPlanError> {
2203 parse_hex(value)
2204}
2205
2206fn parse_hex<const N: usize>(value: &str) -> Result<[u8; N], InstallationPlanError> {
2207 if value.len() != N * 2 {
2208 return Err(InstallationPlanError::new(
2209 InstallationPlanErrorKind::InvalidEncoding,
2210 ));
2211 }
2212 let mut bytes = [0_u8; N];
2213 for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
2214 bytes[index] = (hex_nibble(pair[0])? << 4) | hex_nibble(pair[1])?;
2215 }
2216 Ok(bytes)
2217}
2218
2219fn hex_nibble(byte: u8) -> Result<u8, InstallationPlanError> {
2220 match byte {
2221 b'0'..=b'9' => Ok(byte - b'0'),
2222 b'a'..=b'f' => Ok(byte - b'a' + 10),
2223 _ => Err(InstallationPlanError::new(
2224 InstallationPlanErrorKind::InvalidEncoding,
2225 )),
2226 }
2227}
2228
2229fn parse_capability_id(value: &str) -> Result<CapabilityId, InstallationPlanError> {
2230 CapabilityId::from_bytes(parse_hex16(value)?)
2231 .map_err(|_| InstallationPlanError::new(InstallationPlanErrorKind::InvalidEncoding))
2232}