Skip to main content

chio_store_sqlite/receipt_store/bootstrap/
federated.rs

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    /// Persist a federated issuance lineage as one all-or-nothing operation.
648    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(&current_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}