Skip to main content

igc_net/governance/
store.rs

1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use chrono::{DateTime, Duration, Utc};
5
6use crate::governance::lookup::GovernanceLookup;
7use crate::governance::selection::{
8    GovernanceSelectionError, select_pilot_auth_did_state, select_private_access_rotation_state,
9};
10use crate::id::{Blake3Hex, PilotId};
11use crate::util::write_json_file_atomic as write_json_file_atomic_impl;
12
13use super::record::{
14    ClaimApprovalRecord, ClaimChallengeRecord, ClaimResolutionOutcome, ClaimResolutionRecord,
15    DeletionRequestRecord, FlightGovernanceRecordError, IdentityRecoveryRecord, OwnerClaimRecord,
16    PilotAuthDidRecord, PilotAuthDidRecordError, PrivateAccessRotationRecord,
17    PrivateAccessRotationRecordError, PublicationModeRecord, RosterUpdateAction,
18    RosterUpdateRecord,
19};
20use super::state::{
21    FlightGovernanceState, FlightGovernanceStatus, PilotAuthDidState, PrivateAccessRotationState,
22};
23use super::sync::{
24    GovernanceRecord, GovernanceRecordParseError, PilotAuthDidSyncError, PilotAuthDidSyncRequest,
25    PilotAuthDidSyncResponse,
26};
27
28const GOVERNANCE_DIRNAME: &str = "governance";
29const PILOT_AUTH_DID_RECORDS_DIRNAME: &str = "pilot-auth-did-records";
30const PRIVATE_ACCESS_ROTATION_RECORDS_DIRNAME: &str = "private-access-rotation-records";
31const FLIGHT_GOVERNANCE_STATES_DIRNAME: &str = "flight-governance-states";
32const OWNER_CLAIM_RECORDS_DIRNAME: &str = "owner-claims";
33const CLAIM_APPROVAL_RECORDS_DIRNAME: &str = "claim-approvals";
34const CLAIM_CHALLENGE_RECORDS_DIRNAME: &str = "claim-challenges";
35const CLAIM_RESOLUTION_RECORDS_DIRNAME: &str = "claim-resolutions";
36const IDENTITY_RECOVERY_RECORDS_DIRNAME: &str = "identity-recoveries";
37const ROSTER_UPDATE_RECORDS_DIRNAME: &str = "roster-updates";
38const PUBLICATION_MODE_RECORDS_DIRNAME: &str = "publication-modes";
39const DELETION_REQUEST_RECORDS_DIRNAME: &str = "deletion-requests";
40const TRUSTED_RESOLVERS_FILENAME: &str = "trusted-resolvers.ndjson";
41
42#[derive(Debug, thiserror::Error)]
43pub enum GovernanceStoreError {
44    #[error("I/O: {0}")]
45    Io(#[from] std::io::Error),
46    #[error("JSON: {0}")]
47    Json(#[from] serde_json::Error),
48    #[error("time parse: {0}")]
49    TimeParse(#[from] chrono::ParseError),
50    #[error("pilot-auth-did-record: {0}")]
51    Record(#[from] PilotAuthDidRecordError),
52    #[error("private-access-rotation-record: {0}")]
53    PrivateAccessRotationRecord(#[from] PrivateAccessRotationRecordError),
54    #[error("flight governance record: {0}")]
55    FlightGovernanceRecord(#[from] FlightGovernanceRecordError),
56    #[error("selection: {0}")]
57    Selection(#[from] GovernanceSelectionError),
58    #[error("sync: {0}")]
59    Sync(#[from] PilotAuthDidSyncError),
60    #[error("governance record parse: {0}")]
61    GovernanceRecordParse(#[from] GovernanceRecordParseError),
62    #[error("governance record path has no parent directory")]
63    MissingParentDirectory,
64}
65
66#[derive(Debug, Clone)]
67pub struct GovernanceStore {
68    root: PathBuf,
69}
70
71impl GovernanceStore {
72    pub fn open(root: impl Into<PathBuf>) -> Self {
73        Self { root: root.into() }
74    }
75
76    pub fn for_data_dir(data_dir: impl AsRef<Path>) -> Self {
77        Self::open(data_dir.as_ref().join(GOVERNANCE_DIRNAME))
78    }
79
80    pub fn root_dir(&self) -> &Path {
81        &self.root
82    }
83
84    pub fn init(&self) -> Result<(), GovernanceStoreError> {
85        std::fs::create_dir_all(self.pilot_auth_did_records_root())?;
86        std::fs::create_dir_all(self.private_access_rotation_records_root())?;
87        std::fs::create_dir_all(self.flight_governance_states_root())?;
88        std::fs::create_dir_all(self.owner_claim_records_root())?;
89        std::fs::create_dir_all(self.claim_approval_records_root())?;
90        std::fs::create_dir_all(self.claim_challenge_records_root())?;
91        std::fs::create_dir_all(self.claim_resolution_records_root())?;
92        std::fs::create_dir_all(self.identity_recovery_records_root())?;
93        std::fs::create_dir_all(self.roster_update_records_root())?;
94        std::fs::create_dir_all(self.publication_mode_records_root())?;
95        std::fs::create_dir_all(self.deletion_request_records_root())?;
96        Ok(())
97    }
98
99    pub fn persist_pilot_auth_did_record(
100        &self,
101        record: &PilotAuthDidRecord,
102    ) -> Result<(), GovernanceStoreError> {
103        self.init()?;
104        record.validate()?;
105        let path = self.pilot_auth_did_record_path(&record.pilot_id, &record.record_id);
106        if path.exists() {
107            return Ok(());
108        }
109        write_json_file_atomic(&path, record)
110    }
111
112    pub fn load_pilot_auth_did_records(
113        &self,
114        pilot_id: &PilotId,
115    ) -> Result<Vec<PilotAuthDidRecord>, GovernanceStoreError> {
116        self.init()?;
117        let dir = self.pilot_auth_did_pilot_dir(pilot_id);
118        if !dir.exists() {
119            return Ok(Vec::new());
120        }
121
122        let mut paths = std::fs::read_dir(&dir)?
123            .filter_map(Result::ok)
124            .filter_map(|entry| {
125                let file_type = entry.file_type().ok()?;
126                if !file_type.is_file() {
127                    return None;
128                }
129                let path = entry.path();
130                (path.extension().and_then(|ext| ext.to_str()) == Some("json")).then_some(path)
131            })
132            .collect::<Vec<_>>();
133        paths.sort();
134
135        let mut records = Vec::with_capacity(paths.len());
136        for path in paths {
137            let record: PilotAuthDidRecord = serde_json::from_slice(&std::fs::read(&path)?)?;
138            record.validate()?;
139            records.push(record);
140        }
141        records.sort_by(|left, right| left.record_id.cmp(&right.record_id));
142        Ok(records)
143    }
144
145    pub fn resolve_pilot_auth_did_state(
146        &self,
147        pilot_id: &PilotId,
148    ) -> Result<PilotAuthDidState, GovernanceStoreError> {
149        let records = self.load_pilot_auth_did_records(pilot_id)?;
150        Ok(select_pilot_auth_did_state(pilot_id, &records)?)
151    }
152
153    pub fn persist_private_access_rotation_record(
154        &self,
155        record: &PrivateAccessRotationRecord,
156    ) -> Result<(), GovernanceStoreError> {
157        self.init()?;
158        record.validate()?;
159        let path = self.private_access_rotation_record_path(&record.pilot_id, &record.record_id);
160        if path.exists() {
161            return Ok(());
162        }
163        write_json_file_atomic(&path, record)
164    }
165
166    pub fn load_private_access_rotation_records(
167        &self,
168        pilot_id: &PilotId,
169    ) -> Result<Vec<PrivateAccessRotationRecord>, GovernanceStoreError> {
170        self.init()?;
171        let dir = self.private_access_rotation_pilot_dir(pilot_id);
172        if !dir.exists() {
173            return Ok(Vec::new());
174        }
175
176        let mut paths = std::fs::read_dir(&dir)?
177            .filter_map(Result::ok)
178            .filter_map(|entry| {
179                let file_type = entry.file_type().ok()?;
180                if !file_type.is_file() {
181                    return None;
182                }
183                let path = entry.path();
184                (path.extension().and_then(|ext| ext.to_str()) == Some("json")).then_some(path)
185            })
186            .collect::<Vec<_>>();
187        paths.sort();
188
189        let mut records = Vec::with_capacity(paths.len());
190        for path in paths {
191            let record: PrivateAccessRotationRecord =
192                serde_json::from_slice(&std::fs::read(&path)?)?;
193            record.validate()?;
194            records.push(record);
195        }
196        records.sort_by(|left, right| left.record_id.cmp(&right.record_id));
197        Ok(records)
198    }
199
200    pub fn resolve_private_access_rotation_state(
201        &self,
202        pilot_id: &PilotId,
203    ) -> Result<PrivateAccessRotationState, GovernanceStoreError> {
204        let records = self.load_private_access_rotation_records(pilot_id)?;
205        Ok(select_private_access_rotation_state(pilot_id, &records)?)
206    }
207
208    pub fn persist_flight_governance_state(
209        &self,
210        state: &FlightGovernanceState,
211    ) -> Result<(), GovernanceStoreError> {
212        self.init()?;
213        let path = self.flight_governance_state_path(&state.raw_igc_hash);
214        write_json_file_atomic(&path, state)
215    }
216
217    pub fn load_flight_governance_state(
218        &self,
219        raw_igc_hash: &Blake3Hex,
220    ) -> Result<Option<FlightGovernanceState>, GovernanceStoreError> {
221        self.init()?;
222        let path = self.flight_governance_state_path(raw_igc_hash);
223        if !path.exists() {
224            return Ok(None);
225        }
226        let state: FlightGovernanceState = serde_json::from_slice(&std::fs::read(&path)?)?;
227        Ok(Some(state))
228    }
229
230    pub fn persist_owner_claim_record(
231        &self,
232        record: &OwnerClaimRecord,
233    ) -> Result<(), GovernanceStoreError> {
234        self.init()?;
235        record.validate()?;
236        let path = self.owner_claim_record_path(&record.raw_igc_hash, &record.record_id);
237        if path.exists() {
238            return Ok(());
239        }
240        write_json_file_atomic(&path, record)
241    }
242
243    pub fn load_owner_claim_records(
244        &self,
245        raw_igc_hash: &Blake3Hex,
246    ) -> Result<Vec<OwnerClaimRecord>, GovernanceStoreError> {
247        self.load_flight_record_dir(self.owner_claim_flight_dir(raw_igc_hash))
248    }
249
250    pub fn persist_claim_approval_record(
251        &self,
252        record: &ClaimApprovalRecord,
253    ) -> Result<(), GovernanceStoreError> {
254        self.init()?;
255        record.validate()?;
256        let path = self.claim_approval_record_path(&record.raw_igc_hash, &record.record_id);
257        if path.exists() {
258            return Ok(());
259        }
260        write_json_file_atomic(&path, record)
261    }
262
263    pub fn load_claim_approval_records(
264        &self,
265        raw_igc_hash: &Blake3Hex,
266    ) -> Result<Vec<ClaimApprovalRecord>, GovernanceStoreError> {
267        self.load_flight_record_dir(self.claim_approval_flight_dir(raw_igc_hash))
268    }
269
270    pub fn persist_claim_challenge_record(
271        &self,
272        record: &ClaimChallengeRecord,
273    ) -> Result<(), GovernanceStoreError> {
274        self.init()?;
275        record.validate()?;
276        let path = self.claim_challenge_record_path(&record.raw_igc_hash, &record.record_id);
277        if path.exists() {
278            return Ok(());
279        }
280        write_json_file_atomic(&path, record)
281    }
282
283    pub fn load_claim_challenge_records(
284        &self,
285        raw_igc_hash: &Blake3Hex,
286    ) -> Result<Vec<ClaimChallengeRecord>, GovernanceStoreError> {
287        self.load_flight_record_dir(self.claim_challenge_flight_dir(raw_igc_hash))
288    }
289
290    pub fn persist_claim_resolution_record(
291        &self,
292        record: &ClaimResolutionRecord,
293    ) -> Result<(), GovernanceStoreError> {
294        self.init()?;
295        record.validate()?;
296        let path = self.claim_resolution_record_path(&record.raw_igc_hash, &record.record_id);
297        if path.exists() {
298            return Ok(());
299        }
300        write_json_file_atomic(&path, record)
301    }
302
303    pub fn load_claim_resolution_records(
304        &self,
305        raw_igc_hash: &Blake3Hex,
306    ) -> Result<Vec<ClaimResolutionRecord>, GovernanceStoreError> {
307        self.load_flight_record_dir(self.claim_resolution_flight_dir(raw_igc_hash))
308    }
309
310    pub fn persist_identity_recovery_record(
311        &self,
312        record: &IdentityRecoveryRecord,
313    ) -> Result<(), GovernanceStoreError> {
314        self.init()?;
315        record.validate()?;
316        let path = self.identity_recovery_record_path(&record.record_id);
317        if path.exists() {
318            return Ok(());
319        }
320        write_json_file_atomic(&path, record)
321    }
322
323    pub fn load_identity_recovery_records(
324        &self,
325    ) -> Result<Vec<IdentityRecoveryRecord>, GovernanceStoreError> {
326        self.init()?;
327        let dir = self.identity_recovery_records_root();
328        if !dir.exists() {
329            return Ok(Vec::new());
330        }
331
332        let mut paths = std::fs::read_dir(&dir)?
333            .filter_map(Result::ok)
334            .filter_map(|entry| {
335                let file_type = entry.file_type().ok()?;
336                if !file_type.is_file() {
337                    return None;
338                }
339                let path = entry.path();
340                (path.extension().and_then(|ext| ext.to_str()) == Some("json")).then_some(path)
341            })
342            .collect::<Vec<_>>();
343        paths.sort();
344
345        let mut records = Vec::with_capacity(paths.len());
346        for path in paths {
347            let record: IdentityRecoveryRecord = serde_json::from_slice(&std::fs::read(&path)?)?;
348            record.validate()?;
349            records.push(record);
350        }
351        records.sort_by(|left, right| left.record_id.cmp(&right.record_id));
352        Ok(records)
353    }
354
355    pub fn trust_resolver(&self, resolver_id: &str) -> Result<(), GovernanceStoreError> {
356        self.init()?;
357        validate_trusted_resolver_id(resolver_id)?;
358        let mut trusted = self.load_trusted_resolvers()?;
359        if !trusted.insert(resolver_id.to_string()) {
360            return Ok(());
361        }
362        let path = self.trusted_resolvers_path();
363        let mut rows = trusted.into_iter().collect::<Vec<_>>();
364        rows.sort();
365        let contents = rows
366            .into_iter()
367            .map(|resolver_id| serde_json::json!({ "resolver_id": resolver_id }).to_string())
368            .collect::<Vec<_>>()
369            .join("\n");
370        std::fs::write(path, format!("{contents}\n"))?;
371        Ok(())
372    }
373
374    pub fn load_trusted_resolvers(&self) -> Result<HashSet<String>, GovernanceStoreError> {
375        self.init()?;
376        let path = self.trusted_resolvers_path();
377        if !path.exists() {
378            return Ok(HashSet::new());
379        }
380        let mut trusted = HashSet::new();
381        for line in std::fs::read_to_string(path)?.lines() {
382            if line.trim().is_empty() {
383                continue;
384            }
385            let row: TrustedResolverRow = serde_json::from_str(line)?;
386            validate_trusted_resolver_id(&row.resolver_id)?;
387            trusted.insert(row.resolver_id);
388        }
389        Ok(trusted)
390    }
391
392    pub fn persist_roster_update_record(
393        &self,
394        record: &RosterUpdateRecord,
395    ) -> Result<(), GovernanceStoreError> {
396        self.init()?;
397        record.validate()?;
398        let path = self.roster_update_record_path(&record.record_id);
399        if path.exists() {
400            return Ok(());
401        }
402        write_json_file_atomic(&path, record)
403    }
404
405    pub fn load_roster_update_records(
406        &self,
407    ) -> Result<Vec<RosterUpdateRecord>, GovernanceStoreError> {
408        self.init()?;
409        let dir = self.roster_update_records_root();
410        if !dir.exists() {
411            return Ok(Vec::new());
412        }
413
414        let mut paths = std::fs::read_dir(&dir)?
415            .filter_map(Result::ok)
416            .filter_map(|entry| {
417                let file_type = entry.file_type().ok()?;
418                if !file_type.is_file() {
419                    return None;
420                }
421                let path = entry.path();
422                (path.extension().and_then(|ext| ext.to_str()) == Some("json")).then_some(path)
423            })
424            .collect::<Vec<_>>();
425        paths.sort();
426
427        let mut records = Vec::with_capacity(paths.len());
428        for path in paths {
429            let record: RosterUpdateRecord = serde_json::from_slice(&std::fs::read(&path)?)?;
430            record.validate()?;
431            records.push(record);
432        }
433        records.sort_by(|left, right| left.record_id.cmp(&right.record_id));
434        Ok(records)
435    }
436
437    pub fn persist_publication_mode_record(
438        &self,
439        record: &PublicationModeRecord,
440    ) -> Result<(), GovernanceStoreError> {
441        self.init()?;
442        record.validate()?;
443        let path = self.publication_mode_record_path(&record.raw_igc_hash, &record.record_id);
444        if path.exists() {
445            return Ok(());
446        }
447        write_json_file_atomic(&path, record)
448    }
449
450    pub fn load_publication_mode_records(
451        &self,
452        raw_igc_hash: &Blake3Hex,
453    ) -> Result<Vec<PublicationModeRecord>, GovernanceStoreError> {
454        self.load_flight_record_dir(self.publication_mode_flight_dir(raw_igc_hash))
455    }
456
457    pub fn resolve_publication_mode_record(
458        &self,
459        raw_igc_hash: &Blake3Hex,
460    ) -> Result<Option<PublicationModeRecord>, GovernanceStoreError> {
461        let records = self.load_publication_mode_records(raw_igc_hash)?;
462        Ok(select_publication_mode_tip(&records))
463    }
464
465    pub fn effective_trusted_resolvers(&self) -> Result<HashSet<String>, GovernanceStoreError> {
466        let mut trusted = self.load_trusted_resolvers()?;
467        for record in self.load_roster_update_records()? {
468            if !trusted.contains(&record.signer_id) {
469                continue;
470            }
471            match record.action {
472                RosterUpdateAction::Add => {
473                    trusted.insert(record.resolver_id);
474                }
475                RosterUpdateAction::Remove => {
476                    trusted.remove(&record.resolver_id);
477                }
478            }
479        }
480        Ok(trusted)
481    }
482
483    pub fn persist_deletion_request_record(
484        &self,
485        record: &DeletionRequestRecord,
486    ) -> Result<(), GovernanceStoreError> {
487        self.init()?;
488        record.validate()?;
489        let path = self.deletion_request_record_path(&record.raw_igc_hash, &record.record_id);
490        if path.exists() {
491            return Ok(());
492        }
493        write_json_file_atomic(&path, record)
494    }
495
496    pub fn load_deletion_request_records(
497        &self,
498        raw_igc_hash: &Blake3Hex,
499    ) -> Result<Vec<DeletionRequestRecord>, GovernanceStoreError> {
500        self.load_flight_record_dir(self.deletion_request_flight_dir(raw_igc_hash))
501    }
502
503    pub fn resolve_flight_governance_state(
504        &self,
505        raw_igc_hash: &Blake3Hex,
506    ) -> Result<Option<FlightGovernanceState>, GovernanceStoreError> {
507        self.resolve_flight_governance_state_at(raw_igc_hash, &crate::util::canonical_utc_now())
508    }
509
510    pub fn resolve_flight_governance_state_at(
511        &self,
512        raw_igc_hash: &Blake3Hex,
513        now: &str,
514    ) -> Result<Option<FlightGovernanceState>, GovernanceStoreError> {
515        let now = DateTime::parse_from_rfc3339(now)?.with_timezone(&Utc);
516        let applied = self.load_flight_governance_state(raw_igc_hash)?;
517        let trusted_resolvers = self.effective_trusted_resolvers()?;
518        let claims = self.load_owner_claim_records(raw_igc_hash)?;
519        let approvals = self
520            .load_claim_approval_records(raw_igc_hash)?
521            .into_iter()
522            .filter(|record| trusted_resolvers.contains(&record.resolver_id))
523            .collect::<Vec<_>>();
524        let challenges = self
525            .load_claim_challenge_records(raw_igc_hash)?
526            .into_iter()
527            .filter(|record| trusted_resolvers.contains(&record.challenger_resolver_id))
528            .collect::<Vec<_>>();
529        let resolutions = self
530            .load_claim_resolution_records(raw_igc_hash)?
531            .into_iter()
532            .filter(|record| trusted_resolvers.contains(&record.resolver_id))
533            .collect::<Vec<_>>();
534        let identity_recoveries = self
535            .load_identity_recovery_records()?
536            .into_iter()
537            .filter(|record| trusted_resolvers.contains(&record.resolver_id))
538            .collect::<Vec<_>>();
539        let resolved = resolve_raw_flight_governance(
540            raw_igc_hash,
541            &claims,
542            &approvals,
543            &challenges,
544            &resolutions,
545            &identity_recoveries,
546            now,
547        );
548        let deletion_requests = self.load_deletion_request_records(raw_igc_hash)?;
549        if let Some(state) =
550            apply_deletion_requests(resolved.clone().or(applied.clone()), &deletion_requests)
551        {
552            return Ok(Some(state));
553        }
554
555        if resolved.is_some() {
556            return Ok(resolved);
557        }
558
559        Ok(applied)
560    }
561
562    pub fn apply_governance_record(
563        &self,
564        record: &GovernanceRecord,
565    ) -> Result<bool, GovernanceStoreError> {
566        self.init()?;
567        match record {
568            GovernanceRecord::OwnerClaim(record) => {
569                let path = self.owner_claim_record_path(&record.raw_igc_hash, &record.record_id);
570                let existed = path.exists();
571                self.persist_owner_claim_record(record)?;
572                Ok(!existed)
573            }
574            GovernanceRecord::ClaimApproval(record) => {
575                let path = self.claim_approval_record_path(&record.raw_igc_hash, &record.record_id);
576                let existed = path.exists();
577                self.persist_claim_approval_record(record)?;
578                Ok(!existed)
579            }
580            GovernanceRecord::ClaimChallenge(record) => {
581                let path =
582                    self.claim_challenge_record_path(&record.raw_igc_hash, &record.record_id);
583                let existed = path.exists();
584                self.persist_claim_challenge_record(record)?;
585                Ok(!existed)
586            }
587            GovernanceRecord::ClaimResolution(record) => {
588                let path =
589                    self.claim_resolution_record_path(&record.raw_igc_hash, &record.record_id);
590                let existed = path.exists();
591                self.persist_claim_resolution_record(record)?;
592                Ok(!existed)
593            }
594            GovernanceRecord::IdentityRecovery(record) => {
595                let path = self.identity_recovery_record_path(&record.record_id);
596                let existed = path.exists();
597                self.persist_identity_recovery_record(record)?;
598                Ok(!existed)
599            }
600            GovernanceRecord::DeletionRequest(record) => {
601                let path =
602                    self.deletion_request_record_path(&record.raw_igc_hash, &record.record_id);
603                let existed = path.exists();
604                self.persist_deletion_request_record(record)?;
605                Ok(!existed)
606            }
607            GovernanceRecord::PrivateAccessRotation(record) => {
608                let path =
609                    self.private_access_rotation_record_path(&record.pilot_id, &record.record_id);
610                let existed = path.exists();
611                self.persist_private_access_rotation_record(record)?;
612                Ok(!existed)
613            }
614            GovernanceRecord::PilotAuthDid(record) => {
615                let path = self.pilot_auth_did_record_path(&record.pilot_id, &record.record_id);
616                let existed = path.exists();
617                self.persist_pilot_auth_did_record(record)?;
618                Ok(!existed)
619            }
620            GovernanceRecord::RosterUpdate(record) => {
621                let path = self.roster_update_record_path(&record.record_id);
622                let existed = path.exists();
623                self.persist_roster_update_record(record)?;
624                Ok(!existed)
625            }
626            GovernanceRecord::PublicationMode(record) => {
627                let path =
628                    self.publication_mode_record_path(&record.raw_igc_hash, &record.record_id);
629                let existed = path.exists();
630                self.persist_publication_mode_record(record)?;
631                Ok(!existed)
632            }
633        }
634    }
635
636    pub fn apply_governance_record_json(&self, bytes: &[u8]) -> Result<bool, GovernanceStoreError> {
637        let record = GovernanceRecord::from_slice(bytes)?;
638        self.apply_governance_record(&record)
639    }
640
641    pub fn prepare_pilot_auth_did_sync(
642        &self,
643        request: &PilotAuthDidSyncRequest,
644    ) -> Result<PilotAuthDidSyncResponse, GovernanceStoreError> {
645        let records = self
646            .load_pilot_auth_did_records(&request.pilot_id)?
647            .into_iter()
648            .filter(|record| !request.knows(&record.record_id))
649            .collect::<Vec<_>>();
650        Ok(PilotAuthDidSyncResponse::new(
651            request.pilot_id.clone(),
652            records,
653        ))
654    }
655
656    pub fn build_pilot_auth_did_sync_request(
657        &self,
658        pilot_id: &PilotId,
659    ) -> Result<PilotAuthDidSyncRequest, GovernanceStoreError> {
660        let known_record_ids = self
661            .load_pilot_auth_did_records(pilot_id)?
662            .into_iter()
663            .map(|record| record.record_id)
664            .collect();
665        Ok(PilotAuthDidSyncRequest::new(
666            pilot_id.clone(),
667            known_record_ids,
668        ))
669    }
670
671    pub fn apply_pilot_auth_did_sync(
672        &self,
673        response: &PilotAuthDidSyncResponse,
674    ) -> Result<usize, GovernanceStoreError> {
675        response.validate()?;
676        let mut applied = 0usize;
677        for record in &response.records {
678            let path = self.pilot_auth_did_record_path(&record.pilot_id, &record.record_id);
679            let existed = path.exists();
680            self.persist_pilot_auth_did_record(record)?;
681            if !existed {
682                applied += 1;
683            }
684        }
685        Ok(applied)
686    }
687
688    fn pilot_auth_did_records_root(&self) -> PathBuf {
689        self.root.join(PILOT_AUTH_DID_RECORDS_DIRNAME)
690    }
691
692    fn private_access_rotation_records_root(&self) -> PathBuf {
693        self.root.join(PRIVATE_ACCESS_ROTATION_RECORDS_DIRNAME)
694    }
695
696    fn flight_governance_states_root(&self) -> PathBuf {
697        self.root.join(FLIGHT_GOVERNANCE_STATES_DIRNAME)
698    }
699
700    fn owner_claim_records_root(&self) -> PathBuf {
701        self.root.join(OWNER_CLAIM_RECORDS_DIRNAME)
702    }
703
704    fn claim_approval_records_root(&self) -> PathBuf {
705        self.root.join(CLAIM_APPROVAL_RECORDS_DIRNAME)
706    }
707
708    fn claim_challenge_records_root(&self) -> PathBuf {
709        self.root.join(CLAIM_CHALLENGE_RECORDS_DIRNAME)
710    }
711
712    fn claim_resolution_records_root(&self) -> PathBuf {
713        self.root.join(CLAIM_RESOLUTION_RECORDS_DIRNAME)
714    }
715
716    fn identity_recovery_records_root(&self) -> PathBuf {
717        self.root.join(IDENTITY_RECOVERY_RECORDS_DIRNAME)
718    }
719
720    fn roster_update_records_root(&self) -> PathBuf {
721        self.root.join(ROSTER_UPDATE_RECORDS_DIRNAME)
722    }
723
724    fn publication_mode_records_root(&self) -> PathBuf {
725        self.root.join(PUBLICATION_MODE_RECORDS_DIRNAME)
726    }
727
728    fn deletion_request_records_root(&self) -> PathBuf {
729        self.root.join(DELETION_REQUEST_RECORDS_DIRNAME)
730    }
731
732    fn trusted_resolvers_path(&self) -> PathBuf {
733        self.root.join(TRUSTED_RESOLVERS_FILENAME)
734    }
735
736    fn pilot_auth_did_pilot_dir(&self, pilot_id: &PilotId) -> PathBuf {
737        self.pilot_auth_did_records_root()
738            .join(pilot_id.public_key_hex())
739    }
740
741    fn pilot_auth_did_record_path(
742        &self,
743        pilot_id: &PilotId,
744        record_id: &crate::id::Blake3Hex,
745    ) -> PathBuf {
746        self.pilot_auth_did_pilot_dir(pilot_id)
747            .join(format!("{record_id}.json"))
748    }
749
750    fn private_access_rotation_pilot_dir(&self, pilot_id: &PilotId) -> PathBuf {
751        self.private_access_rotation_records_root()
752            .join(pilot_id.public_key_hex())
753    }
754
755    fn private_access_rotation_record_path(
756        &self,
757        pilot_id: &PilotId,
758        record_id: &crate::id::Blake3Hex,
759    ) -> PathBuf {
760        self.private_access_rotation_pilot_dir(pilot_id)
761            .join(format!("{record_id}.json"))
762    }
763
764    fn flight_governance_state_path(&self, raw_igc_hash: &Blake3Hex) -> PathBuf {
765        self.flight_governance_states_root()
766            .join(&raw_igc_hash.as_str()[..2])
767            .join(format!("{raw_igc_hash}.json"))
768    }
769
770    fn owner_claim_flight_dir(&self, raw_igc_hash: &Blake3Hex) -> PathBuf {
771        self.owner_claim_records_root()
772            .join(&raw_igc_hash.as_str()[..2])
773            .join(raw_igc_hash.as_str())
774    }
775
776    fn owner_claim_record_path(&self, raw_igc_hash: &Blake3Hex, record_id: &Blake3Hex) -> PathBuf {
777        self.owner_claim_flight_dir(raw_igc_hash)
778            .join(format!("{record_id}.json"))
779    }
780
781    fn claim_approval_flight_dir(&self, raw_igc_hash: &Blake3Hex) -> PathBuf {
782        self.claim_approval_records_root()
783            .join(&raw_igc_hash.as_str()[..2])
784            .join(raw_igc_hash.as_str())
785    }
786
787    fn claim_approval_record_path(
788        &self,
789        raw_igc_hash: &Blake3Hex,
790        record_id: &Blake3Hex,
791    ) -> PathBuf {
792        self.claim_approval_flight_dir(raw_igc_hash)
793            .join(format!("{record_id}.json"))
794    }
795
796    fn claim_challenge_flight_dir(&self, raw_igc_hash: &Blake3Hex) -> PathBuf {
797        self.claim_challenge_records_root()
798            .join(&raw_igc_hash.as_str()[..2])
799            .join(raw_igc_hash.as_str())
800    }
801
802    fn claim_challenge_record_path(
803        &self,
804        raw_igc_hash: &Blake3Hex,
805        record_id: &Blake3Hex,
806    ) -> PathBuf {
807        self.claim_challenge_flight_dir(raw_igc_hash)
808            .join(format!("{record_id}.json"))
809    }
810
811    fn claim_resolution_flight_dir(&self, raw_igc_hash: &Blake3Hex) -> PathBuf {
812        self.claim_resolution_records_root()
813            .join(&raw_igc_hash.as_str()[..2])
814            .join(raw_igc_hash.as_str())
815    }
816
817    fn claim_resolution_record_path(
818        &self,
819        raw_igc_hash: &Blake3Hex,
820        record_id: &Blake3Hex,
821    ) -> PathBuf {
822        self.claim_resolution_flight_dir(raw_igc_hash)
823            .join(format!("{record_id}.json"))
824    }
825
826    fn identity_recovery_record_path(&self, record_id: &Blake3Hex) -> PathBuf {
827        self.identity_recovery_records_root()
828            .join(format!("{record_id}.json"))
829    }
830
831    fn roster_update_record_path(&self, record_id: &Blake3Hex) -> PathBuf {
832        self.roster_update_records_root()
833            .join(format!("{record_id}.json"))
834    }
835
836    fn publication_mode_flight_dir(&self, raw_igc_hash: &Blake3Hex) -> PathBuf {
837        self.publication_mode_records_root()
838            .join(&raw_igc_hash.as_str()[..2])
839            .join(raw_igc_hash.as_str())
840    }
841
842    fn publication_mode_record_path(
843        &self,
844        raw_igc_hash: &Blake3Hex,
845        record_id: &Blake3Hex,
846    ) -> PathBuf {
847        self.publication_mode_flight_dir(raw_igc_hash)
848            .join(format!("{record_id}.json"))
849    }
850
851    fn deletion_request_flight_dir(&self, raw_igc_hash: &Blake3Hex) -> PathBuf {
852        self.deletion_request_records_root()
853            .join(&raw_igc_hash.as_str()[..2])
854            .join(raw_igc_hash.as_str())
855    }
856
857    fn deletion_request_record_path(
858        &self,
859        raw_igc_hash: &Blake3Hex,
860        record_id: &Blake3Hex,
861    ) -> PathBuf {
862        self.deletion_request_flight_dir(raw_igc_hash)
863            .join(format!("{record_id}.json"))
864    }
865
866    fn load_flight_record_dir<T>(&self, dir: PathBuf) -> Result<Vec<T>, GovernanceStoreError>
867    where
868        T: serde::de::DeserializeOwned + ValidatedFlightGovernanceRecord,
869    {
870        self.init()?;
871        if !dir.exists() {
872            return Ok(Vec::new());
873        }
874
875        let mut paths = std::fs::read_dir(&dir)?
876            .filter_map(Result::ok)
877            .filter_map(|entry| {
878                let file_type = entry.file_type().ok()?;
879                if !file_type.is_file() {
880                    return None;
881                }
882                let path = entry.path();
883                (path.extension().and_then(|ext| ext.to_str()) == Some("json")).then_some(path)
884            })
885            .collect::<Vec<_>>();
886        paths.sort();
887
888        let mut records = Vec::with_capacity(paths.len());
889        for path in paths {
890            let record: T = serde_json::from_slice(&std::fs::read(&path)?)?;
891            record.validate_flight_governance_record()?;
892            records.push(record);
893        }
894        records.sort_by(|left, right| left.record_id().cmp(right.record_id()));
895        Ok(records)
896    }
897}
898
899trait ValidatedFlightGovernanceRecord {
900    fn validate_flight_governance_record(&self) -> Result<(), FlightGovernanceRecordError>;
901    fn record_id(&self) -> &Blake3Hex;
902}
903
904impl ValidatedFlightGovernanceRecord for OwnerClaimRecord {
905    fn validate_flight_governance_record(&self) -> Result<(), FlightGovernanceRecordError> {
906        self.validate()
907    }
908
909    fn record_id(&self) -> &Blake3Hex {
910        &self.record_id
911    }
912}
913
914impl ValidatedFlightGovernanceRecord for ClaimApprovalRecord {
915    fn validate_flight_governance_record(&self) -> Result<(), FlightGovernanceRecordError> {
916        self.validate()
917    }
918
919    fn record_id(&self) -> &Blake3Hex {
920        &self.record_id
921    }
922}
923
924impl ValidatedFlightGovernanceRecord for ClaimChallengeRecord {
925    fn validate_flight_governance_record(&self) -> Result<(), FlightGovernanceRecordError> {
926        self.validate()
927    }
928
929    fn record_id(&self) -> &Blake3Hex {
930        &self.record_id
931    }
932}
933
934impl ValidatedFlightGovernanceRecord for ClaimResolutionRecord {
935    fn validate_flight_governance_record(&self) -> Result<(), FlightGovernanceRecordError> {
936        self.validate()
937    }
938
939    fn record_id(&self) -> &Blake3Hex {
940        &self.record_id
941    }
942}
943
944impl ValidatedFlightGovernanceRecord for DeletionRequestRecord {
945    fn validate_flight_governance_record(&self) -> Result<(), FlightGovernanceRecordError> {
946        self.validate()
947    }
948
949    fn record_id(&self) -> &Blake3Hex {
950        &self.record_id
951    }
952}
953
954impl ValidatedFlightGovernanceRecord for PublicationModeRecord {
955    fn validate_flight_governance_record(&self) -> Result<(), FlightGovernanceRecordError> {
956        self.validate()
957    }
958
959    fn record_id(&self) -> &Blake3Hex {
960        &self.record_id
961    }
962}
963
964#[derive(serde::Deserialize)]
965struct TrustedResolverRow {
966    resolver_id: String,
967}
968
969fn apply_deletion_requests(
970    applied: Option<FlightGovernanceState>,
971    deletion_requests: &[DeletionRequestRecord],
972) -> Option<FlightGovernanceState> {
973    let mut state = applied?;
974    if state.status == FlightGovernanceStatus::Deleted {
975        return Some(state);
976    }
977
978    let owner = state.owner_pilot_id.as_ref()?;
979    if deletion_requests
980        .iter()
981        .any(|request| &request.pilot_id == owner)
982    {
983        state.status = FlightGovernanceStatus::Deleted;
984        state.baseline_ready = true;
985        if let Some(latest_deletion) = deletion_requests
986            .iter()
987            .filter(|request| &request.pilot_id == owner)
988            .max_by(|left, right| left.record_id.cmp(&right.record_id))
989        {
990            state.recorded_at = latest_deletion.created_at.clone();
991        }
992    }
993
994    Some(state)
995}
996
997fn resolve_raw_flight_governance(
998    raw_igc_hash: &Blake3Hex,
999    claims: &[OwnerClaimRecord],
1000    approvals: &[ClaimApprovalRecord],
1001    challenges: &[ClaimChallengeRecord],
1002    resolutions: &[ClaimResolutionRecord],
1003    identity_recoveries: &[IdentityRecoveryRecord],
1004    now: DateTime<Utc>,
1005) -> Option<FlightGovernanceState> {
1006    let claim_by_id = claims
1007        .iter()
1008        .map(|claim| (claim.record_id.clone(), claim))
1009        .collect::<HashMap<_, _>>();
1010
1011    if let Some(mut state) = resolve_active_resolution(raw_igc_hash, &claim_by_id, resolutions) {
1012        apply_identity_recoveries(&mut state, identity_recoveries);
1013        return Some(state);
1014    }
1015
1016    let approved_claims = approvals
1017        .iter()
1018        .filter_map(|approval| claim_by_id.get(&approval.claim_record_id).copied())
1019        .collect::<Vec<_>>();
1020    let mut approved_owners = approved_claims
1021        .iter()
1022        .map(|claim| claim.pilot_id.clone())
1023        .collect::<Vec<_>>();
1024    approved_owners.sort();
1025    approved_owners.dedup();
1026
1027    if approved_owners.len() > 1 {
1028        let mut state = FlightGovernanceState {
1029            raw_igc_hash: raw_igc_hash.clone(),
1030            owner_pilot_id: None,
1031            status: FlightGovernanceStatus::Contested,
1032            baseline_ready: true,
1033            recorded_at: approvals
1034                .iter()
1035                .map(|approval| approval.created_at.as_str())
1036                .max()
1037                .unwrap_or_default()
1038                .to_string(),
1039        };
1040        apply_identity_recoveries(&mut state, identity_recoveries);
1041        return Some(state);
1042    }
1043
1044    if let Some(claim) = approved_claims.first() {
1045        let active_challenges = challenges
1046            .iter()
1047            .filter(|challenge| challenge.claim_record_id == claim.record_id)
1048            .filter(|challenge| !challenge_expired(challenge, now))
1049            .collect::<Vec<_>>();
1050        if !active_challenges.is_empty() {
1051            let mut state = FlightGovernanceState {
1052                raw_igc_hash: raw_igc_hash.clone(),
1053                owner_pilot_id: Some(claim.pilot_id.clone()),
1054                status: FlightGovernanceStatus::Contested,
1055                baseline_ready: true,
1056                recorded_at: active_challenges
1057                    .iter()
1058                    .map(|challenge| challenge.created_at.as_str())
1059                    .max()
1060                    .unwrap_or_default()
1061                    .to_string(),
1062            };
1063            apply_identity_recoveries(&mut state, identity_recoveries);
1064            return Some(state);
1065        }
1066    }
1067
1068    if let Some(claim) = approved_claims.first() {
1069        let mut state = FlightGovernanceState::approved_owner(
1070            raw_igc_hash.clone(),
1071            claim.pilot_id.clone(),
1072            approvals
1073                .iter()
1074                .filter(|approval| approval.claim_record_id == claim.record_id)
1075                .map(|approval| approval.created_at.as_str())
1076                .max()
1077                .unwrap_or_default(),
1078        );
1079        apply_identity_recoveries(&mut state, identity_recoveries);
1080        return Some(state);
1081    }
1082
1083    let mut state = resolve_pending_owner_claim(raw_igc_hash, claims)?;
1084    apply_identity_recoveries(&mut state, identity_recoveries);
1085    Some(state)
1086}
1087
1088fn challenge_expired(challenge: &ClaimChallengeRecord, now: DateTime<Utc>) -> bool {
1089    let Ok(created_at) = DateTime::parse_from_rfc3339(&challenge.created_at) else {
1090        return false;
1091    };
1092    now >= created_at.with_timezone(&Utc) + Duration::days(15)
1093}
1094
1095fn apply_identity_recoveries(
1096    state: &mut FlightGovernanceState,
1097    identity_recoveries: &[IdentityRecoveryRecord],
1098) {
1099    if state.status != FlightGovernanceStatus::Approved {
1100        return;
1101    }
1102    let Some(owner) = state.owner_pilot_id.as_mut() else {
1103        return;
1104    };
1105    let Some(recovery) = identity_recoveries
1106        .iter()
1107        .filter(|record| &record.old_pilot_id == owner)
1108        .min_by(|left, right| left.record_id.cmp(&right.record_id))
1109    else {
1110        return;
1111    };
1112    *owner = recovery.new_pilot_id.clone();
1113    state.recorded_at = recovery.created_at.clone();
1114}
1115
1116fn resolve_active_resolution(
1117    raw_igc_hash: &Blake3Hex,
1118    claim_by_id: &HashMap<Blake3Hex, &OwnerClaimRecord>,
1119    resolutions: &[ClaimResolutionRecord],
1120) -> Option<FlightGovernanceState> {
1121    let superseded = resolutions
1122        .iter()
1123        .flat_map(|resolution| resolution.supersedes.iter().cloned())
1124        .collect::<HashSet<_>>();
1125    let active = resolutions
1126        .iter()
1127        .filter(|resolution| !superseded.contains(&resolution.record_id))
1128        .min_by(|left, right| left.record_id.cmp(&right.record_id))?;
1129    let claim = claim_by_id.get(&active.claim_record_id)?;
1130    let status = match active.resolution {
1131        ClaimResolutionOutcome::Approved => FlightGovernanceStatus::Approved,
1132        ClaimResolutionOutcome::Rejected => FlightGovernanceStatus::Rejected,
1133        ClaimResolutionOutcome::Superseded => FlightGovernanceStatus::Superseded,
1134        ClaimResolutionOutcome::Revoked => FlightGovernanceStatus::Revoked,
1135    };
1136    Some(FlightGovernanceState {
1137        raw_igc_hash: raw_igc_hash.clone(),
1138        owner_pilot_id: Some(claim.pilot_id.clone()),
1139        status,
1140        baseline_ready: true,
1141        recorded_at: active.created_at.clone(),
1142    })
1143}
1144
1145fn resolve_pending_owner_claim(
1146    raw_igc_hash: &Blake3Hex,
1147    claims: &[OwnerClaimRecord],
1148) -> Option<FlightGovernanceState> {
1149    match claims {
1150        [] => None,
1151        [claim] => Some(FlightGovernanceState {
1152            raw_igc_hash: raw_igc_hash.clone(),
1153            owner_pilot_id: Some(claim.pilot_id.clone()),
1154            status: FlightGovernanceStatus::Pending,
1155            baseline_ready: true,
1156            recorded_at: claim.created_at.clone(),
1157        }),
1158        _ => Some(FlightGovernanceState {
1159            raw_igc_hash: raw_igc_hash.clone(),
1160            owner_pilot_id: None,
1161            status: FlightGovernanceStatus::Pending,
1162            baseline_ready: true,
1163            recorded_at: claims
1164                .iter()
1165                .map(|claim| claim.created_at.as_str())
1166                .max()
1167                .unwrap_or_default()
1168                .to_string(),
1169        }),
1170    }
1171}
1172
1173fn validate_trusted_resolver_id(resolver_id: &str) -> Result<(), GovernanceStoreError> {
1174    if resolver_id.len() == 64
1175        && resolver_id
1176            .bytes()
1177            .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
1178    {
1179        return Ok(());
1180    }
1181    Err(GovernanceStoreError::FlightGovernanceRecord(
1182        FlightGovernanceRecordError::ResolverIdEncoding(resolver_id.to_string()),
1183    ))
1184}
1185
1186fn select_publication_mode_tip(records: &[PublicationModeRecord]) -> Option<PublicationModeRecord> {
1187    let by_id = records
1188        .iter()
1189        .map(|record| (record.record_id.clone(), record))
1190        .collect::<HashMap<_, _>>();
1191    records
1192        .iter()
1193        .filter(|record| publication_mode_chain_complete(record, &by_id))
1194        .filter(|record| {
1195            !records
1196                .iter()
1197                .any(|other| other.supersedes.as_ref() == Some(&record.record_id))
1198        })
1199        .min_by(|left, right| left.record_id.cmp(&right.record_id))
1200        .cloned()
1201}
1202
1203fn publication_mode_chain_complete(
1204    record: &PublicationModeRecord,
1205    by_id: &HashMap<Blake3Hex, &PublicationModeRecord>,
1206) -> bool {
1207    let mut seen = HashSet::new();
1208    let mut current = record;
1209    while let Some(parent_id) = &current.supersedes {
1210        if !seen.insert(parent_id.clone()) {
1211            return false;
1212        }
1213        let Some(parent) = by_id.get(parent_id).copied() else {
1214            return false;
1215        };
1216        current = parent;
1217    }
1218    true
1219}
1220
1221impl GovernanceLookup for GovernanceStore {
1222    fn load_pilot_auth_did_records(
1223        &self,
1224        pilot_id: &PilotId,
1225    ) -> Result<Vec<PilotAuthDidRecord>, GovernanceStoreError> {
1226        self.load_pilot_auth_did_records(pilot_id)
1227    }
1228
1229    fn resolve_pilot_auth_did_state(
1230        &self,
1231        pilot_id: &PilotId,
1232    ) -> Result<PilotAuthDidState, GovernanceStoreError> {
1233        self.resolve_pilot_auth_did_state(pilot_id)
1234    }
1235
1236    fn load_private_access_rotation_records(
1237        &self,
1238        pilot_id: &PilotId,
1239    ) -> Result<Vec<PrivateAccessRotationRecord>, GovernanceStoreError> {
1240        self.load_private_access_rotation_records(pilot_id)
1241    }
1242
1243    fn resolve_private_access_rotation_state(
1244        &self,
1245        pilot_id: &PilotId,
1246    ) -> Result<PrivateAccessRotationState, GovernanceStoreError> {
1247        self.resolve_private_access_rotation_state(pilot_id)
1248    }
1249
1250    fn load_flight_governance_state(
1251        &self,
1252        raw_igc_hash: &Blake3Hex,
1253    ) -> Result<Option<FlightGovernanceState>, GovernanceStoreError> {
1254        self.load_flight_governance_state(raw_igc_hash)
1255    }
1256
1257    fn resolve_flight_governance_state(
1258        &self,
1259        raw_igc_hash: &Blake3Hex,
1260    ) -> Result<Option<FlightGovernanceState>, GovernanceStoreError> {
1261        self.resolve_flight_governance_state(raw_igc_hash)
1262    }
1263}
1264
1265fn write_json_file_atomic<T: serde::Serialize>(
1266    path: &Path,
1267    value: &T,
1268) -> Result<(), GovernanceStoreError> {
1269    write_json_file_atomic_impl(
1270        path,
1271        value,
1272        |parent| {
1273            std::fs::create_dir_all(parent)?;
1274            Ok(())
1275        },
1276        |tmp_path, bytes| {
1277            std::fs::write(tmp_path, bytes)?;
1278            Ok(())
1279        },
1280        GovernanceStoreError::MissingParentDirectory,
1281    )
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286    use crate::{
1287        ClaimApprovalRecord, ClaimChallengeRecord, ClaimResolutionOutcome, ClaimResolutionRecord,
1288        DeletionRequestRecord, FlightGovernanceState, FlightGovernanceStatus,
1289        IdentityRecoveryBasis, IdentityRecoveryRecord, OwnerClaimRecord, PilotAuthDidStateStatus,
1290        PilotAuthDidSyncError, PrivateAccessRotationRecord, PrivateAccessRotationStateStatus,
1291        PublicationMode, PublicationModeRecord, ResolverProfile, RosterUpdateAction,
1292        RosterUpdateRecord, governance::sync::PilotAuthDidSyncRequest, identity::DidKey,
1293    };
1294
1295    use super::*;
1296
1297    fn deterministic_secret_key(byte: u8) -> iroh::SecretKey {
1298        iroh::SecretKey::from_bytes(&[byte; 32])
1299    }
1300
1301    fn temp_store() -> (GovernanceStore, tempfile::TempDir) {
1302        let dir = tempfile::tempdir().unwrap();
1303        let store = GovernanceStore::for_data_dir(dir.path());
1304        store.init().unwrap();
1305        (store, dir)
1306    }
1307
1308    fn resolver_profile(name: &str) -> ResolverProfile {
1309        ResolverProfile {
1310            display_name: name.to_string(),
1311            service_url: format!("https://{name}.example.org"),
1312            privacy_policy_url: format!("https://{name}.example.org/privacy"),
1313            public_key_url: format!("https://{name}.example.org/.well-known/igc-net-resolver-key"),
1314        }
1315    }
1316
1317    #[test]
1318    fn persist_and_load_round_trip() {
1319        let (store, _dir) = temp_store();
1320        let record = PilotAuthDidRecord::issue(
1321            &deterministic_secret_key(91),
1322            DidKey::from_public_key(deterministic_secret_key(92).public()),
1323            None,
1324            "2026-05-01T09:14:00Z",
1325        )
1326        .unwrap();
1327
1328        store.persist_pilot_auth_did_record(&record).unwrap();
1329        let loaded = store.load_pilot_auth_did_records(&record.pilot_id).unwrap();
1330        assert_eq!(loaded, vec![record]);
1331    }
1332
1333    #[test]
1334    fn rauth_08_state_resolution_distinguishes_absent_and_authoritative() {
1335        let (store, _dir) = temp_store();
1336        let pilot_id = PilotId::from_public_key(deterministic_secret_key(101).public());
1337        let absent = store.resolve_pilot_auth_did_state(&pilot_id).unwrap();
1338        assert_eq!(
1339            absent.status(),
1340            super::super::state::PilotAuthDidStateStatus::Absent
1341        );
1342
1343        let record = PilotAuthDidRecord::issue(
1344            &deterministic_secret_key(101),
1345            DidKey::from_public_key(deterministic_secret_key(102).public()),
1346            None,
1347            "2026-05-01T09:14:00Z",
1348        )
1349        .unwrap();
1350        store.persist_pilot_auth_did_record(&record).unwrap();
1351
1352        let authoritative = store
1353            .resolve_pilot_auth_did_state(&record.pilot_id)
1354            .unwrap();
1355        assert_eq!(
1356            authoritative.status(),
1357            super::super::state::PilotAuthDidStateStatus::Authoritative
1358        );
1359    }
1360
1361    #[test]
1362    fn private_access_rotation_round_trip_and_resolution() {
1363        let (store, _dir) = temp_store();
1364        let pilot_root = deterministic_secret_key(151);
1365        let first = PrivateAccessRotationRecord::issue(
1366            &pilot_root,
1367            deterministic_secret_key(152).public(),
1368            None,
1369            "2026-05-01T09:14:00Z",
1370        )
1371        .unwrap();
1372        let second = PrivateAccessRotationRecord::issue(
1373            &pilot_root,
1374            deterministic_secret_key(153).public(),
1375            Some(first.record_id.clone()),
1376            "2026-05-01T10:14:00Z",
1377        )
1378        .unwrap();
1379
1380        store
1381            .persist_private_access_rotation_record(&first)
1382            .unwrap();
1383        store
1384            .persist_private_access_rotation_record(&second)
1385            .unwrap();
1386
1387        let loaded = store
1388            .load_private_access_rotation_records(&first.pilot_id)
1389            .unwrap();
1390        assert_eq!(loaded.len(), 2);
1391
1392        let state = store
1393            .resolve_private_access_rotation_state(&first.pilot_id)
1394            .unwrap();
1395        assert_eq!(
1396            state.status(),
1397            PrivateAccessRotationStateStatus::Authoritative
1398        );
1399        assert_eq!(state.authoritative.unwrap(), second);
1400    }
1401
1402    #[test]
1403    fn private_access_rotation_missing_parent_is_tentative() {
1404        let (store, _dir) = temp_store();
1405        let pilot_root = deterministic_secret_key(154);
1406        let child = PrivateAccessRotationRecord::issue(
1407            &pilot_root,
1408            deterministic_secret_key(155).public(),
1409            Some(Blake3Hex::parse("a".repeat(64)).unwrap()),
1410            "2026-05-01T10:14:00Z",
1411        )
1412        .unwrap();
1413
1414        store
1415            .persist_private_access_rotation_record(&child)
1416            .unwrap();
1417
1418        let state = store
1419            .resolve_private_access_rotation_state(&child.pilot_id)
1420            .unwrap();
1421        assert_eq!(state.status(), PrivateAccessRotationStateStatus::Tentative);
1422        assert_eq!(state.tentative_record_ids, vec![child.record_id]);
1423    }
1424
1425    #[test]
1426    fn flight_governance_state_round_trip() {
1427        let (store, _dir) = temp_store();
1428        let raw_igc_hash = Blake3Hex::parse("b".repeat(64)).unwrap();
1429        let owner = PilotId::from_public_key(deterministic_secret_key(156).public());
1430        let state = FlightGovernanceState::approved_owner(
1431            raw_igc_hash.clone(),
1432            owner.clone(),
1433            "2026-05-01T09:14:00Z",
1434        );
1435
1436        store.persist_flight_governance_state(&state).unwrap();
1437
1438        let loaded = store
1439            .load_flight_governance_state(&raw_igc_hash)
1440            .unwrap()
1441            .unwrap();
1442        assert_eq!(loaded, state);
1443        assert_eq!(loaded.status, FlightGovernanceStatus::Approved);
1444        assert!(loaded.restricted_serving_ready_for(&owner));
1445    }
1446
1447    #[test]
1448    fn absent_flight_governance_state_returns_none() {
1449        let (store, _dir) = temp_store();
1450        assert!(
1451            store
1452                .load_flight_governance_state(&Blake3Hex::parse("c".repeat(64)).unwrap())
1453                .unwrap()
1454                .is_none()
1455        );
1456    }
1457
1458    #[test]
1459    fn owner_claim_resolves_to_pending_flight_governance_state() {
1460        let (store, _dir) = temp_store();
1461        let pilot_root = deterministic_secret_key(157);
1462        let raw_igc_hash = Blake3Hex::parse("d".repeat(64)).unwrap();
1463        let claim = OwnerClaimRecord::issue(
1464            &pilot_root,
1465            raw_igc_hash.clone(),
1466            "2026-05-01T09:14:00Z",
1467            Vec::new(),
1468        )
1469        .unwrap();
1470
1471        store.persist_owner_claim_record(&claim).unwrap();
1472
1473        let resolved = store
1474            .resolve_flight_governance_state(&raw_igc_hash)
1475            .unwrap()
1476            .unwrap();
1477        assert_eq!(resolved.status, FlightGovernanceStatus::Pending);
1478        assert_eq!(resolved.owner_pilot_id, Some(claim.pilot_id));
1479    }
1480
1481    #[test]
1482    fn owner_deletion_request_resolves_applied_state_to_deleted() {
1483        let (store, _dir) = temp_store();
1484        let pilot_root = deterministic_secret_key(158);
1485        let raw_igc_hash = Blake3Hex::parse("e".repeat(64)).unwrap();
1486        let owner = PilotId::from_public_key(pilot_root.public());
1487        store
1488            .persist_flight_governance_state(&FlightGovernanceState::approved_owner(
1489                raw_igc_hash.clone(),
1490                owner,
1491                "2026-05-01T09:14:00Z",
1492            ))
1493            .unwrap();
1494        let deletion =
1495            DeletionRequestRecord::issue(&pilot_root, raw_igc_hash.clone(), "2026-05-01T10:14:00Z")
1496                .unwrap();
1497
1498        store.persist_deletion_request_record(&deletion).unwrap();
1499
1500        let resolved = store
1501            .resolve_flight_governance_state(&raw_igc_hash)
1502            .unwrap()
1503            .unwrap();
1504        assert_eq!(resolved.status, FlightGovernanceStatus::Deleted);
1505        assert!(resolved.serving_blocked());
1506    }
1507
1508    #[test]
1509    fn publication_mode_records_select_complete_chain_tip() {
1510        let (store, _dir) = temp_store();
1511        let pilot_root = deterministic_secret_key(159);
1512        let raw_igc_hash = Blake3Hex::parse("9".repeat(64)).unwrap();
1513        let first = PublicationModeRecord::issue(
1514            &pilot_root,
1515            raw_igc_hash.clone(),
1516            PublicationMode::Public,
1517            None,
1518            None,
1519            "2026-05-01T09:14:00Z",
1520        )
1521        .unwrap();
1522        let second = PublicationModeRecord::issue(
1523            &pilot_root,
1524            raw_igc_hash.clone(),
1525            PublicationMode::Private,
1526            None,
1527            Some(first.record_id.clone()),
1528            "2026-05-01T10:14:00Z",
1529        )
1530        .unwrap();
1531        let incomplete = PublicationModeRecord::issue(
1532            &pilot_root,
1533            raw_igc_hash.clone(),
1534            PublicationMode::Protected,
1535            Some(Blake3Hex::parse("a".repeat(64)).unwrap()),
1536            Some(Blake3Hex::parse("b".repeat(64)).unwrap()),
1537            "2026-05-01T11:14:00Z",
1538        )
1539        .unwrap();
1540
1541        store.persist_publication_mode_record(&first).unwrap();
1542        store.persist_publication_mode_record(&second).unwrap();
1543        store.persist_publication_mode_record(&incomplete).unwrap();
1544
1545        let loaded = store.load_publication_mode_records(&raw_igc_hash).unwrap();
1546        assert_eq!(loaded.len(), 3);
1547        let resolved = store
1548            .resolve_publication_mode_record(&raw_igc_hash)
1549            .unwrap()
1550            .unwrap();
1551        assert_eq!(resolved, second);
1552    }
1553
1554    #[test]
1555    fn publication_mode_record_missing_parent_has_no_tip() {
1556        let (store, _dir) = temp_store();
1557        let pilot_root = deterministic_secret_key(160);
1558        let raw_igc_hash = Blake3Hex::parse("a".repeat(64)).unwrap();
1559        let incomplete = PublicationModeRecord::issue(
1560            &pilot_root,
1561            raw_igc_hash.clone(),
1562            PublicationMode::Private,
1563            None,
1564            Some(Blake3Hex::parse("b".repeat(64)).unwrap()),
1565            "2026-05-01T09:14:00Z",
1566        )
1567        .unwrap();
1568
1569        store.persist_publication_mode_record(&incomplete).unwrap();
1570
1571        assert!(
1572            store
1573                .resolve_publication_mode_record(&raw_igc_hash)
1574                .unwrap()
1575                .is_none()
1576        );
1577    }
1578
1579    #[test]
1580    fn trusted_approval_resolves_claim_to_approved_owner() {
1581        let (store, _dir) = temp_store();
1582        let pilot_root = deterministic_secret_key(162);
1583        let resolver = deterministic_secret_key(163);
1584        let raw_igc_hash = Blake3Hex::parse("f".repeat(64)).unwrap();
1585        let claim = OwnerClaimRecord::issue(
1586            &pilot_root,
1587            raw_igc_hash.clone(),
1588            "2026-05-01T09:14:00Z",
1589            Vec::new(),
1590        )
1591        .unwrap();
1592        let approval = ClaimApprovalRecord::issue(
1593            &resolver,
1594            claim.record_id.clone(),
1595            raw_igc_hash.clone(),
1596            "2026-05-01T10:14:00Z",
1597        )
1598        .unwrap();
1599
1600        store.persist_owner_claim_record(&claim).unwrap();
1601        store.persist_claim_approval_record(&approval).unwrap();
1602        store.trust_resolver(&approval.resolver_id).unwrap();
1603
1604        let resolved = store
1605            .resolve_flight_governance_state(&raw_igc_hash)
1606            .unwrap()
1607            .unwrap();
1608        assert_eq!(resolved.status, FlightGovernanceStatus::Approved);
1609        assert_eq!(resolved.owner_pilot_id, Some(claim.pilot_id));
1610    }
1611
1612    #[test]
1613    fn untrusted_approval_is_ignored() {
1614        let (store, _dir) = temp_store();
1615        let pilot_root = deterministic_secret_key(164);
1616        let resolver = deterministic_secret_key(165);
1617        let raw_igc_hash = Blake3Hex::parse("1".repeat(64)).unwrap();
1618        let claim = OwnerClaimRecord::issue(
1619            &pilot_root,
1620            raw_igc_hash.clone(),
1621            "2026-05-01T09:14:00Z",
1622            Vec::new(),
1623        )
1624        .unwrap();
1625        let approval = ClaimApprovalRecord::issue(
1626            &resolver,
1627            claim.record_id.clone(),
1628            raw_igc_hash.clone(),
1629            "2026-05-01T10:14:00Z",
1630        )
1631        .unwrap();
1632
1633        store.persist_owner_claim_record(&claim).unwrap();
1634        store.persist_claim_approval_record(&approval).unwrap();
1635
1636        let resolved = store
1637            .resolve_flight_governance_state(&raw_igc_hash)
1638            .unwrap()
1639            .unwrap();
1640        assert_eq!(resolved.status, FlightGovernanceStatus::Pending);
1641    }
1642
1643    #[test]
1644    fn trusted_challenge_resolves_approved_claim_to_contested() {
1645        let (store, _dir) = temp_store();
1646        let pilot_root = deterministic_secret_key(166);
1647        let resolver_a = deterministic_secret_key(167);
1648        let resolver_b = deterministic_secret_key(168);
1649        let raw_igc_hash = Blake3Hex::parse("2".repeat(64)).unwrap();
1650        let claim = OwnerClaimRecord::issue(
1651            &pilot_root,
1652            raw_igc_hash.clone(),
1653            "2026-05-01T09:14:00Z",
1654            Vec::new(),
1655        )
1656        .unwrap();
1657        let approval = ClaimApprovalRecord::issue(
1658            &resolver_a,
1659            claim.record_id.clone(),
1660            raw_igc_hash.clone(),
1661            "2026-05-01T10:14:00Z",
1662        )
1663        .unwrap();
1664        let challenge = ClaimChallengeRecord::issue(
1665            &resolver_b,
1666            claim.record_id.clone(),
1667            raw_igc_hash.clone(),
1668            "ownership_dispute",
1669            "2026-05-01T11:14:00Z",
1670        )
1671        .unwrap();
1672
1673        store.persist_owner_claim_record(&claim).unwrap();
1674        store.persist_claim_approval_record(&approval).unwrap();
1675        store.persist_claim_challenge_record(&challenge).unwrap();
1676        store.trust_resolver(&approval.resolver_id).unwrap();
1677        store
1678            .trust_resolver(&challenge.challenger_resolver_id)
1679            .unwrap();
1680
1681        let resolved = store
1682            .resolve_flight_governance_state(&raw_igc_hash)
1683            .unwrap()
1684            .unwrap();
1685        assert_eq!(resolved.status, FlightGovernanceStatus::Contested);
1686        assert!(resolved.serving_blocked());
1687    }
1688
1689    #[test]
1690    fn challenge_lapses_after_fifteen_days_without_resolution() {
1691        let (store, _dir) = temp_store();
1692        let pilot_root = deterministic_secret_key(180);
1693        let resolver_a = deterministic_secret_key(181);
1694        let resolver_b = deterministic_secret_key(182);
1695        let raw_igc_hash = Blake3Hex::parse("7".repeat(64)).unwrap();
1696        let claim = OwnerClaimRecord::issue(
1697            &pilot_root,
1698            raw_igc_hash.clone(),
1699            "2026-05-01T09:14:00Z",
1700            Vec::new(),
1701        )
1702        .unwrap();
1703        let approval = ClaimApprovalRecord::issue(
1704            &resolver_a,
1705            claim.record_id.clone(),
1706            raw_igc_hash.clone(),
1707            "2026-05-01T10:14:00Z",
1708        )
1709        .unwrap();
1710        let challenge = ClaimChallengeRecord::issue(
1711            &resolver_b,
1712            claim.record_id.clone(),
1713            raw_igc_hash.clone(),
1714            "ownership_dispute",
1715            "2026-05-01T11:14:00Z",
1716        )
1717        .unwrap();
1718
1719        store.persist_owner_claim_record(&claim).unwrap();
1720        store.persist_claim_approval_record(&approval).unwrap();
1721        store.persist_claim_challenge_record(&challenge).unwrap();
1722        store.trust_resolver(&approval.resolver_id).unwrap();
1723        store
1724            .trust_resolver(&challenge.challenger_resolver_id)
1725            .unwrap();
1726
1727        let contested = store
1728            .resolve_flight_governance_state_at(&raw_igc_hash, "2026-05-16T11:13:59Z")
1729            .unwrap()
1730            .unwrap();
1731        assert_eq!(contested.status, FlightGovernanceStatus::Contested);
1732
1733        let approved = store
1734            .resolve_flight_governance_state_at(&raw_igc_hash, "2026-05-16T11:14:00Z")
1735            .unwrap()
1736            .unwrap();
1737        assert_eq!(approved.status, FlightGovernanceStatus::Approved);
1738        assert_eq!(approved.owner_pilot_id, Some(claim.pilot_id));
1739    }
1740
1741    #[test]
1742    fn identity_recovery_transfers_approved_owner_state() {
1743        let (store, _dir) = temp_store();
1744        let old_pilot_root = deterministic_secret_key(183);
1745        let new_pilot_root = deterministic_secret_key(184);
1746        let resolver = deterministic_secret_key(185);
1747        let raw_igc_hash = Blake3Hex::parse("8".repeat(64)).unwrap();
1748        let claim = OwnerClaimRecord::issue(
1749            &old_pilot_root,
1750            raw_igc_hash.clone(),
1751            "2026-05-01T09:14:00Z",
1752            Vec::new(),
1753        )
1754        .unwrap();
1755        let approval = ClaimApprovalRecord::issue(
1756            &resolver,
1757            claim.record_id.clone(),
1758            raw_igc_hash.clone(),
1759            "2026-05-01T10:14:00Z",
1760        )
1761        .unwrap();
1762        let recovery = IdentityRecoveryRecord::issue(
1763            &resolver,
1764            claim.pilot_id.clone(),
1765            PilotId::from_public_key(new_pilot_root.public()),
1766            IdentityRecoveryBasis::KeyLossRecovery,
1767            "2026-05-02T09:14:00Z",
1768        )
1769        .unwrap();
1770
1771        store.persist_owner_claim_record(&claim).unwrap();
1772        store.persist_claim_approval_record(&approval).unwrap();
1773        store.persist_identity_recovery_record(&recovery).unwrap();
1774        store.trust_resolver(&approval.resolver_id).unwrap();
1775
1776        let resolved = store
1777            .resolve_flight_governance_state(&raw_igc_hash)
1778            .unwrap()
1779            .unwrap();
1780        assert_eq!(resolved.status, FlightGovernanceStatus::Approved);
1781        assert_eq!(resolved.owner_pilot_id, Some(recovery.new_pilot_id));
1782        assert_eq!(resolved.recorded_at, recovery.created_at);
1783    }
1784
1785    #[test]
1786    fn trusted_resolution_rejects_claim_and_blocks_serving() {
1787        let (store, _dir) = temp_store();
1788        let pilot_root = deterministic_secret_key(169);
1789        let resolver = deterministic_secret_key(170);
1790        let raw_igc_hash = Blake3Hex::parse("3".repeat(64)).unwrap();
1791        let claim = OwnerClaimRecord::issue(
1792            &pilot_root,
1793            raw_igc_hash.clone(),
1794            "2026-05-01T09:14:00Z",
1795            Vec::new(),
1796        )
1797        .unwrap();
1798        let resolution = ClaimResolutionRecord::issue(
1799            &resolver,
1800            raw_igc_hash.clone(),
1801            claim.record_id.clone(),
1802            ClaimResolutionOutcome::Rejected,
1803            vec!["manual_review".to_string()],
1804            Vec::new(),
1805            "2026-05-01T12:14:00Z",
1806        )
1807        .unwrap();
1808
1809        store.persist_owner_claim_record(&claim).unwrap();
1810        store.persist_claim_resolution_record(&resolution).unwrap();
1811        store.trust_resolver(&resolution.resolver_id).unwrap();
1812
1813        let resolved = store
1814            .resolve_flight_governance_state(&raw_igc_hash)
1815            .unwrap()
1816            .unwrap();
1817        assert_eq!(resolved.status, FlightGovernanceStatus::Rejected);
1818        assert!(resolved.serving_blocked());
1819    }
1820
1821    #[test]
1822    fn generic_governance_record_ingestion_persists_and_deduplicates() {
1823        let (store, _dir) = temp_store();
1824        let pilot_root = deterministic_secret_key(171);
1825        let resolver = deterministic_secret_key(172);
1826        let raw_igc_hash = Blake3Hex::parse("4".repeat(64)).unwrap();
1827        let claim = OwnerClaimRecord::issue(
1828            &pilot_root,
1829            raw_igc_hash.clone(),
1830            "2026-05-01T09:14:00Z",
1831            Vec::new(),
1832        )
1833        .unwrap();
1834        let approval = ClaimApprovalRecord::issue(
1835            &resolver,
1836            claim.record_id.clone(),
1837            raw_igc_hash.clone(),
1838            "2026-05-01T10:14:00Z",
1839        )
1840        .unwrap();
1841        let publication_mode = PublicationModeRecord::issue(
1842            &pilot_root,
1843            raw_igc_hash.clone(),
1844            PublicationMode::Private,
1845            None,
1846            None,
1847            "2026-05-01T11:14:00Z",
1848        )
1849        .unwrap();
1850
1851        assert!(
1852            store
1853                .apply_governance_record_json(&serde_json::to_vec(&claim).unwrap())
1854                .unwrap()
1855        );
1856        assert!(
1857            !store
1858                .apply_governance_record_json(&serde_json::to_vec(&claim).unwrap())
1859                .unwrap()
1860        );
1861        assert!(
1862            store
1863                .apply_governance_record(&GovernanceRecord::ClaimApproval(approval.clone()))
1864                .unwrap()
1865        );
1866        assert!(
1867            store
1868                .apply_governance_record_json(&serde_json::to_vec(&publication_mode).unwrap())
1869                .unwrap()
1870        );
1871        assert!(
1872            !store
1873                .apply_governance_record_json(&serde_json::to_vec(&publication_mode).unwrap())
1874                .unwrap()
1875        );
1876        store.trust_resolver(&approval.resolver_id).unwrap();
1877
1878        let resolved = store
1879            .resolve_flight_governance_state(&raw_igc_hash)
1880            .unwrap()
1881            .unwrap();
1882        assert_eq!(resolved.status, FlightGovernanceStatus::Approved);
1883        assert_eq!(resolved.owner_pilot_id, Some(claim.pilot_id));
1884        assert_eq!(
1885            store
1886                .resolve_publication_mode_record(&raw_igc_hash)
1887                .unwrap(),
1888            Some(publication_mode)
1889        );
1890    }
1891
1892    #[test]
1893    fn roster_update_add_authorizes_resolver_records() {
1894        let (store, _dir) = temp_store();
1895        let bootstrap_resolver = deterministic_secret_key(173);
1896        let added_resolver = deterministic_secret_key(174);
1897        let pilot_root = deterministic_secret_key(175);
1898        let raw_igc_hash = Blake3Hex::parse("5".repeat(64)).unwrap();
1899        let claim = OwnerClaimRecord::issue(
1900            &pilot_root,
1901            raw_igc_hash.clone(),
1902            "2026-05-01T09:14:00Z",
1903            Vec::new(),
1904        )
1905        .unwrap();
1906        let update = RosterUpdateRecord::issue(
1907            &bootstrap_resolver,
1908            RosterUpdateAction::Add,
1909            added_resolver.public().to_string(),
1910            Some(resolver_profile("added-resolver")),
1911            "2026-05-01T09:30:00Z",
1912        )
1913        .unwrap();
1914        let approval = ClaimApprovalRecord::issue(
1915            &added_resolver,
1916            claim.record_id.clone(),
1917            raw_igc_hash.clone(),
1918            "2026-05-01T10:14:00Z",
1919        )
1920        .unwrap();
1921
1922        store
1923            .trust_resolver(&bootstrap_resolver.public().to_string())
1924            .unwrap();
1925        store.persist_owner_claim_record(&claim).unwrap();
1926        store.persist_roster_update_record(&update).unwrap();
1927        store.persist_claim_approval_record(&approval).unwrap();
1928
1929        assert!(
1930            store
1931                .effective_trusted_resolvers()
1932                .unwrap()
1933                .contains(&approval.resolver_id)
1934        );
1935        let resolved = store
1936            .resolve_flight_governance_state(&raw_igc_hash)
1937            .unwrap()
1938            .unwrap();
1939        assert_eq!(resolved.status, FlightGovernanceStatus::Approved);
1940    }
1941
1942    #[test]
1943    fn roster_update_remove_deauthorizes_resolver_records() {
1944        let (store, _dir) = temp_store();
1945        let bootstrap_resolver = deterministic_secret_key(176);
1946        let removed_resolver = deterministic_secret_key(177);
1947        let pilot_root = deterministic_secret_key(178);
1948        let raw_igc_hash = Blake3Hex::parse("6".repeat(64)).unwrap();
1949        let claim = OwnerClaimRecord::issue(
1950            &pilot_root,
1951            raw_igc_hash.clone(),
1952            "2026-05-01T09:14:00Z",
1953            Vec::new(),
1954        )
1955        .unwrap();
1956        let remove = RosterUpdateRecord::issue(
1957            &bootstrap_resolver,
1958            RosterUpdateAction::Remove,
1959            removed_resolver.public().to_string(),
1960            None,
1961            "2026-05-01T09:30:00Z",
1962        )
1963        .unwrap();
1964        let approval = ClaimApprovalRecord::issue(
1965            &removed_resolver,
1966            claim.record_id.clone(),
1967            raw_igc_hash.clone(),
1968            "2026-05-01T10:14:00Z",
1969        )
1970        .unwrap();
1971
1972        store
1973            .trust_resolver(&bootstrap_resolver.public().to_string())
1974            .unwrap();
1975        store
1976            .trust_resolver(&removed_resolver.public().to_string())
1977            .unwrap();
1978        store.persist_owner_claim_record(&claim).unwrap();
1979        store.persist_roster_update_record(&remove).unwrap();
1980        store.persist_claim_approval_record(&approval).unwrap();
1981
1982        assert!(
1983            !store
1984                .effective_trusted_resolvers()
1985                .unwrap()
1986                .contains(&approval.resolver_id)
1987        );
1988        let resolved = store
1989            .resolve_flight_governance_state(&raw_igc_hash)
1990            .unwrap()
1991            .unwrap();
1992        assert_eq!(resolved.status, FlightGovernanceStatus::Pending);
1993    }
1994
1995    #[test]
1996    fn simulated_crash_temp_file_does_not_corrupt_previous_state() {
1997        let (store, _dir) = temp_store();
1998        let record = PilotAuthDidRecord::issue(
1999            &deterministic_secret_key(111),
2000            DidKey::from_public_key(deterministic_secret_key(112).public()),
2001            None,
2002            "2026-05-01T09:14:00Z",
2003        )
2004        .unwrap();
2005        store.persist_pilot_auth_did_record(&record).unwrap();
2006
2007        let pilot_dir = store.pilot_auth_did_pilot_dir(&record.pilot_id);
2008        std::fs::create_dir_all(&pilot_dir).unwrap();
2009        std::fs::write(pilot_dir.join(".current.json.tmp-crash"), b"{bad json").unwrap();
2010
2011        let loaded = store.load_pilot_auth_did_records(&record.pilot_id).unwrap();
2012        assert_eq!(loaded, vec![record]);
2013    }
2014
2015    #[test]
2016    fn rgsync_02_restart_after_missing_rotation_can_recover_authoritative_state() {
2017        let (publisher, _publisher_dir) = temp_store();
2018        let (rejoining, _rejoining_dir) = temp_store();
2019
2020        let pilot_root = deterministic_secret_key(121);
2021        let initial = PilotAuthDidRecord::issue(
2022            &pilot_root,
2023            DidKey::from_public_key(deterministic_secret_key(122).public()),
2024            None,
2025            "2026-05-01T09:14:00Z",
2026        )
2027        .unwrap();
2028        let rotated = PilotAuthDidRecord::issue(
2029            &pilot_root,
2030            DidKey::from_public_key(deterministic_secret_key(123).public()),
2031            Some(initial.record_id.clone()),
2032            "2026-05-01T10:14:00Z",
2033        )
2034        .unwrap();
2035
2036        publisher.persist_pilot_auth_did_record(&initial).unwrap();
2037        publisher.persist_pilot_auth_did_record(&rotated).unwrap();
2038        rejoining.persist_pilot_auth_did_record(&initial).unwrap();
2039
2040        let before = rejoining
2041            .resolve_pilot_auth_did_state(&initial.pilot_id)
2042            .unwrap();
2043        assert_eq!(before.status(), PilotAuthDidStateStatus::Authoritative);
2044        assert_eq!(
2045            before.authoritative.as_ref().unwrap().record_id,
2046            initial.record_id
2047        );
2048
2049        let request = PilotAuthDidSyncRequest::from_state(&before);
2050        let response = publisher.prepare_pilot_auth_did_sync(&request).unwrap();
2051        assert_eq!(response.records, vec![rotated.clone()]);
2052
2053        let applied = rejoining.apply_pilot_auth_did_sync(&response).unwrap();
2054        assert_eq!(applied, 1);
2055
2056        let after = rejoining
2057            .resolve_pilot_auth_did_state(&initial.pilot_id)
2058            .unwrap();
2059        assert_eq!(after.status(), PilotAuthDidStateStatus::Authoritative);
2060        assert_eq!(after.authoritative.unwrap().record_id, rotated.record_id);
2061    }
2062
2063    #[test]
2064    fn rauth_12_partial_chain_state_is_tentative_until_catch_up_completes() {
2065        let (store, _dir) = temp_store();
2066        let pilot_root = deterministic_secret_key(131);
2067        let initial = PilotAuthDidRecord::issue(
2068            &pilot_root,
2069            DidKey::from_public_key(deterministic_secret_key(132).public()),
2070            None,
2071            "2026-05-01T09:14:00Z",
2072        )
2073        .unwrap();
2074        let rotated = PilotAuthDidRecord::issue(
2075            &pilot_root,
2076            DidKey::from_public_key(deterministic_secret_key(133).public()),
2077            Some(initial.record_id.clone()),
2078            "2026-05-01T10:14:00Z",
2079        )
2080        .unwrap();
2081
2082        store.persist_pilot_auth_did_record(&rotated).unwrap();
2083        let state = store
2084            .resolve_pilot_auth_did_state(&rotated.pilot_id)
2085            .unwrap();
2086
2087        assert_eq!(state.status(), PilotAuthDidStateStatus::Tentative);
2088        assert!(state.requires_catch_up());
2089        assert_eq!(state.tentative_record_ids, vec![rotated.record_id]);
2090    }
2091
2092    #[test]
2093    fn rgsync_03_completed_catch_up_upgrades_tentative_state_to_authoritative() {
2094        let (authority, _authority_dir) = temp_store();
2095        let (rejoining, _rejoining_dir) = temp_store();
2096
2097        let pilot_root = deterministic_secret_key(141);
2098        let initial = PilotAuthDidRecord::issue(
2099            &pilot_root,
2100            DidKey::from_public_key(deterministic_secret_key(142).public()),
2101            None,
2102            "2026-05-01T09:14:00Z",
2103        )
2104        .unwrap();
2105        let rotated = PilotAuthDidRecord::issue(
2106            &pilot_root,
2107            DidKey::from_public_key(deterministic_secret_key(143).public()),
2108            Some(initial.record_id.clone()),
2109            "2026-05-01T10:14:00Z",
2110        )
2111        .unwrap();
2112
2113        authority.persist_pilot_auth_did_record(&initial).unwrap();
2114        authority.persist_pilot_auth_did_record(&rotated).unwrap();
2115        rejoining.persist_pilot_auth_did_record(&rotated).unwrap();
2116
2117        let before = rejoining
2118            .resolve_pilot_auth_did_state(&rotated.pilot_id)
2119            .unwrap();
2120        assert_eq!(before.status(), PilotAuthDidStateStatus::Tentative);
2121
2122        let request = PilotAuthDidSyncRequest::from_state(&before);
2123        let response = authority.prepare_pilot_auth_did_sync(&request).unwrap();
2124        assert_eq!(response.records, vec![initial.clone()]);
2125
2126        let applied = rejoining.apply_pilot_auth_did_sync(&response).unwrap();
2127        assert_eq!(applied, 1);
2128
2129        let after = rejoining
2130            .resolve_pilot_auth_did_state(&rotated.pilot_id)
2131            .unwrap();
2132        assert_eq!(after.status(), PilotAuthDidStateStatus::Authoritative);
2133        assert_eq!(after.authoritative.unwrap().record_id, rotated.record_id);
2134    }
2135
2136    #[test]
2137    fn apply_sync_rejects_mixed_pilot_batches() {
2138        let (store, _dir) = temp_store();
2139        let pilot_a = deterministic_secret_key(151);
2140        let pilot_b = deterministic_secret_key(152);
2141        let record = PilotAuthDidRecord::issue(
2142            &pilot_b,
2143            DidKey::from_public_key(deterministic_secret_key(153).public()),
2144            None,
2145            "2026-05-01T09:14:00Z",
2146        )
2147        .unwrap();
2148        let response = PilotAuthDidSyncResponse::new(
2149            PilotId::from_public_key(pilot_a.public()),
2150            vec![record.clone()],
2151        );
2152
2153        let err = store.apply_pilot_auth_did_sync(&response).unwrap_err();
2154        assert!(matches!(
2155            err,
2156            GovernanceStoreError::Sync(PilotAuthDidSyncError::MixedPilotRecord {
2157                expected,
2158                found,
2159                record_id
2160            }) if expected == PilotId::from_public_key(pilot_a.public())
2161                && found == record.pilot_id
2162                && record_id == record.record_id
2163        ));
2164    }
2165
2166    #[test]
2167    fn build_sync_request_uses_full_local_history() {
2168        let (store, _dir) = temp_store();
2169        let pilot_root = deterministic_secret_key(161);
2170        let initial = PilotAuthDidRecord::issue(
2171            &pilot_root,
2172            DidKey::from_public_key(deterministic_secret_key(162).public()),
2173            None,
2174            "2026-05-01T09:14:00Z",
2175        )
2176        .unwrap();
2177        let rotated = PilotAuthDidRecord::issue(
2178            &pilot_root,
2179            DidKey::from_public_key(deterministic_secret_key(163).public()),
2180            Some(initial.record_id.clone()),
2181            "2026-05-01T10:14:00Z",
2182        )
2183        .unwrap();
2184        store.persist_pilot_auth_did_record(&initial).unwrap();
2185        store.persist_pilot_auth_did_record(&rotated).unwrap();
2186
2187        let request = store
2188            .build_pilot_auth_did_sync_request(&initial.pilot_id)
2189            .unwrap();
2190        let mut expected = vec![initial.record_id, rotated.record_id];
2191        expected.sort();
2192        assert_eq!(request.known_record_ids, expected);
2193    }
2194}