1use std::fmt;
2
3use riffdb_types::{
4 ApplicationExportManifestHash, ApplicationExportReceiptHash, ApplicationInstallationCampaignId,
5 ApplicationInstallationPlanHash, ApplicationInstallationReceiptHash, ApplicationLockHash,
6 ApplicationManifestHash, ApplicationPortabilityManifestHash, ApplicationReimportReceiptHash,
7 ApplicationRoleHash, ApplicationSourceHash, CapabilityId, ContractBundleHash,
8 ContractMigrationOperationId, ContractVersion, DatabaseId, GeneratedArtifactHash,
9 MigrationBundleHash, hash_application_installation_receipt,
10};
11use serde::{Deserialize, Serialize};
12
13pub use riffdb_types::MAX_APPLICATION_INSTALLATION_CAMPAIGN_STATE_BYTES;
14
15use crate::{
16 ApplicationInstallationPlan, ApplicationReimportCampaignV1, ApplicationReimportSourceV1,
17 InstallationArtifact, InstallationArtifactKind, InstallationCampaignError,
18 InstallationCampaignErrorKind, InstallationDriver, InstallationSymbol, InstalledSeedEvidence,
19 parse_hex16, parse_hex32,
20};
21
22pub const APPLICATION_INSTALLATION_RECEIPT_SCHEMA_V1: &str =
24 "riffdb.application-installation-receipt/v1";
25pub const APPLICATION_INSTALLATION_RECEIPT_SCHEMA_V2: &str =
27 "riffdb.application-installation-receipt/v2";
28pub const APPLICATION_INSTALLATION_RECEIPT_SCHEMA_V3: &str =
30 "riffdb.application-installation-receipt/v3";
31pub const APPLICATION_INSTALLATION_RECEIPT_SCHEMA_CURRENT: &str =
33 APPLICATION_INSTALLATION_RECEIPT_SCHEMA_V3;
34pub const APPLICATION_INSTALLATION_CAMPAIGN_STATE_SCHEMA_V1: &str =
36 "riffdb.application-installation-campaign-state/v1";
37pub const APPLICATION_INSTALLATION_CAMPAIGN_STATE_SCHEMA_V2: &str =
39 "riffdb.application-installation-campaign-state/v2";
40pub const APPLICATION_INSTALLATION_CAMPAIGN_STATE_SCHEMA_V3: &str =
42 "riffdb.application-installation-campaign-state/v3";
43pub const MAX_INSTALLATION_RECEIPT_BYTES: usize = 2 * 1_024 * 1_024;
45pub const MAX_INSTALLATION_CAMPAIGN_STATE_BYTES: usize =
47 MAX_APPLICATION_INSTALLATION_CAMPAIGN_STATE_BYTES;
48
49#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
51pub enum InstallationStage {
52 Preflight,
54 Contract,
56 Migration,
58 QueryModules,
60 ReactiveModules,
62 Roles,
64 Reimport,
66 Credentials,
68 DriverProof,
70 Seeds,
72 Receipt,
74}
75
76impl InstallationStage {
77 pub const ALL: [Self; 11] = [
79 Self::Preflight,
80 Self::Contract,
81 Self::Migration,
82 Self::QueryModules,
83 Self::ReactiveModules,
84 Self::Roles,
85 Self::Reimport,
86 Self::Credentials,
87 Self::DriverProof,
88 Self::Seeds,
89 Self::Receipt,
90 ];
91
92 #[must_use]
94 pub const fn as_str(self) -> &'static str {
95 match self {
96 Self::Preflight => "preflight",
97 Self::Contract => "contract",
98 Self::Migration => "migration",
99 Self::QueryModules => "query_modules",
100 Self::ReactiveModules => "reactive_modules",
101 Self::Roles => "roles",
102 Self::Reimport => "reimport",
103 Self::Credentials => "credentials",
104 Self::DriverProof => "driver_proof",
105 Self::Seeds => "seeds",
106 Self::Receipt => "receipt",
107 }
108 }
109}
110
111#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
113pub struct InstallationPlanIdentity(ApplicationInstallationPlanHash);
114
115impl InstallationPlanIdentity {
116 #[must_use]
118 pub const fn new(hash: ApplicationInstallationPlanHash) -> Self {
119 Self(hash)
120 }
121
122 #[must_use]
124 pub const fn hash(self) -> ApplicationInstallationPlanHash {
125 self.0
126 }
127}
128
129#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
131pub struct InstalledRoleEvidence {
132 name: InstallationSymbol,
133 role_hash: ApplicationRoleHash,
134}
135
136impl InstalledRoleEvidence {
137 #[must_use]
139 pub const fn new(name: InstallationSymbol, role_hash: ApplicationRoleHash) -> Self {
140 Self { name, role_hash }
141 }
142
143 #[must_use]
145 pub const fn name(&self) -> &InstallationSymbol {
146 &self.name
147 }
148
149 #[must_use]
151 pub const fn role_hash(&self) -> ApplicationRoleHash {
152 self.role_hash
153 }
154}
155
156#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub enum InstalledReimportEvidence {
159 NotRequired,
161 Reconciled(InstalledReimportReceiptEvidence),
163}
164
165#[derive(Clone, Copy, Debug, Eq, PartialEq)]
171pub struct InstalledReimportReceiptEvidence {
172 export_manifest_hash: ApplicationExportManifestHash,
173 export_receipt_hash: ApplicationExportReceiptHash,
174 portability_manifest_hash: ApplicationPortabilityManifestHash,
175 reimport_receipt_hash: ApplicationReimportReceiptHash,
176}
177
178#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
180pub struct InstalledCredentialEvidence {
181 destination: InstallationSymbol,
182 capability_id: CapabilityId,
183}
184
185impl InstalledCredentialEvidence {
186 #[must_use]
188 pub const fn new(destination: InstallationSymbol, capability_id: CapabilityId) -> Self {
189 Self {
190 destination,
191 capability_id,
192 }
193 }
194
195 #[must_use]
197 pub const fn destination(&self) -> &InstallationSymbol {
198 &self.destination
199 }
200
201 #[must_use]
203 pub const fn capability_id(&self) -> CapabilityId {
204 self.capability_id
205 }
206}
207
208#[derive(Clone, Debug, Eq, PartialEq)]
210pub enum InstallationStageEvidence {
211 Preflight {
213 source_hash: ApplicationSourceHash,
215 lock_hash: ApplicationLockHash,
217 manifest_hash: ApplicationManifestHash,
219 },
220 Contract {
222 version: ContractVersion,
224 bundle_hash: ContractBundleHash,
226 },
227 Migration {
229 migration_hash: Option<MigrationBundleHash>,
231 },
232 QueryModules(Vec<InstallationArtifact>),
234 ReactiveModules(Vec<InstallationArtifact>),
236 Roles(Vec<InstalledRoleEvidence>),
238 Reimport(InstalledReimportEvidence),
240 Credentials(Vec<InstalledCredentialEvidence>),
242 DriverProof(Vec<InstallationDriver>),
244 Seeds(Vec<InstalledSeedEvidence>),
246 Receipt(ApplicationInstallationReceiptHash),
248}
249
250impl InstallationStageEvidence {
251 #[must_use]
253 pub const fn stage(&self) -> InstallationStage {
254 match self {
255 Self::Preflight { .. } => InstallationStage::Preflight,
256 Self::Contract { .. } => InstallationStage::Contract,
257 Self::Migration { .. } => InstallationStage::Migration,
258 Self::QueryModules(_) => InstallationStage::QueryModules,
259 Self::ReactiveModules(_) => InstallationStage::ReactiveModules,
260 Self::Roles(_) => InstallationStage::Roles,
261 Self::Reimport(_) => InstallationStage::Reimport,
262 Self::Credentials(_) => InstallationStage::Credentials,
263 Self::DriverProof(_) => InstallationStage::DriverProof,
264 Self::Seeds(_) => InstallationStage::Seeds,
265 Self::Receipt(_) => InstallationStage::Receipt,
266 }
267 }
268
269 pub fn validate_for(
275 &self,
276 plan: &ApplicationInstallationPlan,
277 ) -> Result<(), InstallationCampaignError> {
278 validate_stage_evidence(plan, self)
279 }
280}
281
282#[derive(Clone, Copy, Debug, Eq, PartialEq)]
284pub enum InstallationFailureCode {
285 LocalArtifactMismatch,
287 RemoteIdentityMismatch,
289 MigrationGateRequired,
291 RoleWideningApprovalRequired,
293 ReimportPartial,
295 CredentialDestinationOccupied,
297 DriverProofFailed,
299 SeedPartial,
301 AuthorizationDenied,
303 ServiceUnavailable,
305}
306
307#[derive(Clone, Copy, Debug, Eq, PartialEq)]
309pub enum InstallationNextAction {
310 ValidateLocalArtifacts,
312 DeployContract,
314 ApplyMigration,
316 DeployQueryModules,
318 DeployReactiveModules,
320 ReconcileRoles,
322 ReimportApplication,
324 RotateCredentials,
326 ProveDrivers,
328 RunSeeds,
330 SealReceipt,
332 None,
334}
335
336impl InstallationNextAction {
337 #[must_use]
339 pub const fn for_stage(stage: InstallationStage) -> Self {
340 match stage {
341 InstallationStage::Preflight => Self::ValidateLocalArtifacts,
342 InstallationStage::Contract => Self::DeployContract,
343 InstallationStage::Migration => Self::ApplyMigration,
344 InstallationStage::QueryModules => Self::DeployQueryModules,
345 InstallationStage::ReactiveModules => Self::DeployReactiveModules,
346 InstallationStage::Roles => Self::ReconcileRoles,
347 InstallationStage::Reimport => Self::ReimportApplication,
348 InstallationStage::Credentials => Self::RotateCredentials,
349 InstallationStage::DriverProof => Self::ProveDrivers,
350 InstallationStage::Seeds => Self::RunSeeds,
351 InstallationStage::Receipt => Self::SealReceipt,
352 }
353 }
354}
355
356#[derive(Clone, Copy, Debug, Eq, PartialEq)]
358pub struct InstallationFailure {
359 stage: InstallationStage,
360 code: InstallationFailureCode,
361 next_action: InstallationNextAction,
362}
363
364impl InstallationFailure {
365 #[must_use]
367 pub const fn stage(self) -> InstallationStage {
368 self.stage
369 }
370
371 #[must_use]
373 pub const fn code(self) -> InstallationFailureCode {
374 self.code
375 }
376
377 #[must_use]
379 pub const fn next_action(self) -> InstallationNextAction {
380 self.next_action
381 }
382}
383
384#[derive(Clone, Copy, Debug, Eq, PartialEq)]
386pub enum InstallationCampaignPhase {
387 Running,
389 Partial,
391 Installed,
393}
394
395#[derive(Clone, Debug, Eq, PartialEq)]
397pub struct ApplicationInstallationObservation {
398 campaign_id: ApplicationInstallationCampaignId,
399 plan_hash: ApplicationInstallationPlanHash,
400 phase: InstallationCampaignPhase,
401 completed: Vec<InstallationStage>,
402 next_stage: Option<InstallationStage>,
403 next_action: InstallationNextAction,
404 failure: Option<InstallationFailure>,
405 receipt_hash: Option<ApplicationInstallationReceiptHash>,
406}
407
408impl ApplicationInstallationObservation {
409 #[must_use]
411 pub const fn campaign_id(&self) -> ApplicationInstallationCampaignId {
412 self.campaign_id
413 }
414
415 #[must_use]
417 pub const fn plan_hash(&self) -> ApplicationInstallationPlanHash {
418 self.plan_hash
419 }
420
421 #[must_use]
423 pub const fn phase(&self) -> InstallationCampaignPhase {
424 self.phase
425 }
426
427 #[must_use]
429 pub fn completed(&self) -> &[InstallationStage] {
430 &self.completed
431 }
432
433 #[must_use]
435 pub const fn next_stage(&self) -> Option<InstallationStage> {
436 self.next_stage
437 }
438
439 #[must_use]
441 pub const fn next_action(&self) -> InstallationNextAction {
442 self.next_action
443 }
444
445 #[must_use]
447 pub const fn failure(&self) -> Option<InstallationFailure> {
448 self.failure
449 }
450
451 #[must_use]
453 pub const fn receipt_hash(&self) -> Option<ApplicationInstallationReceiptHash> {
454 self.receipt_hash
455 }
456}
457
458#[derive(Clone, Eq, PartialEq)]
460pub struct ApplicationInstallationCampaign {
461 campaign_id: ApplicationInstallationCampaignId,
462 plan_hash: ApplicationInstallationPlanHash,
463 completed: Vec<InstallationStageEvidence>,
464 failure: Option<InstallationFailure>,
465 reimport: Option<ApplicationReimportCampaignV1>,
466}
467
468impl ApplicationInstallationCampaign {
469 #[must_use]
471 pub const fn start(
472 campaign_id: ApplicationInstallationCampaignId,
473 plan_hash: ApplicationInstallationPlanHash,
474 ) -> Self {
475 Self {
476 campaign_id,
477 plan_hash,
478 completed: Vec::new(),
479 failure: None,
480 reimport: None,
481 }
482 }
483
484 pub fn resume(
486 &mut self,
487 campaign_id: ApplicationInstallationCampaignId,
488 plan: &ApplicationInstallationPlan,
489 ) -> Result<ApplicationInstallationObservation, InstallationCampaignError> {
490 self.verify_identity(campaign_id, plan)?;
491 for evidence in &self.completed {
492 validate_stage_evidence(plan, evidence)?;
493 }
494 self.failure = None;
495 Ok(self.observe())
496 }
497
498 pub fn complete_stage(
500 &mut self,
501 plan: &ApplicationInstallationPlan,
502 evidence: InstallationStageEvidence,
503 ) -> Result<ApplicationInstallationObservation, InstallationCampaignError> {
504 self.verify_plan(plan)?;
505 let stage_index = InstallationStage::ALL
506 .iter()
507 .position(|stage| *stage == evidence.stage())
508 .expect("closed installation stage registry");
509 if let Some(completed) = self.completed.get(stage_index) {
510 if completed == &evidence {
511 return Ok(self.observe());
512 }
513 return Err(InstallationCampaignError::new(
514 InstallationCampaignErrorKind::EvidenceMismatch,
515 ));
516 }
517 if self.is_installed() {
518 return Err(InstallationCampaignError::new(
519 InstallationCampaignErrorKind::AlreadyTerminal,
520 ));
521 }
522 let expected = self.next_stage().ok_or_else(|| {
523 InstallationCampaignError::new(InstallationCampaignErrorKind::AlreadyTerminal)
524 })?;
525 if evidence.stage() != expected || expected == InstallationStage::Receipt {
526 return Err(InstallationCampaignError::new(
527 InstallationCampaignErrorKind::StageOutOfOrder,
528 ));
529 }
530 if expected == InstallationStage::Reimport && plan.input().reimport.is_some() {
531 return Err(InstallationCampaignError::new(
532 InstallationCampaignErrorKind::StageOutOfOrder,
533 ));
534 }
535 validate_stage_evidence(plan, &evidence)?;
536 self.completed.push(evidence);
537 self.failure = None;
538 Ok(self.observe())
539 }
540
541 pub fn start_reimport(
543 &mut self,
544 plan: &ApplicationInstallationPlan,
545 source: ApplicationReimportSourceV1,
546 authority: riffdb_types::ApplicationReimportAuthorityV1,
547 scope: riffdb_types::CapabilityApplicationReimportScopeV1,
548 portability_manifest: &crate::ApplicationPortabilityManifest,
549 ) -> Result<&ApplicationReimportCampaignV1, InstallationCampaignError> {
550 self.verify_plan(plan)?;
551 if self.next_stage() != Some(InstallationStage::Reimport) || self.reimport.is_some() {
552 return Err(InstallationCampaignError::new(
553 InstallationCampaignErrorKind::StageOutOfOrder,
554 ));
555 }
556 let expected = plan.input().reimport.ok_or_else(evidence_mismatch)?;
557 if source.export_manifest_hash() != expected.export_manifest_hash()
558 || source.export_receipt_hash() != expected.export_receipt_hash()
559 || source.portability_manifest_hash() != expected.portability_manifest_hash()
560 || portability_manifest.identity() != expected.portability_manifest_hash()
561 {
562 return Err(evidence_mismatch());
563 }
564 self.reimport = Some(
565 ApplicationReimportCampaignV1::start(source, authority, scope, portability_manifest)
566 .map_err(|_| evidence_mismatch())?,
567 );
568 self.failure = None;
569 Ok(self.reimport.as_ref().expect("nested reimport just set"))
570 }
571
572 #[must_use]
574 pub const fn reimport(&self) -> Option<&ApplicationReimportCampaignV1> {
575 self.reimport.as_ref()
576 }
577
578 pub fn reimport_mut(&mut self) -> Option<&mut ApplicationReimportCampaignV1> {
580 self.reimport.as_mut()
581 }
582
583 pub fn complete_reimport(
585 &mut self,
586 plan: &ApplicationInstallationPlan,
587 portability_manifest: &crate::ApplicationPortabilityManifest,
588 reimport_receipt: &crate::ApplicationReimportReceipt,
589 target_database_id: DatabaseId,
590 ) -> Result<ApplicationInstallationObservation, InstallationCampaignError> {
591 self.verify_plan(plan)?;
592 if self.next_stage() != Some(InstallationStage::Reimport) {
593 return Err(InstallationCampaignError::new(
594 InstallationCampaignErrorKind::StageOutOfOrder,
595 ));
596 }
597 let expected = plan.input().reimport.ok_or_else(evidence_mismatch)?;
598 let receipt = reimport_receipt.input();
599 let progress = self.reimport.as_ref().ok_or_else(evidence_mismatch)?;
600 if portability_manifest.identity() != expected.portability_manifest_hash()
601 || portability_manifest.input().contract_lineage != *plan.input().target.lineage()
602 || portability_manifest.input().contract_version != plan.input().contract.version()
603 || portability_manifest.input().contract_bundle_hash
604 != plan.input().contract.bundle_hash()
605 || receipt.portability_manifest_hash != expected.portability_manifest_hash()
606 || receipt.export_manifest_hash != expected.export_manifest_hash()
607 || receipt.target_database_id != target_database_id
608 || progress.phase() != crate::ApplicationReimportCampaignPhaseV1::Reconciled
609 || progress.receipt_hash() != Some(reimport_receipt.identity())
610 {
611 return Err(evidence_mismatch());
612 }
613 self.completed.push(InstallationStageEvidence::Reimport(
614 InstalledReimportEvidence::Reconciled(InstalledReimportReceiptEvidence {
615 export_manifest_hash: expected.export_manifest_hash(),
616 export_receipt_hash: expected.export_receipt_hash(),
617 portability_manifest_hash: expected.portability_manifest_hash(),
618 reimport_receipt_hash: reimport_receipt.identity(),
619 }),
620 ));
621 self.failure = None;
622 Ok(self.observe())
623 }
624
625 pub fn record_failure(
627 &mut self,
628 plan: &ApplicationInstallationPlan,
629 code: InstallationFailureCode,
630 ) -> Result<ApplicationInstallationObservation, InstallationCampaignError> {
631 self.verify_plan(plan)?;
632 let stage = self.next_stage().ok_or_else(|| {
633 InstallationCampaignError::new(InstallationCampaignErrorKind::AlreadyTerminal)
634 })?;
635 self.failure = Some(InstallationFailure {
636 stage,
637 code,
638 next_action: InstallationNextAction::for_stage(stage),
639 });
640 Ok(self.observe())
641 }
642
643 pub fn seal_receipt(
645 &mut self,
646 plan: &ApplicationInstallationPlan,
647 ) -> Result<ApplicationInstallationReceipt, InstallationCampaignError> {
648 self.verify_plan(plan)?;
649 if let Some(InstallationStageEvidence::Receipt(retained_hash)) = self.completed.last() {
650 return ApplicationInstallationReceipt::seal_matching(self, plan, *retained_hash);
651 }
652 if self.next_stage() != Some(InstallationStage::Receipt) {
653 return Err(InstallationCampaignError::new(
654 InstallationCampaignErrorKind::StageOutOfOrder,
655 ));
656 }
657 let receipt = ApplicationInstallationReceipt::seal(self, plan)?;
658 self.completed
659 .push(InstallationStageEvidence::Receipt(receipt.identity()));
660 self.failure = None;
661 Ok(receipt)
662 }
663
664 #[must_use]
666 pub fn observe(&self) -> ApplicationInstallationObservation {
667 let next_stage = self.next_stage();
668 let receipt_hash = self.completed.last().and_then(|evidence| match evidence {
669 InstallationStageEvidence::Receipt(hash) => Some(*hash),
670 _ => None,
671 });
672 let phase = if receipt_hash.is_some() {
673 InstallationCampaignPhase::Installed
674 } else if self.failure.is_some() {
675 InstallationCampaignPhase::Partial
676 } else {
677 InstallationCampaignPhase::Running
678 };
679 ApplicationInstallationObservation {
680 campaign_id: self.campaign_id,
681 plan_hash: self.plan_hash,
682 phase,
683 completed: self
684 .completed
685 .iter()
686 .map(InstallationStageEvidence::stage)
687 .collect(),
688 next_stage,
689 next_action: next_stage.map_or(InstallationNextAction::None, |stage| {
690 self.failure.map_or_else(
691 || InstallationNextAction::for_stage(stage),
692 InstallationFailure::next_action,
693 )
694 }),
695 failure: self.failure,
696 receipt_hash,
697 }
698 }
699
700 #[must_use]
702 pub fn completed_evidence(&self) -> &[InstallationStageEvidence] {
703 &self.completed
704 }
705
706 #[must_use]
708 pub fn is_installed(&self) -> bool {
709 matches!(
710 self.completed.last(),
711 Some(InstallationStageEvidence::Receipt(_))
712 )
713 }
714
715 fn verify_identity(
716 &self,
717 campaign_id: ApplicationInstallationCampaignId,
718 plan: &ApplicationInstallationPlan,
719 ) -> Result<(), InstallationCampaignError> {
720 if self.campaign_id != campaign_id {
721 return Err(InstallationCampaignError::new(
722 InstallationCampaignErrorKind::CampaignIdentityMismatch,
723 ));
724 }
725 self.verify_plan(plan)
726 }
727
728 fn verify_plan(
729 &self,
730 plan: &ApplicationInstallationPlan,
731 ) -> Result<(), InstallationCampaignError> {
732 if self.plan_hash != plan.identity() {
733 return Err(InstallationCampaignError::new(
734 InstallationCampaignErrorKind::PlanIdentityMismatch,
735 ));
736 }
737 Ok(())
738 }
739
740 fn next_stage(&self) -> Option<InstallationStage> {
741 InstallationStage::ALL.get(self.completed.len()).copied()
742 }
743}
744
745impl fmt::Debug for ApplicationInstallationCampaign {
746 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
747 formatter
748 .debug_struct("ApplicationInstallationCampaign")
749 .field("campaign_id", &self.campaign_id)
750 .field("plan_hash", &self.plan_hash)
751 .field("completed_stages", &self.completed.len())
752 .field("failure", &self.failure)
753 .finish_non_exhaustive()
754 }
755}
756
757#[derive(Clone, Eq, PartialEq)]
763pub struct ApplicationInstallationCampaignState {
764 plan: ApplicationInstallationPlan,
765 campaign: ApplicationInstallationCampaign,
766 canonical_bytes: Vec<u8>,
767}
768
769impl ApplicationInstallationCampaignState {
770 pub fn capture(
772 campaign: &ApplicationInstallationCampaign,
773 plan: &ApplicationInstallationPlan,
774 ) -> Result<Self, InstallationCampaignError> {
775 validate_campaign_state(campaign, plan)?;
776 let dto = CampaignStateDto::from_parts(campaign, plan);
777 let canonical_bytes = encode_campaign_state_dto(&dto)?;
778 Ok(Self {
779 plan: plan.clone(),
780 campaign: campaign.clone(),
781 canonical_bytes,
782 })
783 }
784
785 pub fn decode_canonical(bytes: &[u8]) -> Result<Self, InstallationCampaignError> {
787 if bytes.is_empty() || bytes.len() > MAX_INSTALLATION_CAMPAIGN_STATE_BYTES {
788 return Err(InstallationCampaignError::new(
789 InstallationCampaignErrorKind::LimitExceeded,
790 ));
791 }
792 let dto: CampaignStateDto = serde_json::from_slice(bytes).map_err(|_| {
793 InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding)
794 })?;
795 let schema = CampaignStateSchema::parse(&dto.schema)?;
796 let plan = ApplicationInstallationPlan::decode_canonical(dto.plan.as_bytes())
797 .map_err(InstallationCampaignError::from_plan_error)?;
798 let campaign = dto.to_campaign(&plan, schema)?;
799 validate_campaign_state(&campaign, &plan)?;
800 let canonical_bytes = encode_campaign_state_dto(&dto)?;
801 if canonical_bytes != bytes {
802 return Err(InstallationCampaignError::new(
803 InstallationCampaignErrorKind::NonCanonical,
804 ));
805 }
806 Ok(Self {
807 plan,
808 campaign,
809 canonical_bytes,
810 })
811 }
812
813 #[must_use]
815 pub const fn plan(&self) -> &ApplicationInstallationPlan {
816 &self.plan
817 }
818
819 #[must_use]
821 pub const fn campaign(&self) -> &ApplicationInstallationCampaign {
822 &self.campaign
823 }
824
825 #[must_use]
827 pub fn canonical_bytes(&self) -> &[u8] {
828 &self.canonical_bytes
829 }
830
831 #[must_use]
833 pub fn into_parts(self) -> (ApplicationInstallationPlan, ApplicationInstallationCampaign) {
834 (self.plan, self.campaign)
835 }
836}
837
838impl fmt::Debug for ApplicationInstallationCampaignState {
839 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
840 formatter
841 .debug_struct("ApplicationInstallationCampaignState")
842 .field("plan_hash", &self.plan.identity())
843 .field("campaign", &self.campaign)
844 .finish_non_exhaustive()
845 }
846}
847
848#[derive(Clone, Eq, PartialEq)]
850pub struct ApplicationInstallationReceipt {
851 identity: ApplicationInstallationReceiptHash,
852 canonical_bytes: Vec<u8>,
853 campaign_id: ApplicationInstallationCampaignId,
854 plan_hash: ApplicationInstallationPlanHash,
855}
856
857impl ApplicationInstallationReceipt {
858 fn seal(
859 campaign: &ApplicationInstallationCampaign,
860 plan: &ApplicationInstallationPlan,
861 ) -> Result<Self, InstallationCampaignError> {
862 let dto = ReceiptDtoV2::from_campaign(campaign, plan)?;
863 Self::from_v2_dto(dto)
864 }
865
866 fn seal_v1(
867 campaign: &ApplicationInstallationCampaign,
868 plan: &ApplicationInstallationPlan,
869 ) -> Result<Self, InstallationCampaignError> {
870 let input = plan.input();
871 let seed_items = input.seeds.iter().try_fold(0_u64, |sum, seed| {
872 sum.checked_add(seed.item_count()).ok_or_else(|| {
873 InstallationCampaignError::new(InstallationCampaignErrorKind::EvidenceMismatch)
874 })
875 })?;
876 let dto = ReceiptDtoV1 {
877 schema: APPLICATION_INSTALLATION_RECEIPT_SCHEMA_V1.to_owned(),
878 campaign_id: hex16(campaign.campaign_id.as_bytes()),
879 plan_hash: hex32(plan.identity().as_bytes()),
880 application: input.application.as_str().to_owned(),
881 database: input.target.database().as_str().to_owned(),
882 environment: input.target.environment().as_str().to_owned(),
883 lineage: input.target.lineage().as_str().to_owned(),
884 contract_version: input.contract.version().get(),
885 contract_bundle_hash: hex32(input.contract.bundle_hash().as_bytes()),
886 source_hash: hex32(input.source_hash.as_bytes()),
887 lock_hash: hex32(input.lock_hash.as_bytes()),
888 manifest_hash: hex32(input.manifest_hash.as_bytes()),
889 artifacts: input
890 .artifacts
891 .iter()
892 .map(|artifact| ReceiptArtifactDto {
893 kind: artifact.kind().tag().to_owned(),
894 name: artifact.name().as_str().to_owned(),
895 content_hash: hex32(artifact.content_hash().as_bytes()),
896 })
897 .collect(),
898 roles: input
899 .roles
900 .iter()
901 .map(|role| ReceiptRoleDto {
902 name: role.name().as_str().to_owned(),
903 role_hash: hex32(role.role_hash().as_bytes()),
904 })
905 .collect(),
906 drivers: input
907 .drivers
908 .iter()
909 .map(|driver| driver.tag().to_owned())
910 .collect(),
911 seed_batches: u64::try_from(input.seeds.len()).map_err(|_| {
912 InstallationCampaignError::new(InstallationCampaignErrorKind::EvidenceMismatch)
913 })?,
914 seed_items,
915 migration_hash: input
916 .migration
917 .map(|migration| hex32(migration.migration_hash().as_bytes())),
918 adapter_manifest_hash: input
919 .adapter_manifest_hash
920 .map(|hash| hex32(hash.as_bytes())),
921 };
922 Self::from_v1_dto(dto)
923 }
924
925 fn seal_matching(
926 campaign: &ApplicationInstallationCampaign,
927 plan: &ApplicationInstallationPlan,
928 expected: ApplicationInstallationReceiptHash,
929 ) -> Result<Self, InstallationCampaignError> {
930 let current = Self::seal(campaign, plan)?;
931 if current.identity() == expected {
932 return Ok(current);
933 }
934 let legacy = Self::seal_v1(campaign, plan)?;
935 if legacy.identity() == expected {
936 return Ok(legacy);
937 }
938 Err(InstallationCampaignError::new(
939 InstallationCampaignErrorKind::EvidenceMismatch,
940 ))
941 }
942
943 fn from_v1_dto(dto: ReceiptDtoV1) -> Result<Self, InstallationCampaignError> {
944 let campaign_id = parse_receipt_campaign_id(&dto.campaign_id)?;
945 let plan_hash = parse_receipt_plan_hash(&dto.plan_hash)?;
946 let mut canonical_bytes = serde_json::to_vec(&dto).map_err(|_| {
947 InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding)
948 })?;
949 canonical_bytes.push(b'\n');
950 if canonical_bytes.len() > MAX_INSTALLATION_RECEIPT_BYTES {
951 return Err(InstallationCampaignError::new(
952 InstallationCampaignErrorKind::LimitExceeded,
953 ));
954 }
955 Ok(Self {
956 identity: hash_application_installation_receipt(&canonical_bytes),
957 canonical_bytes,
958 campaign_id,
959 plan_hash,
960 })
961 }
962
963 fn from_v2_dto(dto: ReceiptDtoV2) -> Result<Self, InstallationCampaignError> {
964 let campaign_id = parse_receipt_campaign_id(&dto.campaign_id)?;
965 let plan_hash = parse_receipt_plan_hash(&dto.plan_hash)?;
966 let mut canonical_bytes = serde_json::to_vec(&dto).map_err(|_| {
967 InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding)
968 })?;
969 canonical_bytes.push(b'\n');
970 if canonical_bytes.len() > MAX_INSTALLATION_RECEIPT_BYTES {
971 return Err(InstallationCampaignError::new(
972 InstallationCampaignErrorKind::LimitExceeded,
973 ));
974 }
975 Ok(Self {
976 identity: hash_application_installation_receipt(&canonical_bytes),
977 canonical_bytes,
978 campaign_id,
979 plan_hash,
980 })
981 }
982
983 pub fn decode_canonical(bytes: &[u8]) -> Result<Self, InstallationCampaignError> {
985 if bytes.is_empty() || bytes.len() > MAX_INSTALLATION_RECEIPT_BYTES {
986 return Err(InstallationCampaignError::new(
987 InstallationCampaignErrorKind::LimitExceeded,
988 ));
989 }
990 let probe: ReceiptSchemaProbe = serde_json::from_slice(bytes).map_err(|_| {
991 InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding)
992 })?;
993 let receipt = match probe.schema.as_str() {
994 APPLICATION_INSTALLATION_RECEIPT_SCHEMA_V1 => {
995 let dto: ReceiptDtoV1 = serde_json::from_slice(bytes).map_err(|_| {
996 InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding)
997 })?;
998 validate_receipt_v1_dto(&dto)?;
999 Self::from_v1_dto(dto)?
1000 }
1001 APPLICATION_INSTALLATION_RECEIPT_SCHEMA_V2 => {
1002 let dto: ReceiptDtoV2 = serde_json::from_slice(bytes).map_err(|_| {
1003 InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding)
1004 })?;
1005 validate_receipt_v2_dto(&dto)?;
1006 Self::from_v2_dto(dto)?
1007 }
1008 APPLICATION_INSTALLATION_RECEIPT_SCHEMA_V3 => {
1009 let dto: ReceiptDtoV2 = serde_json::from_slice(bytes).map_err(|_| {
1010 InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding)
1011 })?;
1012 validate_receipt_v3_dto(&dto)?;
1013 Self::from_v2_dto(dto)?
1014 }
1015 _ => {
1016 return Err(InstallationCampaignError::new(
1017 InstallationCampaignErrorKind::UnsupportedVersion,
1018 ));
1019 }
1020 };
1021 if receipt.canonical_bytes() != bytes {
1022 return Err(InstallationCampaignError::new(
1023 InstallationCampaignErrorKind::NonCanonical,
1024 ));
1025 }
1026 Ok(receipt)
1027 }
1028
1029 #[must_use]
1031 pub const fn identity(&self) -> ApplicationInstallationReceiptHash {
1032 self.identity
1033 }
1034
1035 #[must_use]
1037 pub const fn campaign_id(&self) -> ApplicationInstallationCampaignId {
1038 self.campaign_id
1039 }
1040
1041 #[must_use]
1043 pub const fn plan_hash(&self) -> ApplicationInstallationPlanHash {
1044 self.plan_hash
1045 }
1046
1047 #[must_use]
1049 pub fn canonical_bytes(&self) -> &[u8] {
1050 &self.canonical_bytes
1051 }
1052}
1053
1054impl fmt::Debug for ApplicationInstallationReceipt {
1055 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1056 formatter
1057 .debug_struct("ApplicationInstallationReceipt")
1058 .field("identity", &self.identity)
1059 .field("campaign_id", &self.campaign_id)
1060 .field("plan_hash", &self.plan_hash)
1061 .finish_non_exhaustive()
1062 }
1063}
1064
1065fn validate_stage_evidence(
1066 plan: &ApplicationInstallationPlan,
1067 evidence: &InstallationStageEvidence,
1068) -> Result<(), InstallationCampaignError> {
1069 let input = plan.input();
1070 let valid = match evidence {
1071 InstallationStageEvidence::Preflight {
1072 source_hash,
1073 lock_hash,
1074 manifest_hash,
1075 } => {
1076 *source_hash == input.source_hash
1077 && *lock_hash == input.lock_hash
1078 && *manifest_hash == input.manifest_hash
1079 }
1080 InstallationStageEvidence::Contract {
1081 version,
1082 bundle_hash,
1083 } => *version == input.contract.version() && *bundle_hash == input.contract.bundle_hash(),
1084 InstallationStageEvidence::Migration { migration_hash } => {
1085 *migration_hash == input.migration.map(|migration| migration.migration_hash())
1086 }
1087 InstallationStageEvidence::QueryModules(artifacts) => {
1088 artifacts == &artifacts_for(input, InstallationArtifactKind::QueryModule)
1089 }
1090 InstallationStageEvidence::ReactiveModules(artifacts) => {
1091 artifacts == &artifacts_for(input, InstallationArtifactKind::ReactiveModule)
1092 }
1093 InstallationStageEvidence::Roles(roles) => {
1094 let expected = input
1095 .roles
1096 .iter()
1097 .map(|role| InstalledRoleEvidence::new(role.name().clone(), role.role_hash()))
1098 .collect::<Vec<_>>();
1099 roles == &expected
1100 }
1101 InstallationStageEvidence::Reimport(evidence) => match (input.reimport, evidence) {
1102 (None, InstalledReimportEvidence::NotRequired) => true,
1103 (
1104 Some(expected),
1105 InstalledReimportEvidence::Reconciled(InstalledReimportReceiptEvidence {
1106 export_manifest_hash,
1107 export_receipt_hash,
1108 portability_manifest_hash,
1109 ..
1110 }),
1111 ) => {
1112 *export_manifest_hash == expected.export_manifest_hash()
1113 && *export_receipt_hash == expected.export_receipt_hash()
1114 && *portability_manifest_hash == expected.portability_manifest_hash()
1115 }
1116 _ => false,
1117 },
1118 InstallationStageEvidence::Credentials(credentials) => {
1119 let expected = input
1120 .credential_destinations
1121 .iter()
1122 .map(|destination| {
1123 InstalledCredentialEvidence::new(
1124 destination.name().clone(),
1125 destination.successor(),
1126 )
1127 })
1128 .collect::<Vec<_>>();
1129 credentials == &expected
1130 }
1131 InstallationStageEvidence::DriverProof(drivers) => drivers == &input.drivers,
1132 InstallationStageEvidence::Seeds(seeds) => {
1133 seeds.len() == input.seeds.len()
1134 && seeds.iter().zip(&input.seeds).all(|(observed, expected)| {
1135 observed.name() == expected.name()
1136 && observed.content_hash() == expected.content_hash()
1137 && observed.succeeded().checked_add(observed.replayed())
1138 == Some(expected.item_count())
1139 })
1140 }
1141 InstallationStageEvidence::Receipt(_) => false,
1142 };
1143 if valid {
1144 Ok(())
1145 } else {
1146 Err(InstallationCampaignError::new(
1147 InstallationCampaignErrorKind::EvidenceMismatch,
1148 ))
1149 }
1150}
1151
1152fn artifacts_for(
1153 input: &crate::ApplicationInstallationPlanInput,
1154 kind: InstallationArtifactKind,
1155) -> Vec<InstallationArtifact> {
1156 input
1157 .artifacts
1158 .iter()
1159 .filter(|artifact| artifact.kind() == kind)
1160 .cloned()
1161 .collect()
1162}
1163
1164fn validate_campaign_state(
1165 campaign: &ApplicationInstallationCampaign,
1166 plan: &ApplicationInstallationPlan,
1167) -> Result<(), InstallationCampaignError> {
1168 campaign.verify_plan(plan)?;
1169 match (&campaign.reimport, plan.input().reimport) {
1170 (None, _) => {}
1171 (Some(progress), Some(expected)) => {
1172 if progress.source().export_manifest_hash() != expected.export_manifest_hash()
1173 || progress.source().export_receipt_hash() != expected.export_receipt_hash()
1174 || progress.source().portability_manifest_hash()
1175 != expected.portability_manifest_hash()
1176 {
1177 return Err(evidence_mismatch());
1178 }
1179 let completed_reimport = campaign
1180 .completed
1181 .iter()
1182 .any(|evidence| matches!(evidence, InstallationStageEvidence::Reimport(_)));
1183 if completed_reimport
1184 && progress.phase() != crate::ApplicationReimportCampaignPhaseV1::Reconciled
1185 {
1186 return Err(evidence_mismatch());
1187 }
1188 if !completed_reimport && campaign.next_stage() != Some(InstallationStage::Reimport) {
1189 return Err(evidence_mismatch());
1190 }
1191 }
1192 (Some(_), None) => return Err(evidence_mismatch()),
1193 }
1194 if campaign.completed.len() > InstallationStage::ALL.len() {
1195 return Err(InstallationCampaignError::new(
1196 InstallationCampaignErrorKind::StageOutOfOrder,
1197 ));
1198 }
1199 for (index, evidence) in campaign.completed.iter().enumerate() {
1200 let expected = InstallationStage::ALL[index];
1201 if evidence.stage() != expected {
1202 return Err(InstallationCampaignError::new(
1203 InstallationCampaignErrorKind::StageOutOfOrder,
1204 ));
1205 }
1206 match evidence {
1207 InstallationStageEvidence::Receipt(hash) => {
1208 if index + 1 != InstallationStage::ALL.len() {
1209 return Err(InstallationCampaignError::new(
1210 InstallationCampaignErrorKind::StageOutOfOrder,
1211 ));
1212 }
1213 let prefix = ApplicationInstallationCampaign {
1214 campaign_id: campaign.campaign_id,
1215 plan_hash: campaign.plan_hash,
1216 completed: campaign.completed[..index].to_vec(),
1217 failure: None,
1218 reimport: campaign.reimport.clone(),
1219 };
1220 ApplicationInstallationReceipt::seal_matching(&prefix, plan, *hash)?;
1221 }
1222 _ => validate_stage_evidence(plan, evidence)?,
1223 }
1224 }
1225 match campaign.failure {
1226 None => Ok(()),
1227 Some(failure) => {
1228 let Some(stage) = campaign.next_stage() else {
1229 return Err(InstallationCampaignError::new(
1230 InstallationCampaignErrorKind::EvidenceMismatch,
1231 ));
1232 };
1233 if failure.stage != stage
1234 || failure.next_action != InstallationNextAction::for_stage(stage)
1235 {
1236 return Err(InstallationCampaignError::new(
1237 InstallationCampaignErrorKind::EvidenceMismatch,
1238 ));
1239 }
1240 Ok(())
1241 }
1242 }
1243}
1244
1245fn encode_campaign_state_dto(dto: &CampaignStateDto) -> Result<Vec<u8>, InstallationCampaignError> {
1246 let mut bytes = serde_json::to_vec(dto).map_err(|_| {
1247 InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding)
1248 })?;
1249 bytes.push(b'\n');
1250 if bytes.len() > MAX_INSTALLATION_CAMPAIGN_STATE_BYTES {
1251 return Err(InstallationCampaignError::new(
1252 InstallationCampaignErrorKind::LimitExceeded,
1253 ));
1254 }
1255 Ok(bytes)
1256}
1257
1258#[derive(Serialize, Deserialize)]
1259#[serde(deny_unknown_fields)]
1260struct CampaignStateDto {
1261 schema: String,
1262 campaign_id: String,
1263 plan_hash: String,
1264 plan: String,
1265 completed: Vec<CampaignEvidenceDto>,
1266 failure: Option<CampaignFailureDto>,
1267 #[serde(default, skip_serializing_if = "Option::is_none")]
1268 reimport: Option<String>,
1269}
1270
1271#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1272enum CampaignStateSchema {
1273 V1,
1274 V2,
1275 V3,
1276}
1277
1278impl CampaignStateSchema {
1279 fn parse(value: &str) -> Result<Self, InstallationCampaignError> {
1280 match value {
1281 APPLICATION_INSTALLATION_CAMPAIGN_STATE_SCHEMA_V1 => Ok(Self::V1),
1282 APPLICATION_INSTALLATION_CAMPAIGN_STATE_SCHEMA_V2 => Ok(Self::V2),
1283 APPLICATION_INSTALLATION_CAMPAIGN_STATE_SCHEMA_V3 => Ok(Self::V3),
1284 _ => Err(InstallationCampaignError::new(
1285 InstallationCampaignErrorKind::UnsupportedVersion,
1286 )),
1287 }
1288 }
1289}
1290
1291impl CampaignStateDto {
1292 fn from_parts(
1293 campaign: &ApplicationInstallationCampaign,
1294 plan: &ApplicationInstallationPlan,
1295 ) -> Self {
1296 Self {
1297 schema: if campaign.reimport.is_some() {
1298 APPLICATION_INSTALLATION_CAMPAIGN_STATE_SCHEMA_V3
1299 } else {
1300 APPLICATION_INSTALLATION_CAMPAIGN_STATE_SCHEMA_V2
1301 }
1302 .to_owned(),
1303 campaign_id: hex16(campaign.campaign_id.as_bytes()),
1304 plan_hash: hex32(campaign.plan_hash.as_bytes()),
1305 plan: String::from_utf8(plan.canonical_bytes().to_vec())
1306 .expect("canonical installation plans are JSON UTF-8"),
1307 completed: campaign
1308 .completed
1309 .iter()
1310 .map(CampaignEvidenceDto::from_evidence)
1311 .collect(),
1312 failure: campaign.failure.map(CampaignFailureDto::from_failure),
1313 reimport: campaign.reimport.as_ref().map(|progress| {
1314 String::from_utf8(
1315 progress
1316 .encode_canonical()
1317 .expect("validated reimport progress is canonical"),
1318 )
1319 .expect("canonical reimport progress is JSON UTF-8")
1320 }),
1321 }
1322 }
1323
1324 fn to_campaign(
1325 &self,
1326 plan: &ApplicationInstallationPlan,
1327 schema: CampaignStateSchema,
1328 ) -> Result<ApplicationInstallationCampaign, InstallationCampaignError> {
1329 let invalid =
1330 || InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding);
1331 let campaign_id = ApplicationInstallationCampaignId::from_bytes(
1332 parse_hex16(&self.campaign_id).map_err(InstallationCampaignError::from_plan_error)?,
1333 )
1334 .map_err(|_| invalid())?;
1335 let plan_hash = ApplicationInstallationPlanHash::from_bytes(
1336 parse_hex32(&self.plan_hash).map_err(InstallationCampaignError::from_plan_error)?,
1337 );
1338 if plan_hash != plan.identity() {
1339 return Err(InstallationCampaignError::new(
1340 InstallationCampaignErrorKind::PlanIdentityMismatch,
1341 ));
1342 }
1343 let mut completed = self
1344 .completed
1345 .iter()
1346 .map(CampaignEvidenceDto::to_evidence)
1347 .collect::<Result<Vec<_>, _>>()?;
1348 if schema == CampaignStateSchema::V1 {
1349 let role_index = completed
1350 .iter()
1351 .position(|evidence| evidence.stage() == InstallationStage::Roles);
1352 if let Some(role_index) = role_index {
1353 completed.insert(
1354 role_index + 1,
1355 InstallationStageEvidence::Reimport(InstalledReimportEvidence::NotRequired),
1356 );
1357 }
1358 }
1359 let reimport = self
1360 .reimport
1361 .as_ref()
1362 .map(|value| ApplicationReimportCampaignV1::decode_canonical(value.as_bytes()))
1363 .transpose()
1364 .map_err(|_| invalid())?;
1365 if (schema != CampaignStateSchema::V3 && reimport.is_some())
1366 || (schema == CampaignStateSchema::V3 && reimport.is_none())
1367 {
1368 return Err(InstallationCampaignError::new(
1369 InstallationCampaignErrorKind::UnsupportedVersion,
1370 ));
1371 }
1372 let failure = self
1373 .failure
1374 .as_ref()
1375 .map(CampaignFailureDto::to_failure)
1376 .transpose()?;
1377 Ok(ApplicationInstallationCampaign {
1378 campaign_id,
1379 plan_hash,
1380 completed,
1381 failure,
1382 reimport,
1383 })
1384 }
1385}
1386
1387#[derive(Serialize, Deserialize)]
1388#[serde(tag = "stage", rename_all = "snake_case", deny_unknown_fields)]
1389enum CampaignEvidenceDto {
1390 Preflight {
1391 source_hash: String,
1392 lock_hash: String,
1393 manifest_hash: String,
1394 },
1395 Contract {
1396 version: u64,
1397 bundle_hash: String,
1398 },
1399 Migration {
1400 migration_hash: Option<String>,
1401 },
1402 QueryModules {
1403 artifacts: Vec<CampaignArtifactDto>,
1404 },
1405 ReactiveModules {
1406 artifacts: Vec<CampaignArtifactDto>,
1407 },
1408 Roles {
1409 roles: Vec<CampaignRoleDto>,
1410 },
1411 Reimport {
1412 status: String,
1413 export_manifest_hash: Option<String>,
1414 export_receipt_hash: Option<String>,
1415 portability_manifest_hash: Option<String>,
1416 reimport_receipt_hash: Option<String>,
1417 },
1418 Credentials {
1419 credentials: Vec<CampaignCredentialDto>,
1420 },
1421 DriverProof {
1422 drivers: Vec<String>,
1423 },
1424 Seeds {
1425 seeds: Vec<CampaignSeedDto>,
1426 },
1427 Receipt {
1428 receipt_hash: String,
1429 },
1430}
1431
1432impl CampaignEvidenceDto {
1433 fn from_evidence(evidence: &InstallationStageEvidence) -> Self {
1434 match evidence {
1435 InstallationStageEvidence::Preflight {
1436 source_hash,
1437 lock_hash,
1438 manifest_hash,
1439 } => Self::Preflight {
1440 source_hash: hex32(source_hash.as_bytes()),
1441 lock_hash: hex32(lock_hash.as_bytes()),
1442 manifest_hash: hex32(manifest_hash.as_bytes()),
1443 },
1444 InstallationStageEvidence::Contract {
1445 version,
1446 bundle_hash,
1447 } => Self::Contract {
1448 version: version.get(),
1449 bundle_hash: hex32(bundle_hash.as_bytes()),
1450 },
1451 InstallationStageEvidence::Migration { migration_hash } => Self::Migration {
1452 migration_hash: migration_hash.map(|hash| hex32(hash.as_bytes())),
1453 },
1454 InstallationStageEvidence::QueryModules(artifacts) => Self::QueryModules {
1455 artifacts: artifacts.iter().map(CampaignArtifactDto::from).collect(),
1456 },
1457 InstallationStageEvidence::ReactiveModules(artifacts) => Self::ReactiveModules {
1458 artifacts: artifacts.iter().map(CampaignArtifactDto::from).collect(),
1459 },
1460 InstallationStageEvidence::Roles(roles) => Self::Roles {
1461 roles: roles.iter().map(CampaignRoleDto::from).collect(),
1462 },
1463 InstallationStageEvidence::Reimport(InstalledReimportEvidence::NotRequired) => {
1464 Self::Reimport {
1465 status: "not_required".to_owned(),
1466 export_manifest_hash: None,
1467 export_receipt_hash: None,
1468 portability_manifest_hash: None,
1469 reimport_receipt_hash: None,
1470 }
1471 }
1472 InstallationStageEvidence::Reimport(InstalledReimportEvidence::Reconciled(
1473 InstalledReimportReceiptEvidence {
1474 export_manifest_hash,
1475 export_receipt_hash,
1476 portability_manifest_hash,
1477 reimport_receipt_hash,
1478 },
1479 )) => Self::Reimport {
1480 status: "reconciled".to_owned(),
1481 export_manifest_hash: Some(hex32(export_manifest_hash.as_bytes())),
1482 export_receipt_hash: Some(hex32(export_receipt_hash.as_bytes())),
1483 portability_manifest_hash: Some(hex32(portability_manifest_hash.as_bytes())),
1484 reimport_receipt_hash: Some(hex32(reimport_receipt_hash.as_bytes())),
1485 },
1486 InstallationStageEvidence::Credentials(credentials) => Self::Credentials {
1487 credentials: credentials
1488 .iter()
1489 .map(CampaignCredentialDto::from)
1490 .collect(),
1491 },
1492 InstallationStageEvidence::DriverProof(drivers) => Self::DriverProof {
1493 drivers: drivers
1494 .iter()
1495 .map(|driver| driver.tag().to_owned())
1496 .collect(),
1497 },
1498 InstallationStageEvidence::Seeds(seeds) => Self::Seeds {
1499 seeds: seeds.iter().map(CampaignSeedDto::from).collect(),
1500 },
1501 InstallationStageEvidence::Receipt(hash) => Self::Receipt {
1502 receipt_hash: hex32(hash.as_bytes()),
1503 },
1504 }
1505 }
1506
1507 fn to_evidence(&self) -> Result<InstallationStageEvidence, InstallationCampaignError> {
1508 let invalid =
1509 || InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding);
1510 let hex =
1511 |value: &str| parse_hex32(value).map_err(InstallationCampaignError::from_plan_error);
1512 Ok(match self {
1513 Self::Preflight {
1514 source_hash,
1515 lock_hash,
1516 manifest_hash,
1517 } => InstallationStageEvidence::Preflight {
1518 source_hash: ApplicationSourceHash::from_bytes(hex(source_hash)?),
1519 lock_hash: ApplicationLockHash::from_bytes(hex(lock_hash)?),
1520 manifest_hash: ApplicationManifestHash::from_bytes(hex(manifest_hash)?),
1521 },
1522 Self::Contract {
1523 version,
1524 bundle_hash,
1525 } => InstallationStageEvidence::Contract {
1526 version: ContractVersion::new(*version).ok_or_else(invalid)?,
1527 bundle_hash: ContractBundleHash::from_bytes(hex(bundle_hash)?),
1528 },
1529 Self::Migration { migration_hash } => InstallationStageEvidence::Migration {
1530 migration_hash: migration_hash
1531 .as_deref()
1532 .map(hex)
1533 .transpose()?
1534 .map(MigrationBundleHash::from_bytes),
1535 },
1536 Self::QueryModules { artifacts } => InstallationStageEvidence::QueryModules(
1537 artifacts
1538 .iter()
1539 .map(CampaignArtifactDto::to_artifact)
1540 .collect::<Result<Vec<_>, _>>()?,
1541 ),
1542 Self::ReactiveModules { artifacts } => InstallationStageEvidence::ReactiveModules(
1543 artifacts
1544 .iter()
1545 .map(CampaignArtifactDto::to_artifact)
1546 .collect::<Result<Vec<_>, _>>()?,
1547 ),
1548 Self::Roles { roles } => InstallationStageEvidence::Roles(
1549 roles
1550 .iter()
1551 .map(CampaignRoleDto::to_role)
1552 .collect::<Result<Vec<_>, _>>()?,
1553 ),
1554 Self::Reimport {
1555 status,
1556 export_manifest_hash,
1557 export_receipt_hash,
1558 portability_manifest_hash,
1559 reimport_receipt_hash,
1560 } => match status.as_str() {
1561 "not_required"
1562 if export_manifest_hash.is_none()
1563 && export_receipt_hash.is_none()
1564 && portability_manifest_hash.is_none()
1565 && reimport_receipt_hash.is_none() =>
1566 {
1567 InstallationStageEvidence::Reimport(InstalledReimportEvidence::NotRequired)
1568 }
1569 "reconciled"
1570 if export_manifest_hash.is_some()
1571 && export_receipt_hash.is_some()
1572 && portability_manifest_hash.is_some()
1573 && reimport_receipt_hash.is_some() =>
1574 {
1575 InstallationStageEvidence::Reimport(InstalledReimportEvidence::Reconciled(
1576 InstalledReimportReceiptEvidence {
1577 export_manifest_hash: ApplicationExportManifestHash::from_bytes(hex(
1578 export_manifest_hash.as_deref().ok_or_else(invalid)?,
1579 )?),
1580 export_receipt_hash: ApplicationExportReceiptHash::from_bytes(hex(
1581 export_receipt_hash.as_deref().ok_or_else(invalid)?,
1582 )?),
1583 portability_manifest_hash:
1584 ApplicationPortabilityManifestHash::from_bytes(hex(
1585 portability_manifest_hash.as_deref().ok_or_else(invalid)?,
1586 )?),
1587 reimport_receipt_hash: ApplicationReimportReceiptHash::from_bytes(hex(
1588 reimport_receipt_hash.as_deref().ok_or_else(invalid)?,
1589 )?),
1590 },
1591 ))
1592 }
1593 _ => return Err(invalid()),
1594 },
1595 Self::Credentials { credentials } => InstallationStageEvidence::Credentials(
1596 credentials
1597 .iter()
1598 .map(CampaignCredentialDto::to_credential)
1599 .collect::<Result<Vec<_>, _>>()?,
1600 ),
1601 Self::DriverProof { drivers } => InstallationStageEvidence::DriverProof(
1602 drivers
1603 .iter()
1604 .map(|driver| InstallationDriver::parse(driver).ok_or_else(invalid))
1605 .collect::<Result<Vec<_>, _>>()?,
1606 ),
1607 Self::Seeds { seeds } => InstallationStageEvidence::Seeds(
1608 seeds
1609 .iter()
1610 .map(CampaignSeedDto::to_seed)
1611 .collect::<Result<Vec<_>, _>>()?,
1612 ),
1613 Self::Receipt { receipt_hash } => InstallationStageEvidence::Receipt(
1614 ApplicationInstallationReceiptHash::from_bytes(hex(receipt_hash)?),
1615 ),
1616 })
1617 }
1618}
1619
1620#[derive(Serialize, Deserialize)]
1621#[serde(deny_unknown_fields)]
1622struct CampaignArtifactDto {
1623 kind: String,
1624 name: String,
1625 content_hash: String,
1626}
1627
1628impl From<&InstallationArtifact> for CampaignArtifactDto {
1629 fn from(artifact: &InstallationArtifact) -> Self {
1630 Self {
1631 kind: artifact.kind().tag().to_owned(),
1632 name: artifact.name().as_str().to_owned(),
1633 content_hash: hex32(artifact.content_hash().as_bytes()),
1634 }
1635 }
1636}
1637
1638impl CampaignArtifactDto {
1639 fn to_artifact(&self) -> Result<InstallationArtifact, InstallationCampaignError> {
1640 let invalid =
1641 || InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding);
1642 Ok(InstallationArtifact::new(
1643 InstallationArtifactKind::parse(&self.kind).ok_or_else(invalid)?,
1644 InstallationSymbol::new(self.name.clone())
1645 .map_err(InstallationCampaignError::from_plan_error)?,
1646 GeneratedArtifactHash::from_bytes(
1647 parse_hex32(&self.content_hash)
1648 .map_err(InstallationCampaignError::from_plan_error)?,
1649 ),
1650 ))
1651 }
1652}
1653
1654#[derive(Serialize, Deserialize)]
1655#[serde(deny_unknown_fields)]
1656struct CampaignRoleDto {
1657 name: String,
1658 role_hash: String,
1659}
1660
1661impl From<&InstalledRoleEvidence> for CampaignRoleDto {
1662 fn from(role: &InstalledRoleEvidence) -> Self {
1663 Self {
1664 name: role.name().as_str().to_owned(),
1665 role_hash: hex32(role.role_hash().as_bytes()),
1666 }
1667 }
1668}
1669
1670impl CampaignRoleDto {
1671 fn to_role(&self) -> Result<InstalledRoleEvidence, InstallationCampaignError> {
1672 Ok(InstalledRoleEvidence::new(
1673 InstallationSymbol::new(self.name.clone())
1674 .map_err(InstallationCampaignError::from_plan_error)?,
1675 ApplicationRoleHash::from_bytes(
1676 parse_hex32(&self.role_hash).map_err(InstallationCampaignError::from_plan_error)?,
1677 ),
1678 ))
1679 }
1680}
1681
1682#[derive(Serialize, Deserialize)]
1683#[serde(deny_unknown_fields)]
1684struct CampaignCredentialDto {
1685 destination: String,
1686 capability_id: String,
1687}
1688
1689impl From<&InstalledCredentialEvidence> for CampaignCredentialDto {
1690 fn from(credential: &InstalledCredentialEvidence) -> Self {
1691 Self {
1692 destination: credential.destination().as_str().to_owned(),
1693 capability_id: hex16(credential.capability_id().as_bytes()),
1694 }
1695 }
1696}
1697
1698impl CampaignCredentialDto {
1699 fn to_credential(&self) -> Result<InstalledCredentialEvidence, InstallationCampaignError> {
1700 let invalid =
1701 || InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding);
1702 Ok(InstalledCredentialEvidence::new(
1703 InstallationSymbol::new(self.destination.clone())
1704 .map_err(InstallationCampaignError::from_plan_error)?,
1705 CapabilityId::from_bytes(
1706 parse_hex16(&self.capability_id)
1707 .map_err(InstallationCampaignError::from_plan_error)?,
1708 )
1709 .map_err(|_| invalid())?,
1710 ))
1711 }
1712}
1713
1714#[derive(Serialize, Deserialize)]
1715#[serde(deny_unknown_fields)]
1716struct CampaignSeedDto {
1717 name: String,
1718 content_hash: String,
1719 succeeded: u64,
1720 replayed: u64,
1721}
1722
1723impl From<&InstalledSeedEvidence> for CampaignSeedDto {
1724 fn from(seed: &InstalledSeedEvidence) -> Self {
1725 Self {
1726 name: seed.name().as_str().to_owned(),
1727 content_hash: hex32(seed.content_hash().as_bytes()),
1728 succeeded: seed.succeeded(),
1729 replayed: seed.replayed(),
1730 }
1731 }
1732}
1733
1734impl CampaignSeedDto {
1735 fn to_seed(&self) -> Result<InstalledSeedEvidence, InstallationCampaignError> {
1736 Ok(InstalledSeedEvidence::new(
1737 InstallationSymbol::new(self.name.clone())
1738 .map_err(InstallationCampaignError::from_plan_error)?,
1739 GeneratedArtifactHash::from_bytes(
1740 parse_hex32(&self.content_hash)
1741 .map_err(InstallationCampaignError::from_plan_error)?,
1742 ),
1743 self.succeeded,
1744 self.replayed,
1745 ))
1746 }
1747}
1748
1749#[derive(Serialize, Deserialize)]
1750#[serde(deny_unknown_fields)]
1751struct CampaignFailureDto {
1752 stage: String,
1753 code: String,
1754 next_action: String,
1755}
1756
1757impl CampaignFailureDto {
1758 fn from_failure(failure: InstallationFailure) -> Self {
1759 Self {
1760 stage: failure.stage().as_str().to_owned(),
1761 code: installation_failure_code_tag(failure.code()).to_owned(),
1762 next_action: installation_next_action_tag(failure.next_action()).to_owned(),
1763 }
1764 }
1765
1766 fn to_failure(&self) -> Result<InstallationFailure, InstallationCampaignError> {
1767 let invalid =
1768 || InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding);
1769 Ok(InstallationFailure {
1770 stage: parse_installation_stage(&self.stage).ok_or_else(invalid)?,
1771 code: parse_installation_failure_code(&self.code).ok_or_else(invalid)?,
1772 next_action: parse_installation_next_action(&self.next_action).ok_or_else(invalid)?,
1773 })
1774 }
1775}
1776
1777const fn installation_failure_code_tag(code: InstallationFailureCode) -> &'static str {
1778 match code {
1779 InstallationFailureCode::LocalArtifactMismatch => "local_artifact_mismatch",
1780 InstallationFailureCode::RemoteIdentityMismatch => "remote_identity_mismatch",
1781 InstallationFailureCode::MigrationGateRequired => "migration_gate_required",
1782 InstallationFailureCode::RoleWideningApprovalRequired => "role_widening_approval_required",
1783 InstallationFailureCode::ReimportPartial => "reimport_partial",
1784 InstallationFailureCode::CredentialDestinationOccupied => "credential_destination_occupied",
1785 InstallationFailureCode::DriverProofFailed => "driver_proof_failed",
1786 InstallationFailureCode::SeedPartial => "seed_partial",
1787 InstallationFailureCode::AuthorizationDenied => "authorization_denied",
1788 InstallationFailureCode::ServiceUnavailable => "service_unavailable",
1789 }
1790}
1791
1792fn parse_installation_failure_code(value: &str) -> Option<InstallationFailureCode> {
1793 match value {
1794 "local_artifact_mismatch" => Some(InstallationFailureCode::LocalArtifactMismatch),
1795 "remote_identity_mismatch" => Some(InstallationFailureCode::RemoteIdentityMismatch),
1796 "migration_gate_required" => Some(InstallationFailureCode::MigrationGateRequired),
1797 "role_widening_approval_required" => {
1798 Some(InstallationFailureCode::RoleWideningApprovalRequired)
1799 }
1800 "reimport_partial" => Some(InstallationFailureCode::ReimportPartial),
1801 "credential_destination_occupied" => {
1802 Some(InstallationFailureCode::CredentialDestinationOccupied)
1803 }
1804 "driver_proof_failed" => Some(InstallationFailureCode::DriverProofFailed),
1805 "seed_partial" => Some(InstallationFailureCode::SeedPartial),
1806 "authorization_denied" => Some(InstallationFailureCode::AuthorizationDenied),
1807 "service_unavailable" => Some(InstallationFailureCode::ServiceUnavailable),
1808 _ => None,
1809 }
1810}
1811
1812const fn installation_next_action_tag(action: InstallationNextAction) -> &'static str {
1813 match action {
1814 InstallationNextAction::ValidateLocalArtifacts => "validate_local_artifacts",
1815 InstallationNextAction::DeployContract => "deploy_contract",
1816 InstallationNextAction::ApplyMigration => "apply_migration",
1817 InstallationNextAction::DeployQueryModules => "deploy_query_modules",
1818 InstallationNextAction::DeployReactiveModules => "deploy_reactive_modules",
1819 InstallationNextAction::ReconcileRoles => "reconcile_roles",
1820 InstallationNextAction::ReimportApplication => "reimport_application",
1821 InstallationNextAction::RotateCredentials => "rotate_credentials",
1822 InstallationNextAction::ProveDrivers => "prove_drivers",
1823 InstallationNextAction::RunSeeds => "run_seeds",
1824 InstallationNextAction::SealReceipt => "seal_receipt",
1825 InstallationNextAction::None => "none",
1826 }
1827}
1828
1829fn parse_installation_next_action(value: &str) -> Option<InstallationNextAction> {
1830 match value {
1831 "validate_local_artifacts" => Some(InstallationNextAction::ValidateLocalArtifacts),
1832 "deploy_contract" => Some(InstallationNextAction::DeployContract),
1833 "apply_migration" => Some(InstallationNextAction::ApplyMigration),
1834 "deploy_query_modules" => Some(InstallationNextAction::DeployQueryModules),
1835 "deploy_reactive_modules" => Some(InstallationNextAction::DeployReactiveModules),
1836 "reconcile_roles" => Some(InstallationNextAction::ReconcileRoles),
1837 "reimport_application" => Some(InstallationNextAction::ReimportApplication),
1838 "rotate_credentials" => Some(InstallationNextAction::RotateCredentials),
1839 "prove_drivers" => Some(InstallationNextAction::ProveDrivers),
1840 "run_seeds" => Some(InstallationNextAction::RunSeeds),
1841 "seal_receipt" => Some(InstallationNextAction::SealReceipt),
1842 "none" => Some(InstallationNextAction::None),
1843 _ => None,
1844 }
1845}
1846
1847fn parse_installation_stage(value: &str) -> Option<InstallationStage> {
1848 InstallationStage::ALL
1849 .into_iter()
1850 .find(|stage| stage.as_str() == value)
1851}
1852
1853#[derive(Deserialize)]
1854struct ReceiptSchemaProbe {
1855 schema: String,
1856}
1857
1858#[derive(Serialize, Deserialize)]
1859#[serde(deny_unknown_fields)]
1860struct ReceiptDtoV1 {
1861 schema: String,
1862 campaign_id: String,
1863 plan_hash: String,
1864 application: String,
1865 database: String,
1866 environment: String,
1867 lineage: String,
1868 contract_version: u64,
1869 contract_bundle_hash: String,
1870 source_hash: String,
1871 lock_hash: String,
1872 manifest_hash: String,
1873 artifacts: Vec<ReceiptArtifactDto>,
1874 roles: Vec<ReceiptRoleDto>,
1875 drivers: Vec<String>,
1876 seed_batches: u64,
1877 seed_items: u64,
1878 migration_hash: Option<String>,
1879 adapter_manifest_hash: Option<String>,
1880}
1881
1882#[derive(Serialize, Deserialize)]
1883#[serde(deny_unknown_fields)]
1884struct ReceiptDtoV2 {
1885 schema: String,
1886 campaign_id: String,
1887 plan_hash: String,
1888 application: String,
1889 database: String,
1890 environment: String,
1891 lineage: String,
1892 contract_version: u64,
1893 contract_bundle_hash: String,
1894 source_hash: String,
1895 lock_hash: String,
1896 manifest_hash: String,
1897 artifacts: Vec<ReceiptArtifactDto>,
1898 roles: Vec<ReceiptRoleDto>,
1899 credentials: Vec<ReceiptCredentialDto>,
1900 drivers: Vec<String>,
1901 seed_checkpoints: Vec<ReceiptSeedCheckpointDto>,
1902 migration_receipt: Option<ReceiptMigrationReferenceDto>,
1903 backup_receipt: Option<ReceiptBackupReferenceDto>,
1904 adapter_manifest_hash: Option<String>,
1905 #[serde(default, skip_serializing_if = "Option::is_none")]
1906 reimport: Option<ReceiptReimportReferenceDto>,
1907 terminal_state: String,
1908 safe_remediation: Vec<String>,
1909}
1910
1911#[derive(Serialize, Deserialize)]
1912#[serde(deny_unknown_fields)]
1913struct ReceiptArtifactDto {
1914 kind: String,
1915 name: String,
1916 content_hash: String,
1917}
1918
1919#[derive(Serialize, Deserialize)]
1920#[serde(deny_unknown_fields)]
1921struct ReceiptRoleDto {
1922 name: String,
1923 role_hash: String,
1924}
1925
1926#[derive(Serialize, Deserialize)]
1927#[serde(deny_unknown_fields)]
1928struct ReceiptCredentialDto {
1929 destination: String,
1930 role: String,
1931 capability_id: String,
1932}
1933
1934#[derive(Serialize, Deserialize)]
1935#[serde(deny_unknown_fields)]
1936struct ReceiptSeedCheckpointDto {
1937 name: String,
1938 content_hash: String,
1939 succeeded: u64,
1940 replayed: u64,
1941}
1942
1943#[derive(Serialize, Deserialize)]
1944#[serde(deny_unknown_fields)]
1945struct ReceiptMigrationReferenceDto {
1946 operation_id: String,
1947 migration_hash: String,
1948}
1949
1950#[derive(Serialize, Deserialize)]
1951#[serde(deny_unknown_fields)]
1952struct ReceiptBackupReferenceDto {
1953 migration_operation_id: String,
1954 policy: String,
1955}
1956
1957#[derive(Serialize, Deserialize)]
1958#[serde(deny_unknown_fields)]
1959struct ReceiptReimportReferenceDto {
1960 export_manifest_hash: String,
1961 export_receipt_hash: String,
1962 portability_manifest_hash: String,
1963 reimport_receipt_hash: String,
1964}
1965
1966impl ReceiptDtoV2 {
1967 fn from_campaign(
1968 campaign: &ApplicationInstallationCampaign,
1969 plan: &ApplicationInstallationPlan,
1970 ) -> Result<Self, InstallationCampaignError> {
1971 let input = plan.input();
1972 let credentials = campaign
1973 .completed
1974 .iter()
1975 .find_map(|evidence| match evidence {
1976 InstallationStageEvidence::Credentials(credentials) => Some(credentials),
1977 _ => None,
1978 })
1979 .ok_or_else(evidence_mismatch)?;
1980 let seed_checkpoints = campaign
1981 .completed
1982 .iter()
1983 .find_map(|evidence| match evidence {
1984 InstallationStageEvidence::Seeds(seeds) => Some(seeds),
1985 _ => None,
1986 })
1987 .ok_or_else(evidence_mismatch)?;
1988 let credential_receipts = credentials
1989 .iter()
1990 .map(|credential| {
1991 let destination = input
1992 .credential_destinations
1993 .iter()
1994 .find(|destination| destination.name() == credential.destination())
1995 .ok_or_else(evidence_mismatch)?;
1996 Ok(ReceiptCredentialDto {
1997 destination: credential.destination().as_str().to_owned(),
1998 role: destination.role().as_str().to_owned(),
1999 capability_id: hex16(credential.capability_id().as_bytes()),
2000 })
2001 })
2002 .collect::<Result<Vec<_>, InstallationCampaignError>>()?;
2003 let operation_id = input
2004 .migration
2005 .map(|_| hex16(campaign.campaign_id.as_bytes()));
2006 let reimport = campaign
2007 .completed
2008 .iter()
2009 .find_map(|evidence| match evidence {
2010 InstallationStageEvidence::Reimport(evidence) => Some(evidence),
2011 _ => None,
2012 })
2013 .ok_or_else(evidence_mismatch)?;
2014 let reimport = match (input.reimport, reimport) {
2015 (None, InstalledReimportEvidence::NotRequired) => None,
2016 (
2017 Some(expected),
2018 InstalledReimportEvidence::Reconciled(InstalledReimportReceiptEvidence {
2019 export_manifest_hash,
2020 export_receipt_hash,
2021 portability_manifest_hash,
2022 reimport_receipt_hash,
2023 }),
2024 ) if *export_manifest_hash == expected.export_manifest_hash()
2025 && *export_receipt_hash == expected.export_receipt_hash()
2026 && *portability_manifest_hash == expected.portability_manifest_hash() =>
2027 {
2028 Some(ReceiptReimportReferenceDto {
2029 export_manifest_hash: hex32(export_manifest_hash.as_bytes()),
2030 export_receipt_hash: hex32(export_receipt_hash.as_bytes()),
2031 portability_manifest_hash: hex32(portability_manifest_hash.as_bytes()),
2032 reimport_receipt_hash: hex32(reimport_receipt_hash.as_bytes()),
2033 })
2034 }
2035 _ => return Err(evidence_mismatch()),
2036 };
2037 let schema = if reimport.is_some() {
2038 APPLICATION_INSTALLATION_RECEIPT_SCHEMA_V3
2039 } else {
2040 APPLICATION_INSTALLATION_RECEIPT_SCHEMA_V2
2041 };
2042 Ok(Self {
2043 schema: schema.to_owned(),
2044 campaign_id: hex16(campaign.campaign_id.as_bytes()),
2045 plan_hash: hex32(plan.identity().as_bytes()),
2046 application: input.application.as_str().to_owned(),
2047 database: input.target.database().as_str().to_owned(),
2048 environment: input.target.environment().as_str().to_owned(),
2049 lineage: input.target.lineage().as_str().to_owned(),
2050 contract_version: input.contract.version().get(),
2051 contract_bundle_hash: hex32(input.contract.bundle_hash().as_bytes()),
2052 source_hash: hex32(input.source_hash.as_bytes()),
2053 lock_hash: hex32(input.lock_hash.as_bytes()),
2054 manifest_hash: hex32(input.manifest_hash.as_bytes()),
2055 artifacts: input
2056 .artifacts
2057 .iter()
2058 .map(|artifact| ReceiptArtifactDto {
2059 kind: artifact.kind().tag().to_owned(),
2060 name: artifact.name().as_str().to_owned(),
2061 content_hash: hex32(artifact.content_hash().as_bytes()),
2062 })
2063 .collect(),
2064 roles: input
2065 .roles
2066 .iter()
2067 .map(|role| ReceiptRoleDto {
2068 name: role.name().as_str().to_owned(),
2069 role_hash: hex32(role.role_hash().as_bytes()),
2070 })
2071 .collect(),
2072 credentials: credential_receipts,
2073 drivers: input
2074 .drivers
2075 .iter()
2076 .map(|driver| driver.tag().to_owned())
2077 .collect(),
2078 seed_checkpoints: seed_checkpoints
2079 .iter()
2080 .map(|seed| ReceiptSeedCheckpointDto {
2081 name: seed.name().as_str().to_owned(),
2082 content_hash: hex32(seed.content_hash().as_bytes()),
2083 succeeded: seed.succeeded(),
2084 replayed: seed.replayed(),
2085 })
2086 .collect(),
2087 migration_receipt: input.migration.zip(operation_id.as_ref()).map(
2088 |(migration, operation_id)| ReceiptMigrationReferenceDto {
2089 operation_id: operation_id.clone(),
2090 migration_hash: hex32(migration.migration_hash().as_bytes()),
2091 },
2092 ),
2093 backup_receipt: operation_id.map(|migration_operation_id| ReceiptBackupReferenceDto {
2094 migration_operation_id,
2095 policy: "required_verified".to_owned(),
2096 }),
2097 adapter_manifest_hash: input
2098 .adapter_manifest_hash
2099 .map(|hash| hex32(hash.as_bytes())),
2100 reimport,
2101 terminal_state: "installed".to_owned(),
2102 safe_remediation: Vec::new(),
2103 })
2104 }
2105}
2106
2107fn validate_receipt_v2_dto(dto: &ReceiptDtoV2) -> Result<(), InstallationCampaignError> {
2108 validate_receipt_dto(dto, APPLICATION_INSTALLATION_RECEIPT_SCHEMA_V2, false)
2109}
2110
2111fn validate_receipt_v3_dto(dto: &ReceiptDtoV2) -> Result<(), InstallationCampaignError> {
2112 validate_receipt_dto(dto, APPLICATION_INSTALLATION_RECEIPT_SCHEMA_V3, true)
2113}
2114
2115fn validate_receipt_dto(
2116 dto: &ReceiptDtoV2,
2117 expected_schema: &str,
2118 expects_reimport: bool,
2119) -> Result<(), InstallationCampaignError> {
2120 let invalid = || InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding);
2121 if dto.schema != expected_schema
2122 || dto.reimport.is_some() != expects_reimport
2123 || dto.terminal_state != "installed"
2124 || !dto.safe_remediation.is_empty()
2125 || dto.artifacts.is_empty()
2126 || dto.roles.is_empty()
2127 || dto.credentials.is_empty()
2128 || dto.artifacts.len() > crate::MAX_INSTALLATION_ARTIFACTS
2129 || dto.roles.len() > crate::MAX_INSTALLATION_ROLES
2130 || dto.credentials.len() > crate::MAX_CREDENTIAL_DESTINATIONS
2131 || dto.seed_checkpoints.len() > crate::MAX_INSTALLATION_SEEDS
2132 || dto.migration_receipt.is_some() != dto.backup_receipt.is_some()
2133 {
2134 return Err(invalid());
2135 }
2136 let campaign_id = parse_receipt_campaign_id(&dto.campaign_id)?;
2137 parse_receipt_plan_hash(&dto.plan_hash)?;
2138 InstallationSymbol::new(dto.application.clone())
2139 .map_err(InstallationCampaignError::from_plan_error)?;
2140 riffdb_types::DatabaseAlias::new(dto.database.clone()).map_err(|_| invalid())?;
2141 riffdb_types::Environment::new(dto.environment.clone()).map_err(|_| invalid())?;
2142 riffdb_types::ContractLineage::new(dto.lineage.clone()).map_err(|_| invalid())?;
2143 ContractVersion::new(dto.contract_version).ok_or_else(invalid)?;
2144 for hash in [
2145 &dto.contract_bundle_hash,
2146 &dto.source_hash,
2147 &dto.lock_hash,
2148 &dto.manifest_hash,
2149 ] {
2150 parse_hex32(hash).map_err(InstallationCampaignError::from_plan_error)?;
2151 }
2152 if let Some(reimport) = &dto.reimport {
2153 for hash in [
2154 &reimport.export_manifest_hash,
2155 &reimport.export_receipt_hash,
2156 &reimport.portability_manifest_hash,
2157 &reimport.reimport_receipt_hash,
2158 ] {
2159 parse_hex32(hash).map_err(InstallationCampaignError::from_plan_error)?;
2160 }
2161 }
2162 validate_receipt_artifacts(&dto.artifacts)?;
2163 validate_receipt_roles(&dto.roles)?;
2164 if !dto
2165 .artifacts
2166 .iter()
2167 .any(|artifact| artifact.kind == "manifest")
2168 || !dto
2169 .artifacts
2170 .iter()
2171 .any(|artifact| artifact.kind == "contract_bundle")
2172 {
2173 return Err(invalid());
2174 }
2175 let mut previous_credential: Option<InstallationSymbol> = None;
2176 for credential in &dto.credentials {
2177 let destination = InstallationSymbol::new(credential.destination.clone())
2178 .map_err(InstallationCampaignError::from_plan_error)?;
2179 InstallationSymbol::new(credential.role.clone())
2180 .map_err(InstallationCampaignError::from_plan_error)?;
2181 if !dto.roles.iter().any(|role| role.name == credential.role) {
2182 return Err(invalid());
2183 }
2184 CapabilityId::from_bytes(
2185 parse_hex16(&credential.capability_id)
2186 .map_err(InstallationCampaignError::from_plan_error)?,
2187 )
2188 .map_err(|_| invalid())?;
2189 if previous_credential
2190 .as_ref()
2191 .is_some_and(|previous| previous >= &destination)
2192 {
2193 return Err(invalid());
2194 }
2195 previous_credential = Some(destination);
2196 }
2197 if dto.roles.iter().any(|role| {
2198 !dto.credentials
2199 .iter()
2200 .any(|credential| credential.role == role.name)
2201 }) {
2202 return Err(invalid());
2203 }
2204 validate_receipt_drivers(&dto.drivers)?;
2205 let mut previous_seed: Option<InstallationSymbol> = None;
2206 for seed in &dto.seed_checkpoints {
2207 let name = InstallationSymbol::new(seed.name.clone())
2208 .map_err(InstallationCampaignError::from_plan_error)?;
2209 parse_hex32(&seed.content_hash).map_err(InstallationCampaignError::from_plan_error)?;
2210 let item_count = seed
2211 .succeeded
2212 .checked_add(seed.replayed)
2213 .ok_or_else(invalid)?;
2214 if item_count == 0
2215 || item_count > crate::MAX_SEED_BATCH_ITEMS
2216 || previous_seed
2217 .as_ref()
2218 .is_some_and(|previous| previous >= &name)
2219 {
2220 return Err(invalid());
2221 }
2222 previous_seed = Some(name);
2223 }
2224 if let (Some(migration), Some(backup)) = (&dto.migration_receipt, &dto.backup_receipt) {
2225 let operation_id = parse_receipt_operation_id(&migration.operation_id)?;
2226 let backup_operation_id = parse_receipt_operation_id(&backup.migration_operation_id)?;
2227 if operation_id.into_bytes() != campaign_id.into_bytes()
2228 || operation_id != backup_operation_id
2229 || backup.policy != "required_verified"
2230 {
2231 return Err(invalid());
2232 }
2233 parse_hex32(&migration.migration_hash)
2234 .map_err(InstallationCampaignError::from_plan_error)?;
2235 }
2236 if let Some(hash) = &dto.adapter_manifest_hash {
2237 parse_hex32(hash).map_err(InstallationCampaignError::from_plan_error)?;
2238 }
2239 Ok(())
2240}
2241
2242fn validate_receipt_v1_dto(dto: &ReceiptDtoV1) -> Result<(), InstallationCampaignError> {
2243 let invalid = || InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding);
2244 ApplicationInstallationCampaignId::from_bytes(
2245 parse_hex16(&dto.campaign_id).map_err(InstallationCampaignError::from_plan_error)?,
2246 )
2247 .map_err(|_| invalid())?;
2248 parse_hex32(&dto.plan_hash).map_err(InstallationCampaignError::from_plan_error)?;
2249 InstallationSymbol::new(dto.application.clone())
2250 .map_err(InstallationCampaignError::from_plan_error)?;
2251 riffdb_types::DatabaseAlias::new(dto.database.clone()).map_err(|_| invalid())?;
2252 riffdb_types::Environment::new(dto.environment.clone()).map_err(|_| invalid())?;
2253 riffdb_types::ContractLineage::new(dto.lineage.clone()).map_err(|_| invalid())?;
2254 ContractVersion::new(dto.contract_version).ok_or_else(invalid)?;
2255 parse_hex32(&dto.contract_bundle_hash).map_err(InstallationCampaignError::from_plan_error)?;
2256 parse_hex32(&dto.source_hash).map_err(InstallationCampaignError::from_plan_error)?;
2257 parse_hex32(&dto.lock_hash).map_err(InstallationCampaignError::from_plan_error)?;
2258 parse_hex32(&dto.manifest_hash).map_err(InstallationCampaignError::from_plan_error)?;
2259 let mut previous_artifact: Option<(InstallationArtifactKind, InstallationSymbol)> = None;
2260 for artifact in &dto.artifacts {
2261 let kind = InstallationArtifactKind::parse(&artifact.kind).ok_or_else(invalid)?;
2262 let name = InstallationSymbol::new(artifact.name.clone())
2263 .map_err(InstallationCampaignError::from_plan_error)?;
2264 parse_hex32(&artifact.content_hash).map_err(InstallationCampaignError::from_plan_error)?;
2265 if previous_artifact
2266 .as_ref()
2267 .is_some_and(|previous| previous >= &(kind, name.clone()))
2268 {
2269 return Err(invalid());
2270 }
2271 previous_artifact = Some((kind, name));
2272 }
2273 let mut previous_role: Option<InstallationSymbol> = None;
2274 for role in &dto.roles {
2275 let name = InstallationSymbol::new(role.name.clone())
2276 .map_err(InstallationCampaignError::from_plan_error)?;
2277 parse_hex32(&role.role_hash).map_err(InstallationCampaignError::from_plan_error)?;
2278 if previous_role
2279 .as_ref()
2280 .is_some_and(|previous| previous >= &name)
2281 {
2282 return Err(invalid());
2283 }
2284 previous_role = Some(name);
2285 }
2286 let mut drivers = dto
2287 .drivers
2288 .iter()
2289 .map(|driver| InstallationDriver::parse(driver).ok_or_else(invalid))
2290 .collect::<Result<Vec<_>, _>>()?;
2291 let original = drivers.clone();
2292 drivers.sort();
2293 drivers.dedup();
2294 if original != drivers || dto.seed_batches > crate::MAX_INSTALLATION_SEEDS as u64 {
2295 return Err(invalid());
2296 }
2297 if let Some(hash) = &dto.migration_hash {
2298 parse_hex32(hash).map_err(InstallationCampaignError::from_plan_error)?;
2299 }
2300 if let Some(hash) = &dto.adapter_manifest_hash {
2301 parse_hex32(hash).map_err(InstallationCampaignError::from_plan_error)?;
2302 }
2303 Ok(())
2304}
2305
2306fn evidence_mismatch() -> InstallationCampaignError {
2307 InstallationCampaignError::new(InstallationCampaignErrorKind::EvidenceMismatch)
2308}
2309
2310fn parse_receipt_campaign_id(
2311 value: &str,
2312) -> Result<ApplicationInstallationCampaignId, InstallationCampaignError> {
2313 ApplicationInstallationCampaignId::from_bytes(
2314 parse_hex16(value).map_err(InstallationCampaignError::from_plan_error)?,
2315 )
2316 .map_err(|_| InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding))
2317}
2318
2319fn parse_receipt_plan_hash(
2320 value: &str,
2321) -> Result<ApplicationInstallationPlanHash, InstallationCampaignError> {
2322 Ok(ApplicationInstallationPlanHash::from_bytes(
2323 parse_hex32(value).map_err(InstallationCampaignError::from_plan_error)?,
2324 ))
2325}
2326
2327fn parse_receipt_operation_id(
2328 value: &str,
2329) -> Result<ContractMigrationOperationId, InstallationCampaignError> {
2330 ContractMigrationOperationId::from_bytes(
2331 parse_hex16(value).map_err(InstallationCampaignError::from_plan_error)?,
2332 )
2333 .map_err(|_| InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding))
2334}
2335
2336fn validate_receipt_artifacts(
2337 artifacts: &[ReceiptArtifactDto],
2338) -> Result<(), InstallationCampaignError> {
2339 let invalid = || InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding);
2340 let mut previous: Option<(InstallationArtifactKind, InstallationSymbol)> = None;
2341 for artifact in artifacts {
2342 let kind = InstallationArtifactKind::parse(&artifact.kind).ok_or_else(invalid)?;
2343 let name = InstallationSymbol::new(artifact.name.clone())
2344 .map_err(InstallationCampaignError::from_plan_error)?;
2345 parse_hex32(&artifact.content_hash).map_err(InstallationCampaignError::from_plan_error)?;
2346 if previous
2347 .as_ref()
2348 .is_some_and(|prior| prior >= &(kind, name.clone()))
2349 {
2350 return Err(invalid());
2351 }
2352 previous = Some((kind, name));
2353 }
2354 Ok(())
2355}
2356
2357fn validate_receipt_roles(roles: &[ReceiptRoleDto]) -> Result<(), InstallationCampaignError> {
2358 let invalid = || InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding);
2359 let mut previous: Option<InstallationSymbol> = None;
2360 for role in roles {
2361 let name = InstallationSymbol::new(role.name.clone())
2362 .map_err(InstallationCampaignError::from_plan_error)?;
2363 parse_hex32(&role.role_hash).map_err(InstallationCampaignError::from_plan_error)?;
2364 if previous.as_ref().is_some_and(|prior| prior >= &name) {
2365 return Err(invalid());
2366 }
2367 previous = Some(name);
2368 }
2369 Ok(())
2370}
2371
2372fn validate_receipt_drivers(drivers: &[String]) -> Result<(), InstallationCampaignError> {
2373 let invalid = || InstallationCampaignError::new(InstallationCampaignErrorKind::InvalidEncoding);
2374 let mut parsed = drivers
2375 .iter()
2376 .map(|driver| InstallationDriver::parse(driver).ok_or_else(invalid))
2377 .collect::<Result<Vec<_>, _>>()?;
2378 let original = parsed.clone();
2379 parsed.sort();
2380 parsed.dedup();
2381 if original != parsed {
2382 return Err(invalid());
2383 }
2384 Ok(())
2385}
2386
2387fn hex32(bytes: &[u8; 32]) -> String {
2388 crate::plan::hex32(bytes)
2389}
2390
2391fn hex16(bytes: &[u8; 16]) -> String {
2392 crate::plan::hex16(bytes)
2393}
2394
2395#[cfg(test)]
2396mod tests {
2397 use super::InstallationStage;
2398 use crate::APPLICATION_INSTALLATION_PLAN_SCHEMA_V1;
2399
2400 #[test]
2401 fn installation_stages_are_closed_and_dependency_ordered() {
2402 assert_eq!(
2403 APPLICATION_INSTALLATION_PLAN_SCHEMA_V1,
2404 "riffdb.application-installation-plan/v1"
2405 );
2406 assert_eq!(InstallationStage::ALL.len(), 11);
2407 assert_eq!(InstallationStage::ALL[0], InstallationStage::Preflight);
2408 assert_eq!(InstallationStage::ALL[6], InstallationStage::Reimport);
2409 assert_eq!(InstallationStage::ALL[10], InstallationStage::Receipt);
2410 assert_eq!(InstallationStage::ALL[10].as_str(), "receipt");
2411 }
2412}