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
18pub const LINK_RECORD_FACT_NAME: &str = "link";
21
22#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Hash)]
25pub struct AddressBookIpld {
26 pub identities: Link<IdentitiesIpld>,
28}
29
30impl AddressBookIpld {
31 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#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Hash)]
47pub struct IdentityIpld {
48 pub did: Did,
50 pub link_record: Option<Link<LinkRecord>>,
52}
53
54impl IdentityIpld {
55 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#[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 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 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 pub fn has_publishable_timeframe(&self) -> bool {
141 !self.0.is_expired(None) && !self.0.is_too_early()
142 }
143
144 pub fn to_sphere_identity(&self) -> Did {
146 Did::from(self.0.audience())
147 }
148
149 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 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 #[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
225impl 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 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 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 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 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 {
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 {
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 {
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 {
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}