1use super::*;
2
3fn persist_immutable_federated_lineage_bridge(
4 transaction: &rusqlite::Transaction<'_>,
5 local_capability_id: &str,
6 parent_capability_id: &str,
7 share_id: Option<&str>,
8) -> Result<(), ReceiptStoreError> {
9 if local_capability_id == parent_capability_id {
10 return Err(ReceiptStoreError::Conflict(format!(
11 "capability lineage {local_capability_id} cannot bridge to itself"
12 )));
13 }
14 let projected_parent = transaction
15 .query_row(
16 "SELECT federated_parent_capability_id FROM capability_lineage WHERE capability_id = ?1",
17 params![local_capability_id],
18 |row| row.get::<_, Option<String>>(0),
19 )
20 .optional()?
21 .ok_or_else(|| {
22 ReceiptStoreError::Conflict(format!(
23 "local capability {local_capability_id} is missing for its federation bridge"
24 ))
25 })?;
26 if let Some(existing_parent) = projected_parent.as_deref() {
27 if existing_parent != parent_capability_id {
28 return Err(ReceiptStoreError::Conflict(format!(
29 "capability lineage {local_capability_id} already has federated parent {existing_parent}"
30 )));
31 }
32 }
33 let target_exists: bool = match share_id {
34 Some(share_id) => transaction.query_row(
35 r#"
36 SELECT EXISTS(
37 SELECT 1
38 FROM federated_share_capability_lineage
39 WHERE share_id = ?1 AND capability_id = ?2
40 )
41 "#,
42 params![share_id, parent_capability_id],
43 |row| row.get(0),
44 )?,
45 None => transaction.query_row(
46 "SELECT EXISTS(SELECT 1 FROM capability_lineage WHERE capability_id = ?1)",
47 params![parent_capability_id],
48 |row| row.get(0),
49 )?,
50 };
51 if !target_exists {
52 return Err(ReceiptStoreError::Conflict(format!(
53 "federated lineage bridge {local_capability_id} references missing parent {parent_capability_id}"
54 )));
55 }
56
57 let existing = transaction
58 .query_row(
59 r#"
60 SELECT parent_capability_id, share_id
61 FROM federated_lineage_bridges
62 WHERE local_capability_id = ?1
63 "#,
64 params![local_capability_id],
65 |row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?)),
66 )
67 .optional()?;
68 if let Some((existing_parent, existing_share)) = existing {
69 if existing_parent != parent_capability_id || existing_share.as_deref() != share_id {
70 return Err(ReceiptStoreError::Conflict(format!(
71 "capability lineage bridge {local_capability_id} is immutable"
72 )));
73 }
74 } else {
75 transaction.execute(
76 r#"
77 INSERT INTO federated_lineage_bridges (
78 local_capability_id,
79 parent_capability_id,
80 share_id
81 ) VALUES (?1, ?2, ?3)
82 "#,
83 params![local_capability_id, parent_capability_id, share_id],
84 )?;
85 }
86
87 if projected_parent.is_none() {
88 let max_rowid: i64 = transaction.query_row(
89 "SELECT COALESCE(MAX(rowid), 0) FROM capability_lineage",
90 [],
91 |row| row.get(0),
92 )?;
93 let next_rowid = max_rowid.checked_add(1).ok_or_else(|| {
94 ReceiptStoreError::Conflict(
95 "capability lineage replication sequence is exhausted".to_string(),
96 )
97 })?;
98 let updated = transaction.execute(
99 r#"
100 UPDATE capability_lineage
101 SET federated_parent_capability_id = ?2,
102 rowid = ?3
103 WHERE capability_id = ?1
104 AND federated_parent_capability_id IS NULL
105 "#,
106 params![local_capability_id, parent_capability_id, next_rowid],
107 )?;
108 if updated != 1 {
109 return Err(ReceiptStoreError::Conflict(format!(
110 "capability lineage {local_capability_id} changed while recording its federation bridge"
111 )));
112 }
113 }
114 Ok(())
115}
116
117impl SqliteReceiptStore {
118 pub fn import_federated_evidence_share(
119 &mut self,
120 import: &FederatedEvidenceShareImport,
121 ) -> Result<FederatedEvidenceShareSummary, ReceiptStoreError> {
122 let imported_at = SystemTime::now()
123 .duration_since(UNIX_EPOCH)
124 .map(|duration| duration.as_secs())
125 .unwrap_or(0);
126 let import_owned = import.clone();
127 self.writer_handle().run_write(move |connection| {
128 let import = &import_owned;
129 let imported_at_sqlite = sqlite_i64(imported_at, "federated share imported_at")?;
130 let exported_at_sqlite = sqlite_i64(import.exported_at, "federated share exported_at")?;
131 let tx =
132 connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
133 tx.execute(
134 r#"
135 INSERT INTO federated_evidence_shares (
136 share_id,
137 manifest_hash,
138 imported_at,
139 exported_at,
140 issuer,
141 partner,
142 signer_public_key,
143 require_proofs,
144 query_json
145 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
146 ON CONFLICT(share_id) DO UPDATE SET
147 manifest_hash = excluded.manifest_hash,
148 imported_at = excluded.imported_at,
149 exported_at = excluded.exported_at,
150 issuer = excluded.issuer,
151 partner = excluded.partner,
152 signer_public_key = excluded.signer_public_key,
153 require_proofs = excluded.require_proofs,
154 query_json = excluded.query_json
155 "#,
156 params![
157 import.share_id,
158 import.manifest_hash,
159 imported_at_sqlite,
160 exported_at_sqlite,
161 import.issuer,
162 import.partner,
163 import.signer_public_key,
164 if import.require_proofs { 1_i64 } else { 0_i64 },
165 import.query_json,
166 ],
167 )?;
168
169 let lineage_by_capability = import
170 .capability_lineage
171 .iter()
172 .map(|snapshot| (snapshot.capability_id.as_str(), snapshot))
173 .collect::<BTreeMap<_, _>>();
174
175 for snapshot in &import.capability_lineage {
176 crate::capability_lineage::validate_snapshot_for_transport(snapshot)?;
177 let issued_at = sqlite_i64(snapshot.issued_at, "federated lineage issued_at")?;
178 let expires_at = sqlite_i64(snapshot.expires_at, "federated lineage expires_at")?;
179 let delegation_depth = sqlite_i64(
180 snapshot.delegation_depth,
181 "federated lineage delegation_depth",
182 )?;
183 {
184 let mut statement = tx.prepare(
185 r#"
186 SELECT
187 capability_id,
188 subject_key,
189 issuer_key,
190 issued_at,
191 expires_at,
192 grants_json,
193 delegation_depth,
194 parent_capability_id,
195 federated_parent_capability_id,
196 provenance,
197 signed_capability_json
198 FROM federated_share_capability_lineage
199 WHERE capability_id = ?1
200 "#,
201 )?;
202 let existing = statement.query_map(
203 params![snapshot.capability_id],
204 crate::capability_lineage::snapshot_from_row,
205 )?;
206 for existing in existing {
207 crate::capability_lineage::ensure_snapshots_compatible(
208 &existing?, snapshot,
209 )?;
210 }
211 }
212 let signed_capability_json = snapshot
213 .signed_capability
214 .as_ref()
215 .map(serde_json::to_string)
216 .transpose()?;
217 tx.execute(
218 r#"
219 INSERT INTO federated_share_capability_lineage (
220 share_id,
221 capability_id,
222 subject_key,
223 issuer_key,
224 issued_at,
225 expires_at,
226 grants_json,
227 delegation_depth,
228 parent_capability_id,
229 federated_parent_capability_id,
230 provenance,
231 signed_capability_json
232 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
233 ON CONFLICT(share_id, capability_id) DO UPDATE SET
234 provenance = excluded.provenance,
235 signed_capability_json = excluded.signed_capability_json
236 WHERE federated_share_capability_lineage.provenance = 'legacy_projection'
237 "#,
238 params![
239 import.share_id,
240 snapshot.capability_id,
241 snapshot.subject_key,
242 snapshot.issuer_key,
243 issued_at,
244 expires_at,
245 snapshot.grants_json,
246 delegation_depth,
247 snapshot.parent_capability_id,
248 snapshot.federated_parent_capability_id,
249 snapshot.provenance.as_str(),
250 signed_capability_json,
251 ],
252 )?;
253 }
254
255 for record in &import.tool_receipts {
256 let seq = sqlite_i64(record.seq, "federated receipt seq")?;
257 let timestamp =
258 sqlite_i64(record.receipt.timestamp, "federated receipt timestamp")?;
259 let attribution = extract_receipt_attribution(&record.receipt);
260 let lineage_subject = lineage_by_capability
261 .get(record.receipt.capability_id.as_str())
262 .map(|snapshot| snapshot.subject_key.as_str());
263 let lineage_issuer = lineage_by_capability
264 .get(record.receipt.capability_id.as_str())
265 .map(|snapshot| snapshot.issuer_key.as_str());
266 tx.execute(
267 r#"
268 INSERT INTO federated_share_tool_receipts (
269 share_id,
270 seq,
271 receipt_id,
272 timestamp,
273 capability_id,
274 subject_key,
275 issuer_key,
276 raw_json
277 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
278 ON CONFLICT(share_id, seq) DO UPDATE SET
279 receipt_id = excluded.receipt_id,
280 timestamp = excluded.timestamp,
281 capability_id = excluded.capability_id,
282 subject_key = excluded.subject_key,
283 issuer_key = excluded.issuer_key,
284 raw_json = excluded.raw_json
285 "#,
286 params![
287 import.share_id,
288 seq,
289 record.receipt.id,
290 timestamp,
291 record.receipt.capability_id,
292 attribution
293 .subject_key
294 .or_else(|| lineage_subject.map(ToOwned::to_owned)),
295 attribution
296 .issuer_key
297 .or_else(|| lineage_issuer.map(ToOwned::to_owned)),
298 serde_json::to_string(&record.receipt)?,
299 ],
300 )?;
301 }
302
303 tx.commit()?;
304
305 Ok(FederatedEvidenceShareSummary {
306 share_id: import.share_id.clone(),
307 manifest_hash: import.manifest_hash.clone(),
308 imported_at,
309 exported_at: import.exported_at,
310 issuer: import.issuer.clone(),
311 partner: import.partner.clone(),
312 signer_public_key: import.signer_public_key.clone(),
313 require_proofs: import.require_proofs,
314 tool_receipts: import.tool_receipts.len() as u64,
315 capability_lineage: import.capability_lineage.len() as u64,
316 })
317 })
318 }
319
320 pub fn get_federated_share_for_capability(
321 &self,
322 capability_id: &str,
323 ) -> Result<Option<(FederatedEvidenceShareSummary, CapabilitySnapshot)>, ReceiptStoreError>
324 {
325 let row = self
326 .connection()?
327 .query_row(
328 r#"
329 SELECT
330 s.share_id,
331 s.manifest_hash,
332 s.imported_at,
333 s.exported_at,
334 s.issuer,
335 s.partner,
336 s.signer_public_key,
337 s.require_proofs,
338 (SELECT COUNT(*) FROM federated_share_tool_receipts r WHERE r.share_id = s.share_id),
339 (SELECT COUNT(*) FROM federated_share_capability_lineage c WHERE c.share_id = s.share_id),
340 l.capability_id,
341 l.subject_key,
342 l.issuer_key,
343 l.issued_at,
344 l.expires_at,
345 l.grants_json,
346 l.delegation_depth,
347 l.parent_capability_id,
348 l.federated_parent_capability_id,
349 l.provenance,
350 l.signed_capability_json
351 FROM federated_share_capability_lineage l
352 INNER JOIN federated_evidence_shares s ON s.share_id = l.share_id
353 WHERE l.capability_id = ?1
354 ORDER BY s.imported_at DESC, s.share_id DESC
355 LIMIT 1
356 "#,
357 params![capability_id],
358 |row| {
359 Ok((
360 FederatedEvidenceShareSummary {
361 share_id: row.get::<_, String>(0)?,
362 manifest_hash: row.get::<_, String>(1)?,
363 imported_at:
364 crate::capability_lineage::non_negative_u64_from_column(
365 row,
366 2,
367 "federated share imported_at",
368 )?,
369 exported_at:
370 crate::capability_lineage::non_negative_u64_from_column(
371 row,
372 3,
373 "federated share exported_at",
374 )?,
375 issuer: row.get::<_, String>(4)?,
376 partner: row.get::<_, String>(5)?,
377 signer_public_key: row.get::<_, String>(6)?,
378 require_proofs: row.get::<_, i64>(7)? != 0,
379 tool_receipts:
380 crate::capability_lineage::non_negative_u64_from_column(
381 row,
382 8,
383 "federated share receipt count",
384 )?,
385 capability_lineage:
386 crate::capability_lineage::non_negative_u64_from_column(
387 row,
388 9,
389 "federated share lineage count",
390 )?,
391 },
392 crate::capability_lineage::validate_snapshot_from_row(
393 CapabilitySnapshot {
394 capability_id: row.get::<_, String>(10)?,
395 subject_key: row.get::<_, String>(11)?,
396 issuer_key: row.get::<_, String>(12)?,
397 issued_at:
398 crate::capability_lineage::non_negative_u64_from_column(
399 row,
400 13,
401 "federated lineage issued_at",
402 )?,
403 expires_at:
404 crate::capability_lineage::non_negative_u64_from_column(
405 row,
406 14,
407 "federated lineage expires_at",
408 )?,
409 grants_json: row.get::<_, String>(15)?,
410 delegation_depth:
411 crate::capability_lineage::non_negative_u64_from_column(
412 row,
413 16,
414 "federated lineage delegation_depth",
415 )?,
416 parent_capability_id: row.get::<_, Option<String>>(17)?,
417 federated_parent_capability_id: row
418 .get::<_, Option<String>>(18)?,
419 provenance: crate::capability_lineage::provenance_from_row(row, 19)?,
420 signed_capability:
421 crate::capability_lineage::signed_capability_from_row(row, 20)?,
422 },
423 20,
424 false,
425 )?,
426 ))
427 },
428 )
429 .optional()?;
430 Ok(row)
431 }
432
433 pub fn list_federated_share_subject_corpora(
434 &self,
435 subject_key: &str,
436 since: Option<u64>,
437 until: Option<u64>,
438 ) -> Result<Vec<FederatedShareSubjectCorpus>, ReceiptStoreError> {
439 let since = since
440 .map(|value| sqlite_i64(value, "federated corpus since"))
441 .transpose()?;
442 let until = until
443 .map(|value| sqlite_i64(value, "federated corpus until"))
444 .transpose()?;
445 let mut share_ids = self
446 .connection()?
447 .prepare(
448 r#"
449 SELECT DISTINCT share_id
450 FROM federated_share_tool_receipts
451 WHERE subject_key = ?1
452 AND (?2 IS NULL OR timestamp >= ?2)
453 AND (?3 IS NULL OR timestamp <= ?3)
454 ORDER BY share_id
455 "#,
456 )?
457 .query_map(params![subject_key, since, until], |row| {
458 row.get::<_, String>(0)
459 })?
460 .collect::<Result<Vec<_>, _>>()?;
461
462 share_ids.sort();
463 let mut results = Vec::new();
464 for share_id in share_ids {
465 let summary = self
466 .connection()?
467 .query_row(
468 r#"
469 SELECT
470 share_id,
471 manifest_hash,
472 imported_at,
473 exported_at,
474 issuer,
475 partner,
476 signer_public_key,
477 require_proofs,
478 (SELECT COUNT(*) FROM federated_share_tool_receipts r WHERE r.share_id = s.share_id),
479 (SELECT COUNT(*) FROM federated_share_capability_lineage c WHERE c.share_id = s.share_id)
480 FROM federated_evidence_shares s
481 WHERE share_id = ?1
482 "#,
483 params![share_id],
484 |row| {
485 Ok(FederatedEvidenceShareSummary {
486 share_id: row.get::<_, String>(0)?,
487 manifest_hash: row.get::<_, String>(1)?,
488 imported_at:
489 crate::capability_lineage::non_negative_u64_from_column(
490 row,
491 2,
492 "federated share imported_at",
493 )?,
494 exported_at:
495 crate::capability_lineage::non_negative_u64_from_column(
496 row,
497 3,
498 "federated share exported_at",
499 )?,
500 issuer: row.get::<_, String>(4)?,
501 partner: row.get::<_, String>(5)?,
502 signer_public_key: row.get::<_, String>(6)?,
503 require_proofs: row.get::<_, i64>(7)? != 0,
504 tool_receipts:
505 crate::capability_lineage::non_negative_u64_from_column(
506 row,
507 8,
508 "federated share receipt count",
509 )?,
510 capability_lineage:
511 crate::capability_lineage::non_negative_u64_from_column(
512 row,
513 9,
514 "federated share lineage count",
515 )?,
516 })
517 },
518 )?;
519
520 let receipts = self
521 .connection()?
522 .prepare(
523 r#"
524 SELECT seq, raw_json
525 FROM federated_share_tool_receipts
526 WHERE share_id = ?1
527 AND subject_key = ?2
528 AND (?3 IS NULL OR timestamp >= ?3)
529 AND (?4 IS NULL OR timestamp <= ?4)
530 ORDER BY seq ASC
531 "#,
532 )?
533 .query_map(
534 params![summary.share_id, subject_key, since, until],
535 |row| {
536 let raw_json = row.get::<_, String>(1)?;
537 let seq = crate::capability_lineage::non_negative_u64_from_column(
538 row,
539 0,
540 "federated receipt seq",
541 )?;
542 Ok(StoredToolReceipt {
543 seq,
544 receipt: decode_verified_chio_receipt(
545 &raw_json,
546 "federated share tool receipt",
547 Some(seq),
548 )
549 .map_err(|error| {
550 rusqlite::Error::FromSqlConversionFailure(
551 raw_json.len(),
552 rusqlite::types::Type::Text,
553 Box::new(error),
554 )
555 })?,
556 })
557 },
558 )?
559 .collect::<Result<Vec<_>, _>>()?;
560
561 let capabilities = self
562 .connection()?
563 .prepare(
564 r#"
565 SELECT
566 capability_id,
567 subject_key,
568 issuer_key,
569 issued_at,
570 expires_at,
571 grants_json,
572 delegation_depth,
573 parent_capability_id,
574 federated_parent_capability_id,
575 provenance,
576 signed_capability_json
577 FROM federated_share_capability_lineage
578 WHERE share_id = ?1
579 AND (subject_key = ?2 OR issuer_key = ?2)
580 ORDER BY issued_at ASC, capability_id ASC
581 "#,
582 )?
583 .query_map(params![summary.share_id, subject_key], |row| {
584 crate::capability_lineage::validate_snapshot_from_row(
585 CapabilitySnapshot {
586 capability_id: row.get::<_, String>(0)?,
587 subject_key: row.get::<_, String>(1)?,
588 issuer_key: row.get::<_, String>(2)?,
589 issued_at: crate::capability_lineage::non_negative_u64_from_column(
590 row,
591 3,
592 "federated lineage issued_at",
593 )?,
594 expires_at: crate::capability_lineage::non_negative_u64_from_column(
595 row,
596 4,
597 "federated lineage expires_at",
598 )?,
599 grants_json: row.get::<_, String>(5)?,
600 delegation_depth:
601 crate::capability_lineage::non_negative_u64_from_column(
602 row,
603 6,
604 "federated lineage delegation_depth",
605 )?,
606 parent_capability_id: row.get::<_, Option<String>>(7)?,
607 federated_parent_capability_id: row.get::<_, Option<String>>(8)?,
608 provenance: crate::capability_lineage::provenance_from_row(row, 9)?,
609 signed_capability:
610 crate::capability_lineage::signed_capability_from_row(row, 10)?,
611 },
612 10,
613 false,
614 )
615 })?
616 .collect::<Result<Vec<_>, _>>()?;
617
618 results.push((summary, receipts, capabilities));
619 }
620
621 Ok(results)
622 }
623
624 pub fn record_federated_lineage_bridge(
625 &mut self,
626 local_capability_id: &str,
627 parent_capability_id: &str,
628 share_id: Option<&str>,
629 ) -> Result<(), ReceiptStoreError> {
630 let local_capability_id = local_capability_id.to_string();
631 let parent_capability_id = parent_capability_id.to_string();
632 let share_id = share_id.map(ToString::to_string);
633 self.writer_handle().run_write(move |connection| {
634 let transaction =
635 connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
636 persist_immutable_federated_lineage_bridge(
637 &transaction,
638 &local_capability_id,
639 &parent_capability_id,
640 share_id.as_deref(),
641 )?;
642 transaction.commit()?;
643 Ok(())
644 })
645 }
646
647 pub fn persist_federated_delegation_lineage(
649 &mut self,
650 anchor: &CapabilitySnapshot,
651 upstream_bridge: Option<(&str, &str)>,
652 child: &CapabilitySnapshot,
653 ) -> Result<(), ReceiptStoreError> {
654 crate::capability_lineage::validate_snapshot_for_transport(anchor)?;
655 crate::capability_lineage::validate_snapshot_for_transport(child)?;
656 if anchor.provenance != chio_kernel::CapabilitySnapshotProvenance::SyntheticAnchor {
657 return Err(ReceiptStoreError::Conflict(format!(
658 "federated delegation anchor {} is not synthetic",
659 anchor.capability_id
660 )));
661 }
662 if child.provenance != chio_kernel::CapabilitySnapshotProvenance::SignedToken {
663 return Err(ReceiptStoreError::Conflict(format!(
664 "federated delegation child {} is not signed",
665 child.capability_id
666 )));
667 }
668 if child.parent_capability_id.is_some() || child.delegation_depth != 0 {
669 return Err(ReceiptStoreError::Conflict(format!(
670 "federated delegation child {} must be a direct signed capability",
671 child.capability_id
672 )));
673 }
674 if anchor.federated_parent_capability_id.as_deref()
675 != upstream_bridge.map(|(parent, _)| parent)
676 {
677 return Err(ReceiptStoreError::Conflict(format!(
678 "federated delegation anchor {} does not match its upstream bridge",
679 anchor.capability_id
680 )));
681 }
682 if child.federated_parent_capability_id.as_deref() != Some(anchor.capability_id.as_str()) {
683 return Err(ReceiptStoreError::Conflict(format!(
684 "federated delegation child {} is not bridged to anchor {}",
685 child.capability_id, anchor.capability_id
686 )));
687 }
688
689 let anchor = anchor.clone();
690 let child = child.clone();
691 let upstream_bridge =
692 upstream_bridge.map(|(parent, share)| (parent.to_string(), share.to_string()));
693 self.writer_handle().run_write(move |connection| {
694 let transaction =
695 connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
696 crate::capability_lineage::persist_compatible_snapshot(&transaction, &anchor)?;
697 if let Some((parent, share)) = upstream_bridge.as_ref() {
698 persist_immutable_federated_lineage_bridge(
699 &transaction,
700 &anchor.capability_id,
701 parent,
702 Some(share),
703 )?;
704 }
705 crate::capability_lineage::persist_compatible_snapshot(&transaction, &child)?;
706 persist_immutable_federated_lineage_bridge(
707 &transaction,
708 &child.capability_id,
709 &anchor.capability_id,
710 None,
711 )?;
712 transaction.commit()?;
713 Ok(())
714 })
715 }
716
717 pub fn get_combined_lineage(
718 &self,
719 capability_id: &str,
720 ) -> Result<Option<chio_kernel::CapabilitySnapshot>, ReceiptStoreError> {
721 if let Some(snapshot) = self
722 .get_lineage(capability_id)
723 .map_err(|error| match error {
724 chio_kernel::CapabilityLineageError::ReceiptStore(error) => error,
725 chio_kernel::CapabilityLineageError::Sqlite(error) => {
726 ReceiptStoreError::Sqlite(error)
727 }
728 chio_kernel::CapabilityLineageError::Json(error) => ReceiptStoreError::Json(error),
729 })?
730 {
731 return Ok(Some(snapshot));
732 }
733 Ok(self
734 .get_federated_share_for_capability(capability_id)?
735 .map(|(_, snapshot)| snapshot))
736 }
737
738 pub fn get_combined_delegation_chain(
739 &self,
740 capability_id: &str,
741 ) -> Result<Vec<chio_kernel::CapabilitySnapshot>, ReceiptStoreError> {
742 const MAX_CHAIN_LENGTH: usize = 32;
743
744 let mut chain = Vec::new();
745 let mut current = Some(capability_id.to_string());
746 let mut seen = BTreeSet::new();
747
748 while let Some(current_capability_id) = current.take() {
749 if !seen.insert(current_capability_id.clone()) {
750 return Err(ReceiptStoreError::Conflict(format!(
751 "combined delegation chain for {capability_id} contains a cycle at {current_capability_id}"
752 )));
753 }
754 if chain.len() >= MAX_CHAIN_LENGTH {
755 return Err(ReceiptStoreError::Conflict(format!(
756 "combined delegation chain for {capability_id} exceeds {MAX_CHAIN_LENGTH} capabilities"
757 )));
758 }
759 let Some(snapshot) = self.get_combined_lineage(¤t_capability_id)? else {
760 if chain.is_empty() {
761 return Ok(Vec::new());
762 }
763 return Err(ReceiptStoreError::Conflict(format!(
764 "combined delegation chain for {capability_id} references missing parent {current_capability_id}"
765 )));
766 };
767 current = snapshot
768 .parent_capability_id
769 .clone()
770 .or_else(|| snapshot.federated_parent_capability_id.clone());
771 chain.push(snapshot);
772 }
773
774 chain.reverse();
775 Ok(chain)
776 }
777}