noosphere_core/data/
address.rs

1use crate::authority::{
2    collect_ucan_proofs, generate_capability, SphereAbility, SPHERE_SEMANTICS, SUPPORTED_KEYS,
3};
4use anyhow::Result;
5use cid::Cid;
6use libipld_cbor::DagCborCodec;
7use noosphere_storage::BlockStore;
8use serde::{de, ser, Deserialize, Serialize};
9use std::fmt::Debug;
10use std::{convert::TryFrom, fmt::Display, ops::Deref, str::FromStr};
11use ucan::{chain::ProofChain, crypto::did::DidParser, store::UcanJwtStore, Ucan};
12
13use super::{Did, IdentitiesIpld, Jwt, Link, MemoIpld};
14
15#[cfg(docs)]
16use crate::data::SphereIpld;
17
18/// The name of the fact (as defined for a [Ucan]) that contains the link for a
19/// [LinkRecord].
20pub const LINK_RECORD_FACT_NAME: &str = "link";
21
22/// A subdomain of a [SphereIpld] that pertains to the management and recording of
23/// the petnames associated with the sphere.
24#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Hash)]
25pub struct AddressBookIpld {
26    /// A pointer to the [IdentitiesIpld] associated with this address book
27    pub identities: Link<IdentitiesIpld>,
28}
29
30impl AddressBookIpld {
31    /// Initialize an empty [AddressBookIpld], with a valid [Cid] that refers to
32    /// an empty [IdentitiesIpld] in the provided storage
33    pub async fn empty<S: BlockStore>(store: &mut S) -> Result<Self> {
34        let identities_ipld = IdentitiesIpld::empty(store).await?;
35        let identities = store.save::<DagCborCodec, _>(identities_ipld).await?.into();
36
37        Ok(AddressBookIpld { identities })
38    }
39}
40
41/// An [IdentityIpld] represents an entry in a user's pet name address book.
42/// It is intended to be associated with a human readable name, and enables the
43/// user to resolve the name to a DID. Eventually the DID will be resolved by
44/// some mechanism to a UCAN, so this struct also records the last resolved
45/// value if one has ever been resolved.
46#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Hash)]
47pub struct IdentityIpld {
48    /// The [Did] of a peer
49    pub did: Did,
50    /// An optional pointer to a known [LinkRecord] for the peer
51    pub link_record: Option<Link<LinkRecord>>,
52}
53
54impl IdentityIpld {
55    /// If there is a [LinkRecord] for this [IdentityIpld], attempt to retrieve
56    /// it from storage
57    pub async fn link_record<S: UcanJwtStore>(&self, store: &S) -> Option<LinkRecord> {
58        match &self.link_record {
59            Some(cid) => match store.read_token(cid).await.unwrap_or(None) {
60                Some(jwt) => LinkRecord::from_str(&jwt).ok(),
61                None => None,
62            },
63            _ => None,
64        }
65    }
66}
67
68/// A [LinkRecord] is a wrapper around a decoded [Jwt] ([Ucan]),
69/// representing a link address as a [Cid] to a sphere.
70#[derive(Clone)]
71#[repr(transparent)]
72pub struct LinkRecord(Ucan);
73
74impl Debug for LinkRecord {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        f.debug_tuple("LinkRecord")
77            .field(
78                &self
79                    .0
80                    .to_cid(cid::multihash::Code::Blake3_256)
81                    .map_or_else(|_| String::from("<Invalid>"), |cid| cid.to_string()),
82            )
83            .finish()
84    }
85}
86
87impl LinkRecord {
88    /// Validates the [Ucan] token as a [LinkRecord], ensuring that
89    /// the sphere's owner authorized the publishing of a new
90    /// content address. Notably does not check the publishing timeframe
91    /// permissions, as an expired token can be considered valid.
92    /// Returns an `Err` if validation fails.
93    pub async fn validate<S: UcanJwtStore>(&self, store: &S) -> Result<()> {
94        let identity = self.to_sphere_identity();
95        let token = &self.0;
96
97        if self.get_link().is_none() {
98            return Err(anyhow::anyhow!("LinkRecord missing link."));
99        }
100
101        let mut did_parser = DidParser::new(SUPPORTED_KEYS);
102
103        // We're interested in the validity of the proof at the time
104        // of publishing.
105        let now_time = if let Some(nbf) = token.not_before() {
106            Some(nbf.to_owned())
107        } else {
108            token.expires_at().as_ref().map(|exp| exp - 1)
109        };
110
111        let proof =
112            ProofChain::from_ucan(token.to_owned(), now_time, &mut did_parser, store).await?;
113
114        {
115            let desired_capability = generate_capability(&identity, SphereAbility::Publish);
116            let mut has_capability = false;
117            for capability_info in proof.reduce_capabilities(&SPHERE_SEMANTICS) {
118                let capability = capability_info.capability;
119                if capability_info.originators.contains(identity.as_str())
120                    && capability.enables(&desired_capability)
121                {
122                    has_capability = true;
123                    break;
124                }
125            }
126            if !has_capability {
127                return Err(anyhow::anyhow!("LinkRecord is not authorized."));
128            }
129        }
130
131        token
132            .check_signature(&mut did_parser)
133            .await
134            .map(|_| ())
135            .map_err(|_| anyhow::anyhow!("LinkRecord has invalid signature."))
136    }
137
138    /// Returns true if the [Ucan] token is currently publishable
139    /// within the bounds of its expiry/not before time.
140    pub fn has_publishable_timeframe(&self) -> bool {
141        !self.0.is_expired(None) && !self.0.is_too_early()
142    }
143
144    /// The DID key of the sphere that this record maps.
145    pub fn to_sphere_identity(&self) -> Did {
146        Did::from(self.0.audience())
147    }
148
149    /// The sphere revision address ([Link<MemoIpld>]) that the sphere's identity maps to.
150    pub fn get_link(&self) -> Option<Link<MemoIpld>> {
151        let facts = if let Some(facts) = self.0.facts() {
152            facts
153        } else {
154            warn!("No facts found in the link record!");
155            return None;
156        };
157
158        for (name, value) in facts.iter() {
159            if name == LINK_RECORD_FACT_NAME {
160                return match value.as_str() {
161                    Some(link) => match Cid::try_from(link) {
162                        Ok(cid) => Some(cid.into()),
163                        Err(error) => {
164                            warn!("Could not parse '{}' as name record link: {}", link, error);
165                            None
166                        }
167                    },
168                    None => {
169                        warn!("Link record fact value must be a string.");
170                        None
171                    }
172                };
173            }
174        }
175        None
176    }
177
178    /// Returns a boolean indicating whether the `other` [LinkRecord]
179    /// is a newer record referring to the same identity.
180    /// Underlying [Ucan] expiry is used to compare. A record with
181    /// `null` expiry cannot supercede or be superceded.
182    pub fn superceded_by(&self, other: &LinkRecord) -> bool {
183        match (self.0.expires_at(), other.0.expires_at()) {
184            (Some(self_expiry), Some(other_expiry)) => {
185                other_expiry > self_expiry
186                    && self.to_sphere_identity() == other.to_sphere_identity()
187            }
188            (None, _) => false,
189            (_, None) => false,
190        }
191    }
192
193    /// Walk the underlying [Ucan] and collect all of the supporting proofs that
194    /// verify the link publisher's authority to publish the link
195    #[instrument(level = "trace", skip(self, store))]
196    pub async fn collect_proofs<S>(&self, store: &S) -> Result<Vec<Ucan>>
197    where
198        S: UcanJwtStore,
199    {
200        collect_ucan_proofs(&self.0, store).await
201    }
202}
203
204impl ser::Serialize for LinkRecord {
205    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
206    where
207        S: ser::Serializer,
208    {
209        let encoded = self.encode().map_err(ser::Error::custom)?;
210        serializer.serialize_str(&encoded)
211    }
212}
213
214impl<'de> de::Deserialize<'de> for LinkRecord {
215    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
216    where
217        D: de::Deserializer<'de>,
218    {
219        let s = String::deserialize(deserializer)?;
220        let record = LinkRecord::try_from(s).map_err(de::Error::custom)?;
221        Ok(record)
222    }
223}
224
225/// [LinkRecord]s compare their [Jwt] representations
226/// for equality. If a record cannot be encoded as such,
227/// they will not be considered equal to any other record.
228impl PartialEq for LinkRecord {
229    fn eq(&self, other: &Self) -> bool {
230        if let Ok(encoded_a) = self.encode() {
231            if let Ok(encoded_b) = other.encode() {
232                return encoded_a == encoded_b;
233            }
234        }
235        false
236    }
237}
238impl Eq for LinkRecord {}
239
240impl Deref for LinkRecord {
241    type Target = Ucan;
242
243    fn deref(&self) -> &Self::Target {
244        &self.0
245    }
246}
247
248impl Display for LinkRecord {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        write!(
251            f,
252            "LinkRecord({}, {})",
253            self.to_sphere_identity(),
254            self.get_link()
255                .map_or_else(|| String::from("None"), String::from)
256        )
257    }
258}
259
260impl TryFrom<&Jwt> for LinkRecord {
261    type Error = anyhow::Error;
262    fn try_from(value: &Jwt) -> Result<Self, Self::Error> {
263        LinkRecord::from_str(value)
264    }
265}
266
267impl TryFrom<&LinkRecord> for Jwt {
268    type Error = anyhow::Error;
269    fn try_from(value: &LinkRecord) -> Result<Self, Self::Error> {
270        Ok(Jwt(value.encode()?))
271    }
272}
273
274impl TryFrom<Jwt> for LinkRecord {
275    type Error = anyhow::Error;
276    fn try_from(value: Jwt) -> Result<Self, Self::Error> {
277        LinkRecord::try_from(&value)
278    }
279}
280
281impl TryFrom<LinkRecord> for Jwt {
282    type Error = anyhow::Error;
283    fn try_from(value: LinkRecord) -> Result<Self, Self::Error> {
284        Jwt::try_from(&value)
285    }
286}
287
288impl From<&Ucan> for LinkRecord {
289    fn from(value: &Ucan) -> Self {
290        LinkRecord::from(value.to_owned())
291    }
292}
293
294impl From<&LinkRecord> for Ucan {
295    fn from(value: &LinkRecord) -> Self {
296        value.0.clone()
297    }
298}
299
300impl From<Ucan> for LinkRecord {
301    fn from(value: Ucan) -> Self {
302        LinkRecord(value)
303    }
304}
305
306impl From<LinkRecord> for Ucan {
307    fn from(value: LinkRecord) -> Self {
308        value.0
309    }
310}
311
312impl TryFrom<&[u8]> for LinkRecord {
313    type Error = anyhow::Error;
314    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
315        LinkRecord::try_from(value.to_vec())
316    }
317}
318
319impl TryFrom<Vec<u8>> for LinkRecord {
320    type Error = anyhow::Error;
321    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
322        LinkRecord::from_str(&String::from_utf8(value)?)
323    }
324}
325
326impl TryFrom<LinkRecord> for Vec<u8> {
327    type Error = anyhow::Error;
328    fn try_from(value: LinkRecord) -> Result<Self, Self::Error> {
329        Ok(value.encode()?.into_bytes())
330    }
331}
332
333impl FromStr for LinkRecord {
334    type Err = anyhow::Error;
335    fn from_str(value: &str) -> Result<Self, Self::Err> {
336        Ok(Ucan::from_str(value)?.into())
337    }
338}
339
340impl TryFrom<String> for LinkRecord {
341    type Error = anyhow::Error;
342    fn try_from(value: String) -> Result<Self, Self::Error> {
343        Ok(Ucan::from_str(&value)?.into())
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350    use crate::{
351        authority::generate_ed25519_key,
352        data::Did,
353        tracing::initialize_tracing,
354        view::{Sphere, SPHERE_LIFETIME},
355    };
356    use noosphere_storage::{MemoryStorage, SphereDb, UcanStore};
357    use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore};
358
359    #[cfg(target_arch = "wasm32")]
360    use wasm_bindgen_test::wasm_bindgen_test;
361
362    #[cfg(target_arch = "wasm32")]
363    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
364
365    pub async fn from_issuer<K: KeyMaterial>(
366        issuer: &K,
367        sphere_id: &Did,
368        link: &Cid,
369        proofs: Option<&Vec<Ucan>>,
370    ) -> Result<LinkRecord, anyhow::Error> {
371        let capability = generate_capability(sphere_id, SphereAbility::Publish);
372
373        let mut builder = UcanBuilder::default()
374            .issued_by(issuer)
375            .for_audience(sphere_id)
376            .claiming_capability(&capability)
377            .with_fact(LINK_RECORD_FACT_NAME, link.to_string());
378
379        if let Some(proofs) = proofs {
380            let mut earliest_expiry: u64 = u64::MAX;
381            for token in proofs {
382                if let Some(exp) = token.expires_at() {
383                    earliest_expiry = *exp.min(&earliest_expiry);
384                    builder = builder.witnessed_by(token, None);
385                }
386            }
387            builder = builder.with_expiration(earliest_expiry);
388        } else {
389            builder = builder.with_lifetime(SPHERE_LIFETIME);
390        }
391
392        Ok(builder.build()?.sign().await?.into())
393    }
394
395    async fn expect_failure(message: &str, store: &SphereDb<MemoryStorage>, record: LinkRecord) {
396        assert!(record.validate(store).await.is_err(), "{}", message);
397    }
398
399    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
400    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
401    async fn test_self_signed_link_record() -> Result<()> {
402        let sphere_key = generate_ed25519_key();
403        let sphere_identity = Did::from(sphere_key.get_did().await?);
404        let link = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i";
405        let cid_link: Link<MemoIpld> = link.parse()?;
406        let store = SphereDb::new(&MemoryStorage::default()).await.unwrap();
407
408        let record = from_issuer(&sphere_key, &sphere_identity, &cid_link, None).await?;
409
410        assert_eq!(&record.to_sphere_identity(), &sphere_identity);
411        assert_eq!(LinkRecord::get_link(&record), Some(cid_link));
412        LinkRecord::validate(&record, &store).await?;
413        Ok(())
414    }
415
416    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
417    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
418    async fn test_delegated_link_record() -> Result<()> {
419        let owner_key = generate_ed25519_key();
420        let owner_identity = Did::from(owner_key.get_did().await?);
421        let sphere_key = generate_ed25519_key();
422        let sphere_identity = Did::from(sphere_key.get_did().await?);
423        let link = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i";
424        let cid_link: Cid = link.parse()?;
425        let mut store = SphereDb::new(&MemoryStorage::default()).await.unwrap();
426
427        // First verify that `owner` cannot publish for `sphere`
428        // without delegation.
429        let record = from_issuer(&owner_key, &sphere_identity, &cid_link, None).await?;
430
431        assert_eq!(record.to_sphere_identity(), sphere_identity);
432        assert_eq!(record.get_link(), Some(cid_link.into()));
433        if LinkRecord::validate(&record, &store).await.is_ok() {
434            panic!("Owner should not have authorization to publish record")
435        }
436
437        // Delegate `sphere_key`'s publishing authority to `owner_key`
438        let delegate_ucan = UcanBuilder::default()
439            .issued_by(&sphere_key)
440            .for_audience(&owner_identity)
441            .with_lifetime(SPHERE_LIFETIME)
442            .claiming_capability(&generate_capability(
443                &sphere_identity,
444                SphereAbility::Publish,
445            ))
446            .build()?
447            .sign()
448            .await?;
449        let _ = store.write_token(&delegate_ucan.encode()?).await?;
450
451        // Attempt `owner` publishing `sphere` with the proper authorization.
452        let proofs = vec![delegate_ucan.clone()];
453        let record = from_issuer(&owner_key, &sphere_identity, &cid_link, Some(&proofs)).await?;
454
455        assert_eq!(record.to_sphere_identity(), sphere_identity);
456        assert_eq!(record.get_link(), Some(cid_link.into()));
457        assert!(LinkRecord::has_publishable_timeframe(&record));
458        LinkRecord::validate(&record, &store).await?;
459
460        // Now test a similar record that has an expired capability.
461        // It must still be valid.
462        let expired: LinkRecord = UcanBuilder::default()
463            .issued_by(&owner_key)
464            .for_audience(&sphere_identity)
465            .claiming_capability(&generate_capability(
466                &sphere_identity,
467                SphereAbility::Publish,
468            ))
469            .with_fact(LINK_RECORD_FACT_NAME, cid_link.to_string())
470            .witnessed_by(&delegate_ucan, None)
471            .with_expiration(ucan::time::now() - 1234)
472            .build()?
473            .sign()
474            .await?
475            .into();
476        assert_eq!(expired.to_sphere_identity(), sphere_identity);
477        assert_eq!(expired.get_link(), Some(cid_link.into()));
478        assert!(!expired.has_publishable_timeframe());
479        LinkRecord::validate(&record, &store).await?;
480        Ok(())
481    }
482
483    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
484    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
485    async fn test_link_record_failures() -> Result<()> {
486        let sphere_key = generate_ed25519_key();
487        let sphere_identity = Did::from(sphere_key.get_did().await?);
488        let cid_address = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i";
489        let store = SphereDb::new(&MemoryStorage::default()).await.unwrap();
490
491        expect_failure(
492            "fails when expect `fact` is missing",
493            &store,
494            UcanBuilder::default()
495                .issued_by(&sphere_key)
496                .for_audience(&sphere_identity)
497                .with_lifetime(1000)
498                .claiming_capability(&generate_capability(
499                    sphere_identity.as_str(),
500                    SphereAbility::Publish,
501                ))
502                .with_fact("invalid-fact", cid_address.to_owned())
503                .build()?
504                .sign()
505                .await?
506                .into(),
507        )
508        .await;
509
510        let capability = generate_capability(
511            &Did(generate_ed25519_key().get_did().await?),
512            SphereAbility::Publish,
513        );
514        expect_failure(
515            "fails when capability resource does not match sphere identity",
516            &store,
517            UcanBuilder::default()
518                .issued_by(&sphere_key)
519                .for_audience(&sphere_identity)
520                .with_lifetime(1000)
521                .claiming_capability(&capability)
522                .with_fact(LINK_RECORD_FACT_NAME, cid_address.to_owned())
523                .build()?
524                .sign()
525                .await?
526                .into(),
527        )
528        .await;
529
530        let non_auth_key = generate_ed25519_key();
531        expect_failure(
532            "fails when a non-authorized key signs the record",
533            &store,
534            UcanBuilder::default()
535                .issued_by(&non_auth_key)
536                .for_audience(&sphere_identity)
537                .with_lifetime(1000)
538                .claiming_capability(&generate_capability(
539                    &sphere_identity,
540                    SphereAbility::Publish,
541                ))
542                .with_fact(LINK_RECORD_FACT_NAME, cid_address.to_owned())
543                .build()?
544                .sign()
545                .await?
546                .into(),
547        )
548        .await;
549
550        Ok(())
551    }
552
553    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
554    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
555    async fn test_link_record_convert() -> Result<()> {
556        let sphere_key = generate_ed25519_key();
557        let identity = Did::from(sphere_key.get_did().await?);
558        let capability = generate_capability(&identity, SphereAbility::Publish);
559        let cid_address = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i";
560        let link = Cid::from_str(cid_address)?;
561        let maybe_link = Some(link.into());
562
563        let ucan = UcanBuilder::default()
564            .issued_by(&sphere_key)
565            .for_audience(&identity)
566            .with_lifetime(1000)
567            .claiming_capability(&capability)
568            .with_fact(LINK_RECORD_FACT_NAME, cid_address.to_owned())
569            .build()?
570            .sign()
571            .await?;
572
573        let encoded = ucan.encode()?;
574        let base = LinkRecord::from(ucan.clone());
575
576        // from_str, String
577        {
578            let record: LinkRecord = encoded.parse()?;
579            assert_eq!(
580                record.to_sphere_identity(),
581                identity,
582                "LinkRecord::from_str()"
583            );
584            assert_eq!(record.get_link(), maybe_link, "LinkRecord::from_str()");
585            let record: LinkRecord = encoded.clone().try_into()?;
586            assert_eq!(
587                record.to_sphere_identity(),
588                identity,
589                "LinkRecord::try_from(String)"
590            );
591            assert_eq!(
592                record.get_link(),
593                maybe_link,
594                "LinkRecord::try_from(String)"
595            );
596        }
597
598        // Ucan convert
599        {
600            let from_ucan_ref = LinkRecord::from(&ucan);
601            assert_eq!(
602                base.to_sphere_identity(),
603                identity,
604                "LinkRecord::from(Ucan)"
605            );
606            assert_eq!(base.get_link(), maybe_link, "LinkRecord::from(Ucan)");
607            assert_eq!(
608                from_ucan_ref.to_sphere_identity(),
609                identity,
610                "LinkRecord::from(&Ucan)"
611            );
612            assert_eq!(
613                from_ucan_ref.get_link(),
614                maybe_link,
615                "LinkRecord::from(&Ucan)"
616            );
617            assert_eq!(
618                Ucan::from(base.clone()).encode()?,
619                encoded,
620                "Ucan::from(LinkRecord)"
621            );
622            assert_eq!(
623                Ucan::from(&base).encode()?,
624                encoded,
625                "Ucan::from(&LinkRecord)"
626            );
627        };
628
629        // Vec<u8> convert
630        {
631            let bytes = Vec::from(encoded.clone());
632            let record = LinkRecord::try_from(bytes.clone())?;
633            assert_eq!(
634                record.to_sphere_identity(),
635                identity,
636                "LinkRecord::try_from(Vec<u8>)"
637            );
638            assert_eq!(
639                record.get_link(),
640                maybe_link,
641                "LinkRecord::try_from(Vec<u8>)"
642            );
643
644            let record = LinkRecord::try_from(bytes.as_slice())?;
645            assert_eq!(
646                record.to_sphere_identity(),
647                identity,
648                "LinkRecord::try_from(&[u8])"
649            );
650            assert_eq!(record.get_link(), maybe_link, "LinkRecord::try_from(&[u8])");
651
652            let bytes_from_record: Vec<u8> = record.try_into()?;
653            assert_eq!(bytes_from_record, bytes, "LinkRecord::try_into(Vec<u8>>)");
654        };
655
656        // LinkRecord::serialize
657        // LinkRecord::deserialize
658        {
659            let serialized = serde_json::to_string(&base)?;
660            assert_eq!(serialized, format!("\"{}\"", encoded), "serialize()");
661            let record: LinkRecord = serde_json::from_str(&serialized)?;
662            assert_eq!(record.to_sphere_identity(), identity, "deserialize()");
663            assert_eq!(record.get_link(), maybe_link, "deserialize()");
664        }
665
666        Ok(())
667    }
668
669    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
670    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
671    async fn it_can_collect_related_proofs_from_storage() -> Result<()> {
672        initialize_tracing(None);
673        let owner_key = generate_ed25519_key();
674        let owner_did = owner_key.get_did().await?;
675
676        let delegatee_key = generate_ed25519_key();
677        let delegatee_did = delegatee_key.get_did().await?;
678
679        let mut db = SphereDb::new(&MemoryStorage::default()).await?;
680        let mut ucan_store = UcanStore(db.clone());
681
682        let (sphere, proof, _) = Sphere::generate(&owner_did, &mut db).await?;
683        let ucan = proof.as_ucan(&db).await?;
684
685        let sphere_identity = sphere.get_identity().await?;
686
687        let delegated_ucan = UcanBuilder::default()
688            .issued_by(&owner_key)
689            .for_audience(&delegatee_did)
690            .witnessed_by(&ucan, None)
691            .claiming_capability(&generate_capability(
692                &sphere_identity,
693                SphereAbility::Publish,
694            ))
695            .with_lifetime(120)
696            .build()?
697            .sign()
698            .await?;
699
700        let link_record_ucan = UcanBuilder::default()
701            .issued_by(&delegatee_key)
702            .for_audience(&sphere_identity)
703            .witnessed_by(&delegated_ucan, None)
704            .claiming_capability(&generate_capability(
705                &sphere_identity,
706                SphereAbility::Publish,
707            ))
708            .with_lifetime(120)
709            .with_fact(LINK_RECORD_FACT_NAME, sphere.cid().to_string())
710            .build()?
711            .sign()
712            .await?;
713
714        let link_record = LinkRecord::from(link_record_ucan.clone());
715
716        ucan_store.write_token(&ucan.encode()?).await?;
717        ucan_store.write_token(&delegated_ucan.encode()?).await?;
718        ucan_store.write_token(&link_record.encode()?).await?;
719
720        let proofs = link_record.collect_proofs(&ucan_store).await?;
721
722        assert_eq!(proofs.len(), 3);
723        assert_eq!(vec![link_record_ucan, delegated_ucan, ucan], proofs);
724
725        Ok(())
726    }
727
728    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
729    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
730    async fn test_superceded_by() -> Result<()> {
731        let sphere_key = generate_ed25519_key();
732        let identity = Did::from(sphere_key.get_did().await?);
733        let capability = generate_capability(&identity, SphereAbility::Publish);
734        let cid_address = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i";
735        let other_key = generate_ed25519_key();
736        let other_identity = Did::from(other_key.get_did().await?);
737
738        let earlier = LinkRecord::from(
739            UcanBuilder::default()
740                .issued_by(&sphere_key)
741                .for_audience(&identity)
742                .with_lifetime(1000)
743                .claiming_capability(&capability)
744                .with_fact(LINK_RECORD_FACT_NAME, cid_address.to_owned())
745                .build()?
746                .sign()
747                .await?,
748        );
749
750        let later = LinkRecord::from(
751            UcanBuilder::default()
752                .issued_by(&sphere_key)
753                .for_audience(&identity)
754                .with_lifetime(2000)
755                .claiming_capability(&capability)
756                .with_fact(LINK_RECORD_FACT_NAME, cid_address.to_owned())
757                .build()?
758                .sign()
759                .await?,
760        );
761
762        let no_expiry = LinkRecord::from(
763            UcanBuilder::default()
764                .issued_by(&sphere_key)
765                .for_audience(&identity)
766                .claiming_capability(&capability)
767                .with_fact(LINK_RECORD_FACT_NAME, cid_address.to_owned())
768                .build()?
769                .sign()
770                .await?,
771        );
772
773        let other_identity = LinkRecord::from(
774            UcanBuilder::default()
775                .issued_by(&sphere_key)
776                .for_audience(&other_identity)
777                .claiming_capability(&generate_capability(
778                    &other_identity,
779                    SphereAbility::Publish,
780                ))
781                .with_fact(LINK_RECORD_FACT_NAME, cid_address.to_owned())
782                .build()?
783                .sign()
784                .await?,
785        );
786
787        assert!(earlier.superceded_by(&later));
788        assert!(!later.superceded_by(&earlier));
789        assert!(!earlier.superceded_by(&no_expiry));
790        assert!(!earlier.superceded_by(&other_identity));
791        assert!(!no_expiry.superceded_by(&later));
792        assert!(!other_identity.superceded_by(&later));
793        Ok(())
794    }
795}