ucan 0.4.0

Implement UCAN-based authorization with conciseness and ease!
Documentation
mod validate {
    use crate::{
        builder::UcanBuilder,
        capability::CapabilitySemantics,
        crypto::did::DidParser,
        tests::fixtures::{EmailSemantics, Identities, SUPPORTED_KEYS},
        time::now,
        ucan::Ucan,
    };
    use anyhow::Result;

    use serde_json::json;
    #[cfg(target_arch = "wasm32")]
    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};

    #[cfg(target_arch = "wasm32")]
    wasm_bindgen_test_configure!(run_in_browser);

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
    async fn it_round_trips_with_encode() {
        let identities = Identities::new().await;
        let mut did_parser = DidParser::new(SUPPORTED_KEYS);

        let ucan = UcanBuilder::default()
            .issued_by(&identities.alice_key)
            .for_audience(identities.bob_did.as_str())
            .with_lifetime(30)
            .build()
            .unwrap()
            .sign()
            .await
            .unwrap();

        let encoded_ucan = ucan.encode().unwrap();
        let decoded_ucan = Ucan::try_from(encoded_ucan.as_str()).unwrap();

        decoded_ucan.validate(None, &mut did_parser).await.unwrap();
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
    async fn it_identifies_a_ucan_that_is_not_active_yet() {
        let identities = Identities::new().await;

        let ucan = UcanBuilder::default()
            .issued_by(&identities.alice_key)
            .for_audience(identities.bob_did.as_str())
            .not_before(now() + 30)
            .with_lifetime(30)
            .build()
            .unwrap()
            .sign()
            .await
            .unwrap();

        assert!(ucan.is_too_early());
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
    async fn it_identifies_a_ucan_that_has_become_active() {
        let identities = Identities::new().await;
        let ucan = UcanBuilder::default()
            .issued_by(&identities.alice_key)
            .for_audience(identities.bob_did.as_str())
            .not_before(now() / 1000)
            .with_lifetime(30)
            .build()
            .unwrap()
            .sign()
            .await
            .unwrap();

        assert!(!ucan.is_too_early());
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
    async fn it_can_be_serialized_as_json() -> Result<()> {
        let identities = Identities::new().await;

        let email_semantics = EmailSemantics {};
        let send_email_as_alice = email_semantics
            .parse("mailto:alice@email.com".into(), "email/send".into(), None)
            .unwrap();

        let ucan = UcanBuilder::default()
            .issued_by(&identities.alice_key)
            .for_audience(identities.bob_did.as_str())
            .not_before(now() / 1000)
            .with_lifetime(30)
            .with_fact("abc/challenge", json!({ "foo": "bar" }))
            .claiming_capability(&send_email_as_alice)
            .build()?
            .sign()
            .await?;

        let ucan_json = serde_json::to_value(ucan.clone())?;

        assert_eq!(
            ucan_json,
            serde_json::json!({
                "header": {
                    "alg": "EdDSA",
                    "typ": "JWT"
                },
                "payload": {
                    "ucv": crate::ucan::UCAN_VERSION,
                    "iss": ucan.issuer(),
                    "aud": ucan.audience(),
                    "exp": ucan.expires_at(),
                    "nbf": ucan.not_before(),
                    "cap": {
                        "mailto:alice@email.com": {
                            "email/send": [{}]
                        }
                    },
                    "fct": {
                        "abc/challenge": { "foo": "bar" }
                    }
                },
                "signed_data": ucan.signed_data(),
                "signature": ucan.signature()
            })
        );
        Ok(())
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
    async fn it_can_be_serialized_as_json_without_optionals() -> Result<()> {
        let identities = Identities::new().await;
        let ucan = UcanBuilder::default()
            .issued_by(&identities.alice_key)
            .for_audience(identities.bob_did.as_str())
            .build()?
            .sign()
            .await?;

        let ucan_json = serde_json::to_value(ucan.clone())?;

        assert_eq!(
            ucan_json,
            serde_json::json!({
                "header": {
                    "alg": "EdDSA",
                    "typ": "JWT"
                },
                "payload": {
                    "ucv": crate::ucan::UCAN_VERSION,
                    "iss": ucan.issuer(),
                    "aud": ucan.audience(),
                    "exp": serde_json::Value::Null,
                    "cap": {}
                },
                "signed_data": ucan.signed_data(),
                "signature": ucan.signature()
            })
        );

        Ok(())
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
    async fn it_implements_partial_eq() {
        let identities = Identities::new().await;
        let ucan_a = UcanBuilder::default()
            .issued_by(&identities.alice_key)
            .for_audience(identities.bob_did.as_str())
            .with_expiration(10000000)
            .build()
            .unwrap()
            .sign()
            .await
            .unwrap();

        let ucan_b = UcanBuilder::default()
            .issued_by(&identities.alice_key)
            .for_audience(identities.bob_did.as_str())
            .with_expiration(10000000)
            .build()
            .unwrap()
            .sign()
            .await
            .unwrap();

        let ucan_c = UcanBuilder::default()
            .issued_by(&identities.alice_key)
            .for_audience(identities.bob_did.as_str())
            .with_expiration(20000000)
            .build()
            .unwrap()
            .sign()
            .await
            .unwrap();

        assert!(ucan_a == ucan_b);
        assert!(ucan_a != ucan_c);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
    async fn test_lifetime_ends_after() -> Result<()> {
        let identities = Identities::new().await;
        let forever_ucan = UcanBuilder::default()
            .issued_by(&identities.alice_key)
            .for_audience(identities.bob_did.as_str())
            .build()?
            .sign()
            .await?;
        let early_ucan = UcanBuilder::default()
            .issued_by(&identities.alice_key)
            .for_audience(identities.bob_did.as_str())
            .with_lifetime(2000)
            .build()?
            .sign()
            .await?;
        let later_ucan = UcanBuilder::default()
            .issued_by(&identities.alice_key)
            .for_audience(identities.bob_did.as_str())
            .with_lifetime(4000)
            .build()?
            .sign()
            .await?;

        assert_eq!(*forever_ucan.expires_at(), None);
        assert!(forever_ucan.lifetime_ends_after(&early_ucan));
        assert!(!early_ucan.lifetime_ends_after(&forever_ucan));
        assert!(later_ucan.lifetime_ends_after(&early_ucan));

        Ok(())
    }
}