Skip to main content

riffdb_types/
application_plan.rs

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
14/// Canonical schema version for exact application installation plans.
15pub const APPLICATION_INSTALLATION_PLAN_SCHEMA_V1: &str = "riffdb.application-installation-plan/v1";
16/// Canonical schema version for plans carrying exact reimport authority.
17pub const APPLICATION_INSTALLATION_PLAN_SCHEMA_V2: &str = "riffdb.application-installation-plan/v2";
18/// Canonical schema version for plans carrying secret-output authority atoms.
19pub const APPLICATION_INSTALLATION_PLAN_SCHEMA_V3: &str = "riffdb.application-installation-plan/v3";
20/// Maximum canonical bytes in one installation plan.
21pub const MAX_INSTALLATION_PLAN_BYTES: usize = 4 * 1_024 * 1_024;
22/// Maximum exact artifacts in one plan.
23pub const MAX_INSTALLATION_ARTIFACTS: usize = 256;
24/// Maximum symbolic application roles in one plan.
25pub const MAX_INSTALLATION_ROLES: usize = 128;
26/// Maximum credential destinations in one plan.
27pub const MAX_CREDENTIAL_DESTINATIONS: usize = 256;
28/// Maximum bounded seed batches in one plan.
29pub const MAX_INSTALLATION_SEEDS: usize = 256;
30/// Maximum ordinary commands represented by one seed batch.
31pub const MAX_SEED_BATCH_ITEMS: u64 = 1_000_000;
32/// Maximum symbolic operations in one application role.
33pub const MAX_ROLE_OPERATIONS: usize = 1_024;
34/// Maximum symbolic secret-output atoms in one application role.
35pub const MAX_ROLE_SECRET_OUTPUTS: usize = 1_024;
36/// Maximum total authority atoms in one application role.
37pub const MAX_ROLE_AUTHORITY_ATOMS: usize = 2_048;
38/// Maximum bytes in one installation-local symbolic name.
39pub const MAX_INSTALLATION_SYMBOL_BYTES: usize = 256;
40
41/// A bounded, non-secret symbol retained in plans and diagnostics.
42#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
43pub struct InstallationSymbol(String);
44
45impl InstallationSymbol {
46    /// Validates one exact source-like symbol without normalization.
47    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    /// Borrows the exact symbol.
71    #[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/// Exact selected database, environment, and application lineage.
87#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct InstallationTarget {
89    database: DatabaseAlias,
90    environment: Environment,
91    lineage: ContractLineage,
92}
93
94impl InstallationTarget {
95    /// Creates an exact installation authority target.
96    #[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    /// Selected canonical database alias.
110    #[must_use]
111    pub const fn database(&self) -> &DatabaseAlias {
112        &self.database
113    }
114
115    /// Selected exact deployment environment.
116    #[must_use]
117    pub const fn environment(&self) -> &Environment {
118        &self.environment
119    }
120
121    /// Selected application lineage.
122    #[must_use]
123    pub const fn lineage(&self) -> &ContractLineage {
124        &self.lineage
125    }
126}
127
128/// Candidate contract identity pinned by the exact lock.
129#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
130pub struct InstallationContract {
131    version: ContractVersion,
132    bundle_hash: ContractBundleHash,
133}
134
135impl InstallationContract {
136    /// Binds one exact candidate version and bundle.
137    #[must_use]
138    pub const fn new(version: ContractVersion, bundle_hash: ContractBundleHash) -> Self {
139        Self {
140            version,
141            bundle_hash,
142        }
143    }
144
145    /// Exact candidate version.
146    #[must_use]
147    pub const fn version(self) -> ContractVersion {
148        self.version
149    }
150
151    /// Exact candidate bundle hash.
152    #[must_use]
153    pub const fn bundle_hash(self) -> ContractBundleHash {
154        self.bundle_hash
155    }
156}
157
158/// Closed compiler-owned artifact class.
159#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
160pub enum InstallationArtifactKind {
161    /// Canonical application manifest.
162    Manifest,
163    /// Canonical contract bundle.
164    ContractBundle,
165    /// Named-query module.
166    QueryModule,
167    /// Reactive module.
168    ReactiveModule,
169    /// Compiled migration bundle.
170    MigrationBundle,
171    /// Rust generated bindings.
172    Rust,
173    /// TypeScript generated bindings.
174    TypeScript,
175    /// Go generated bindings.
176    Go,
177    /// Python generated bindings.
178    Python,
179    /// Generated MCP catalog.
180    Mcp,
181    /// Bounded seed input.
182    SeedInput,
183    /// Adapter conformance manifest.
184    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/// One exact compiler-produced artifact without a host filesystem path.
227#[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    /// Creates one name-addressed exact artifact reference.
236    #[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    /// Closed artifact kind.
250    #[must_use]
251    pub const fn kind(&self) -> InstallationArtifactKind {
252        self.kind
253    }
254
255    /// Symbolic artifact name, never a host path.
256    #[must_use]
257    pub const fn name(&self) -> &InstallationSymbol {
258        &self.name
259    }
260
261    /// Exact generated content identity.
262    #[must_use]
263    pub const fn content_hash(&self) -> GeneratedArtifactHash {
264        self.content_hash
265    }
266}
267
268/// Closed symbolic operation category used only to compute authority diffs.
269#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
270pub enum RoleOperationKind {
271    /// Named query.
272    Query,
273    /// Compiled command.
274    Command,
275    /// Durable event stream.
276    EventStream,
277    /// Live named-query watch.
278    QueryWatch,
279    /// Contextual agent subscription.
280    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/// One symbolic application operation in an authority diff.
309#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
310pub struct RoleOperation {
311    kind: RoleOperationKind,
312    name: InstallationSymbol,
313}
314
315/// One symbolic secret output granted to one exact named query.
316#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
317pub struct QuerySecretOutput {
318    query: InstallationSymbol,
319    entity: InstallationSymbol,
320    field: InstallationSymbol,
321}
322
323impl QuerySecretOutput {
324    /// Creates a value-free secret-output authority atom.
325    #[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    /// Exact named-query symbol.
339    #[must_use]
340    pub const fn query(&self) -> &InstallationSymbol {
341        &self.query
342    }
343
344    /// Exact entity symbol.
345    #[must_use]
346    pub const fn entity(&self) -> &InstallationSymbol {
347        &self.entity
348    }
349
350    /// Exact secret field symbol.
351    #[must_use]
352    pub const fn field(&self) -> &InstallationSymbol {
353        &self.field
354    }
355}
356
357/// Closed symbolic vocabulary used by installation authority diffs.
358#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
359pub enum RoleAuthorityAtom {
360    /// One invocable application operation.
361    Operation(RoleOperation),
362    /// One exact secret output of one exact named query.
363    QuerySecretOutput(QuerySecretOutput),
364}
365
366/// Independently canonical bounded authority for one application role.
367#[derive(Clone, Debug, Eq, PartialEq)]
368pub struct RoleAuthoritySet {
369    operations: Vec<RoleOperation>,
370    secret_outputs: Vec<QuerySecretOutput>,
371}
372
373impl RoleAuthoritySet {
374    /// Validates and canonicalizes one complete symbolic authority set.
375    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    /// Canonical operation atoms.
396    #[must_use]
397    pub fn operations(&self) -> &[RoleOperation] {
398        &self.operations
399    }
400
401    /// Canonical named-query secret-output atoms.
402    #[must_use]
403    pub fn secret_outputs(&self) -> &[QuerySecretOutput] {
404        &self.secret_outputs
405    }
406
407    /// Complete canonical closed authority vocabulary.
408    #[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    /// Creates one symbolic operation.
430    #[must_use]
431    pub const fn new(kind: RoleOperationKind, name: InstallationSymbol) -> Self {
432        Self { kind, name }
433    }
434
435    /// Operation category.
436    #[must_use]
437    pub const fn kind(&self) -> RoleOperationKind {
438        self.kind
439    }
440
441    /// Exact operation symbol.
442    #[must_use]
443    pub const fn name(&self) -> &InstallationSymbol {
444        &self.name
445    }
446}
447
448/// Exact approval for only the additions in one existing role.
449#[derive(Clone, Debug, Eq, PartialEq)]
450pub struct RoleWideningApproval {
451    expected_previous_role: ApplicationRoleHash,
452    additions: Vec<RoleAuthorityAtom>,
453}
454
455impl RoleWideningApproval {
456    /// Creates an approval whose exact additions are checked during compilation.
457    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    /// Creates an exact, already-canonical approval over the closed authority vocabulary.
477    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    /// Exact predecessor role identity being widened.
503    #[must_use]
504    pub const fn expected_previous_role(&self) -> ApplicationRoleHash {
505        self.expected_previous_role
506    }
507
508    /// Exact symbolic additions approved by the operator.
509    #[must_use]
510    pub fn additions(&self) -> &[RoleAuthorityAtom] {
511        &self.additions
512    }
513}
514
515/// One desired exact role plus enough predecessor state to compute its authority diff.
516#[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    /// Creates one exact initial or successor role.
528    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    /// Creates one exact initial or successor role over the complete closed authority vocabulary.
547    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    /// Symbolic role name.
600    #[must_use]
601    pub const fn name(&self) -> &InstallationSymbol {
602        &self.name
603    }
604
605    /// Exact desired role identity.
606    #[must_use]
607    pub const fn role_hash(&self) -> ApplicationRoleHash {
608        self.role_hash
609    }
610
611    /// Exact predecessor identity, when reconciling an existing role.
612    #[must_use]
613    pub const fn previous_role_hash(&self) -> Option<ApplicationRoleHash> {
614        self.previous_role_hash
615    }
616
617    /// Desired symbolic operation surface.
618    #[must_use]
619    pub fn desired_operations(&self) -> &[RoleOperation] {
620        self.desired_authority.operations()
621    }
622
623    /// Existing symbolic operation surface used to compute the diff.
624    #[must_use]
625    pub fn previous_operations(&self) -> &[RoleOperation] {
626        self.previous_authority.operations()
627    }
628
629    /// Desired named-query secret-output authority.
630    #[must_use]
631    pub fn desired_secret_outputs(&self) -> &[QuerySecretOutput] {
632        self.desired_authority.secret_outputs()
633    }
634
635    /// Previously observed named-query secret-output authority.
636    #[must_use]
637    pub fn previous_secret_outputs(&self) -> &[QuerySecretOutput] {
638        self.previous_authority.secret_outputs()
639    }
640
641    /// Complete desired authority set.
642    #[must_use]
643    pub const fn desired_authority(&self) -> &RoleAuthoritySet {
644        &self.desired_authority
645    }
646
647    /// Complete predecessor authority set.
648    #[must_use]
649    pub const fn previous_authority(&self) -> &RoleAuthoritySet {
650        &self.previous_authority
651    }
652
653    /// Exact role-widening approval, present only when additions exist.
654    #[must_use]
655    pub const fn widening_approval(&self) -> Option<&RoleWideningApproval> {
656        self.widening_approval.as_ref()
657    }
658}
659
660/// One credential destination with exact no-overwrite semantics.
661#[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    /// Creates one empty-slot install or exact-predecessor replacement.
671    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    /// Symbolic destination name; this is never a host path.
691    #[must_use]
692    pub const fn name(&self) -> &InstallationSymbol {
693        &self.name
694    }
695
696    /// Symbolic role whose credential is installed.
697    #[must_use]
698    pub const fn role(&self) -> &InstallationSymbol {
699        &self.role
700    }
701
702    /// Exact current capability that may be replaced, or `None` for must-be-empty.
703    #[must_use]
704    pub const fn expected_current(&self) -> Option<CapabilityId> {
705        self.expected_current
706    }
707
708    /// Caller-stable successor capability identity; credential bytes are never retained.
709    #[must_use]
710    pub const fn successor(&self) -> CapabilityId {
711        self.successor
712    }
713}
714
715/// Only backup posture admitted for an installation-driven migration.
716#[derive(Clone, Copy, Debug, Eq, PartialEq)]
717pub enum MigrationBackupPolicy {
718    /// Create and verify the operation-owned backup before staging.
719    RequiredVerified,
720}
721
722/// Only downtime posture admitted by the current offline migration engine.
723#[derive(Clone, Copy, Debug, Eq, PartialEq)]
724pub enum MigrationDowntimeClass {
725    /// Reject application work for the complete migration operation.
726    OfflineExclusive,
727}
728
729/// Exact, explicitly confirmed migration gate.
730#[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    /// Creates the only safe migration shape admitted by an installation plan.
744    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    /// Exact predecessor version.
770    #[must_use]
771    pub const fn parent_version(self) -> ContractVersion {
772        self.parent_version
773    }
774
775    /// Exact predecessor bundle.
776    #[must_use]
777    pub const fn parent_bundle_hash(self) -> ContractBundleHash {
778        self.parent_bundle_hash
779    }
780
781    /// Exact successor version.
782    #[must_use]
783    pub const fn successor_version(self) -> ContractVersion {
784        self.successor_version
785    }
786
787    /// Exact successor bundle.
788    #[must_use]
789    pub const fn successor_bundle_hash(self) -> ContractBundleHash {
790        self.successor_bundle_hash
791    }
792
793    /// Exact compiled migration identity.
794    #[must_use]
795    pub const fn migration_hash(self) -> MigrationBundleHash {
796        self.migration_hash
797    }
798
799    /// Exact caller confirmation; always equal to the migration hash.
800    #[must_use]
801    pub const fn confirmation(self) -> MigrationBundleHash {
802        self.confirmation
803    }
804
805    /// Required backup policy.
806    #[must_use]
807    pub const fn backup_policy(self) -> MigrationBackupPolicy {
808        self.backup_policy
809    }
810
811    /// Required downtime class.
812    #[must_use]
813    pub const fn downtime(self) -> MigrationDowntimeClass {
814        self.downtime
815    }
816}
817
818/// Exact completed portability export selected for an empty-destination reimport.
819#[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    /// Binds the source receipt and compiler-closed portability mapping exactly.
828    #[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    /// Exact completed export manifest identity.
842    #[must_use]
843    pub const fn export_manifest_hash(self) -> ApplicationExportManifestHash {
844        self.export_manifest_hash
845    }
846
847    /// Exact completed portability-intent export receipt identity.
848    #[must_use]
849    pub const fn export_receipt_hash(self) -> ApplicationExportReceiptHash {
850        self.export_receipt_hash
851    }
852
853    /// Exact compiler-closed portability manifest identity.
854    #[must_use]
855    pub const fn portability_manifest_hash(self) -> ApplicationPortabilityManifestHash {
856        self.portability_manifest_hash
857    }
858}
859
860/// First-class stable driver targets that must prove the installed identity.
861#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
862pub enum InstallationDriver {
863    /// First-party Rust client.
864    Rust,
865    /// First-party TypeScript client.
866    TypeScript,
867    /// First-party Go client.
868    Go,
869    /// First-party Python client.
870    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/// Closed alpha application feature catalog consumed by installation planning.
897#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
898pub enum InstallationFeature {
899    /// Authenticated remote TLS transport.
900    RemoteTls,
901    /// Compiler-visible bounded bulk commands.
902    BulkCommands,
903    /// Bounded operational RiffQL.
904    OperationalQueries,
905    /// Fenced workflow concurrency.
906    WorkflowConcurrency,
907    /// Principal-aware row policies.
908    RowPolicies,
909    /// Programmatic exact installation campaigns.
910    InstallationCampaigns,
911    /// Public export and compatibility lifecycle.
912    DataLifecycle,
913    /// Exact reviewed secret outputs from named RiffQL queries.
914    QuerySecretOutputs,
915}
916
917impl InstallationFeature {
918    /// Complete closed alpha feature catalog in canonical order.
919    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    /// Adapter-owned alpha features in canonical conformance order.
931    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/// One bounded seed batch whose items remain ordinary compiled commands.
972#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
973pub struct InstallationSeed {
974    name: InstallationSymbol,
975    content_hash: GeneratedArtifactHash,
976    item_count: u64,
977}
978
979/// Bounded terminal counters for one ordinary-command seed batch.
980#[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    /// Creates exact seed completion evidence without retaining item values.
990    #[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    /// Symbolic seed name.
1006    #[must_use]
1007    pub const fn name(&self) -> &InstallationSymbol {
1008        &self.name
1009    }
1010
1011    /// Exact seed artifact identity.
1012    #[must_use]
1013    pub const fn content_hash(&self) -> GeneratedArtifactHash {
1014        self.content_hash
1015    }
1016
1017    /// Newly completed command items.
1018    #[must_use]
1019    pub const fn succeeded(&self) -> u64 {
1020        self.succeeded
1021    }
1022
1023    /// Previously completed command items recovered by idempotency.
1024    #[must_use]
1025    pub const fn replayed(&self) -> u64 {
1026        self.replayed
1027    }
1028}
1029
1030impl InstallationSeed {
1031    /// Binds one seed artifact and its exact nonzero item count.
1032    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    /// Symbolic seed name.
1050    #[must_use]
1051    pub const fn name(&self) -> &InstallationSymbol {
1052        &self.name
1053    }
1054
1055    /// Exact content identity without exposing seed values.
1056    #[must_use]
1057    pub const fn content_hash(&self) -> GeneratedArtifactHash {
1058        self.content_hash
1059    }
1060
1061    /// Number of individually idempotent ordinary command items.
1062    #[must_use]
1063    pub const fn item_count(&self) -> u64 {
1064        self.item_count
1065    }
1066}
1067
1068/// Complete local input to deterministic installation-plan compilation.
1069#[derive(Clone, Debug, Eq, PartialEq)]
1070pub struct ApplicationInstallationPlanInput {
1071    /// Symbolic application name.
1072    pub application: InstallationSymbol,
1073    /// Exact author-source identity.
1074    pub source_hash: ApplicationSourceHash,
1075    /// Exact compiler-owned lock identity.
1076    pub lock_hash: ApplicationLockHash,
1077    /// Exact application-manifest identity.
1078    pub manifest_hash: ApplicationManifestHash,
1079    /// Exact installation authority target.
1080    pub target: InstallationTarget,
1081    /// Exact candidate contract.
1082    pub contract: InstallationContract,
1083    /// Complete exact generated artifact set.
1084    pub artifacts: Vec<InstallationArtifact>,
1085    /// Optional explicit offline migration gate.
1086    pub migration: Option<InstallationMigration>,
1087    /// Exact symbolic desired roles and predecessor authority observations.
1088    pub roles: Vec<InstallationRole>,
1089    /// Optional exact completed portability export reconstituted before credentials exist.
1090    pub reimport: Option<InstallationReimport>,
1091    /// Exact empty-or-predecessor-bound credential destinations.
1092    pub credential_destinations: Vec<CredentialDestination>,
1093    /// Public drivers that must prove the installed identity.
1094    pub drivers: Vec<InstallationDriver>,
1095    /// Bounded ordinary-command seed batches.
1096    pub seeds: Vec<InstallationSeed>,
1097    /// Closed features required by the application.
1098    pub required_features: Vec<InstallationFeature>,
1099    /// Optional exact adapter conformance manifest identity.
1100    pub adapter_manifest_hash: Option<AdapterConformanceManifestHash>,
1101}
1102
1103/// One immutable content-addressed installation plan.
1104#[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    /// Compiles a canonical plan without remote mutation.
1141    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    /// Strictly decodes canonical bytes and revalidates every semantic gate.
1165    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    /// Exact plan identity.
1189    #[must_use]
1190    pub const fn identity(&self) -> ApplicationInstallationPlanHash {
1191        self.identity
1192    }
1193
1194    /// Complete validated input retained by the plan.
1195    #[must_use]
1196    pub const fn input(&self) -> &ApplicationInstallationPlanInput {
1197        &self.input
1198    }
1199
1200    /// Canonical compatibility bytes.
1201    #[must_use]
1202    pub fn canonical_bytes(&self) -> &[u8] {
1203        &self.canonical_bytes
1204    }
1205
1206    /// Canonical installation-plan schema selected by the least-sufficient writer.
1207    #[must_use]
1208    pub const fn schema(&self) -> &'static str {
1209        self.schema.name()
1210    }
1211
1212    /// Validates exact public-driver proof for a transport submission.
1213    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    /// Validates bounded seed receipts for a transport submission.
1227    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/// Closed deterministic plan-compilation failure.
1267#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1268pub enum InstallationPlanErrorKind {
1269    /// A bounded symbolic identifier is invalid.
1270    InvalidIdentifier,
1271    /// One hard input or encoded-size bound was exceeded.
1272    LimitExceeded,
1273    /// A supposedly unique symbolic key was duplicated.
1274    Duplicate,
1275    /// Exact source, lock, manifest, contract, or artifact identities disagree.
1276    IdentityMismatch,
1277    /// An existing role would gain authority without explicit exact approval.
1278    ApprovalRequired,
1279    /// A supplied authority approval is stale, broader, or otherwise inexact.
1280    InvalidApproval,
1281    /// Migration confirmation, ordering, backup, or downtime semantics are unsafe.
1282    UnsafeMigration,
1283    /// A credential replacement is ambiguous or would overwrite its predecessor.
1284    CredentialConflict,
1285    /// The decoded version is not supported.
1286    UnsupportedVersion,
1287    /// The submitted document is malformed.
1288    InvalidEncoding,
1289    /// The submitted document has valid meaning but noncanonical bytes.
1290    NonCanonical,
1291    /// The submitted values do not form a closed installation shape.
1292    InvalidShape,
1293}
1294
1295/// Bounded, redaction-safe plan compilation failure.
1296#[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    /// Stable error kind.
1307    #[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/// Closed campaign state-machine failure.
1364#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1365pub enum InstallationCampaignErrorKind {
1366    /// Caller supplied another campaign identity for retained state.
1367    CampaignIdentityMismatch,
1368    /// Caller reused a campaign identity for another exact plan.
1369    PlanIdentityMismatch,
1370    /// A stage was skipped, repeated, or submitted out of order.
1371    StageOutOfOrder,
1372    /// Observed stage state differs from the exact plan.
1373    EvidenceMismatch,
1374    /// The campaign already sealed its terminal receipt.
1375    AlreadyTerminal,
1376    /// A canonical campaign artifact exceeds its hard bound.
1377    LimitExceeded,
1378    /// A canonical campaign artifact is malformed.
1379    InvalidEncoding,
1380    /// A canonical campaign artifact has a valid shape but inexact bytes.
1381    NonCanonical,
1382    /// A canonical campaign artifact version is unsupported.
1383    UnsupportedVersion,
1384}
1385
1386/// Bounded, redaction-safe campaign error.
1387#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1388pub struct InstallationCampaignError {
1389    kind: InstallationCampaignErrorKind,
1390}
1391
1392impl InstallationCampaignError {
1393    /// Constructs one closed error for the application-owned campaign machine.
1394    #[doc(hidden)]
1395    #[must_use]
1396    pub const fn new(kind: InstallationCampaignErrorKind) -> Self {
1397        Self { kind }
1398    }
1399
1400    /// Maps a plan codec failure without exposing its source document.
1401    #[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    /// Stable campaign failure kind.
1419    #[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}