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) = ¤t.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}