Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle src identity doc.rs
pub mod update;

use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::num::{NonZeroU32, NonZeroUsize};
use std::ops::{Deref, Not};
use std::path::Path;
use std::str::FromStr;
use std::sync::LazyLock;

use crate::git::Oid;
use nonempty::NonEmpty;
use radicle_cob::type_name::{TypeName, TypeNameParse};
use serde::{Deserialize, Serialize, de};
use thiserror::Error;

use crate::canonical::formatter::CanonicalFormatter;
use crate::cob::identity;
use crate::crypto;
use crate::crypto::Signature;
use crate::git;
use crate::git::canonical::rules;
use crate::git::raw::ErrorExt as _;
use crate::identity::{Did, project::Project};
use crate::node::device::Device;
use crate::storage;
use crate::storage::{ReadRepository, RepositoryError};

pub use crypto::PublicKey;
pub use radicle_core::repo::*;

use super::CanonicalRefs;
use super::crefs::RawCanonicalRefs;

/// Path to the identity document in the identity branch.
pub static PATH: LazyLock<&Path> = LazyLock::new(|| Path::new("radicle.json"));
/// Maximum length of a string in the identity document.
pub const MAX_STRING_LENGTH: usize = 255;
/// Maximum number of a delegates in the identity document.
pub const MAX_DELEGATES: usize = 255;
/// The current, most recent version of the identity document.
// SAFETY: identity version should never be 0, so we can use `unsafe` here
pub const IDENTITY_VERSION: Version = Version(NonZeroU32::new(1).unwrap());

#[derive(Error, Debug)]
pub enum DocError {
    #[error("json: {0}")]
    Json(#[from] serde_json::Error),
    #[error(transparent)]
    Delegates(#[from] DelegatesError),
    #[error(transparent)]
    Threshold(#[from] ThresholdError),
    #[error("git: {0}")]
    Git(#[from] git::raw::Error),
    #[error("missing identity document")]
    Missing,
}

#[derive(Debug, Error)]
#[error("invalid delegates: {0}")]
pub struct DelegatesError(&'static str);

#[derive(Debug, Error)]
#[error("invalid threshold `{0}`: {1}")]
pub struct ThresholdError(usize, &'static str);

impl DocError {
    /// Whether this error is caused by the document not being found.
    pub fn is_not_found(&self) -> bool {
        match self {
            Self::Git(e) => e.is_not_found(),
            _ => false,
        }
    }
}

#[derive(Debug, Error)]
pub enum DefaultBranchRuleError {
    #[error("could not load `xyz.radicle.project` to get default branch name: {0}")]
    Payload(#[from] PayloadError),
}

/// The version number of the identity document.
///
/// It is used to ensure compatibility when parsing identity documents.
///
/// If an invalid version is found – either the `0` version, or an unrecognized
/// future version – the parsing of a version will fail.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Version(NonZeroU32);

impl Version {
    /// Construct a [`Version`].
    ///
    /// # Errors
    ///
    ///   - `n` is 0
    ///   - `n` is greater than the latest version, specified by
    ///     [`IDENTITY_VERSION`].
    pub fn new(n: u32) -> Result<Version, VersionError> {
        match NonZeroU32::new(n) {
            None => Err(VersionError::ZeroVersion),
            Some(n) if n > IDENTITY_VERSION.into() => Err(VersionError::UnknownVersion(n)),
            Some(n) => Ok(Version(n)),
        }
    }

    /// Return the underlying [`NonZeroU32`] number of the `Version`.
    pub fn number(&self) -> NonZeroU32 {
        self.0
    }

    /// Check if the provided version is part of the set of accepted versions.
    pub fn is_valid_version(v: &u32) -> bool {
        0 < *v && *v <= IDENTITY_VERSION.into()
    }

    /// Helper for skipping the serialization of the version if `version <= 1`.
    ///
    /// Note that we shouldn't allow `version: 0`, but there is no harm in
    /// skipping it anyway.
    fn skip_serializing(&self) -> bool {
        u32::from(*self) <= 1
    }
}

impl From<Version> for NonZeroU32 {
    fn from(Version(n): Version) -> Self {
        n
    }
}

impl From<Version> for u32 {
    fn from(Version(n): Version) -> Self {
        n.into()
    }
}

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum VersionError {
    #[error("the version 0 is not supported")]
    ZeroVersion,
    #[error("unknown identity document version {0}, only version {IDENTITY_VERSION} is supported")]
    UnknownVersion(NonZeroU32),
}

impl VersionError {
    /// Provide a verbose error.
    ///
    /// This will give a user more information on how to upgrade to a newer
    /// version of an identity document, if there is one.
    pub fn verbose(&self) -> String {
        const UNKNOWN_VERSION_ERROR: &str = r#"
Perhaps a new version of the identity document is released which is not supported by the current client.
See https://radicle.dev for the latest versions of Radicle.
The CLI command `rad id migrate` will help to migrate to an up-to-date versions."#;

        match self {
            err @ Self::ZeroVersion => err.to_string(),
            err @ Self::UnknownVersion(_) => format!("{err}{UNKNOWN_VERSION_ERROR}"),
        }
    }
}

impl TryFrom<u32> for Version {
    type Error = VersionError;

    fn try_from(n: u32) -> Result<Self, Self::Error> {
        Version::new(n)
    }
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl<'de> Deserialize<'de> for Version {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        u32::deserialize(deserializer)
            .and_then(|v| Version::new(v).map_err(|e| de::Error::custom(e.to_string())))
    }
}

/// Used for [`Deserialize`] of a [`Version`] in [`RawDoc`], so that
/// deserializing a missing version results in `Version(1)`.
fn missing_version() -> Version {
    // N.B. the default version is `1` which is non-zero so unsafe is fine here
    unsafe { Version(NonZeroU32::new_unchecked(1)) }
}

/// Identifies an identity document payload type.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PayloadId(TypeName);

impl fmt::Display for PayloadId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

impl FromStr for PayloadId {
    type Err = TypeNameParse;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        TypeName::from_str(s).map(Self)
    }
}

impl PayloadId {
    /// Project payload type.
    pub fn project() -> Self {
        Self(
            // SAFETY: We know this is valid.
            TypeName::from_str("xyz.radicle.project")
                .expect("PayloadId::project: type name is valid"),
        )
    }

    pub fn canonical_refs() -> Self {
        Self(
            // SAFETY: We know this is valid.
            TypeName::from_str("xyz.radicle.crefs")
                .expect("PayloadId::canonical_refs: type name is valid"),
        )
    }
}

#[derive(Debug, Error)]
pub enum PayloadError {
    #[error("json: {0}")]
    Json(#[from] serde_json::Error),
    #[error("payload '{0}' not found in identity document")]
    NotFound(PayloadId),
}

/// A `Payload` is a free-form JSON value that can be associated with an
/// identity's [`Doc`].
/// The payload is identified in the [`Doc`] by its corresponding [`PayloadId`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Payload {
    value: serde_json::Value,
}

impl Payload {
    /// Get a mutable reference to the JSON map, or `None` if the payload is not a map.
    pub fn as_object_mut(
        &mut self,
    ) -> Option<&mut serde_json::value::Map<String, serde_json::Value>> {
        self.value.as_object_mut()
    }

    pub fn into_inner(self) -> serde_json::Value {
        self.value
    }
}

impl From<serde_json::Value> for Payload {
    fn from(value: serde_json::Value) -> Self {
        Self { value }
    }
}

impl Deref for Payload {
    type Target = serde_json::Value;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

/// Trait for all types that may carry payloads.
pub trait GetPayload {
    fn get_payload(&self, id: &PayloadId) -> Option<&Payload>;
}

impl GetPayload for Doc {
    fn get_payload(&self, id: &PayloadId) -> Option<&Payload> {
        self.payload.get(id)
    }
}

impl GetPayload for RawDoc {
    fn get_payload(&self, id: &PayloadId) -> Option<&Payload> {
        self.payload.get(id)
    }
}

/// A verified identity document at a specific commit.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocAt {
    /// The commit at which this document exists.
    pub commit: Oid,
    /// The document blob at this commit.
    pub blob: Oid,
    /// The parsed document.
    pub doc: Doc,
}

impl Deref for DocAt {
    type Target = Doc;

    fn deref(&self) -> &Self::Target {
        &self.doc
    }
}

impl From<DocAt> for Doc {
    fn from(value: DocAt) -> Self {
        value.doc
    }
}

impl AsRef<Doc> for DocAt {
    fn as_ref(&self) -> &Doc {
        &self.doc
    }
}

/// Repository visibility.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum Visibility {
    /// Anyone and everyone.
    #[default]
    Public,
    /// Delegates plus the allowed DIDs.
    Private {
        #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
        allow: BTreeSet<Did>,
    },
}

#[derive(Error, Debug)]
#[error("'{0}' is not a valid visibility type")]
pub struct VisibilityParseError(String);

impl FromStr for Visibility {
    type Err = VisibilityParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "public" => Ok(Visibility::Public),
            "private" => Ok(Visibility::private([])),
            _ => Err(VisibilityParseError(s.to_owned())),
        }
    }
}

impl Visibility {
    /// Check whether the visibility is public.
    pub fn is_public(&self) -> bool {
        matches!(self, Self::Public)
    }

    /// Check whether the visibility is private.
    pub fn is_private(&self) -> bool {
        matches!(self, Self::Private { .. })
    }

    /// Private visibility with list of allowed DIDs beyond the repository delegates.
    pub fn private(allow: impl IntoIterator<Item = Did>) -> Self {
        Self::Private {
            allow: BTreeSet::from_iter(allow),
        }
    }
}

/// `RawDoc` is similar to the [`Doc`] type, however, it can be edited and may
/// not be valid.
///
/// It is expected that any changes to a [`Doc`] are made via [`RawDoc`], and
/// then verified by using [`RawDoc::verified`].
///
/// Note that `RawDoc` only implements [`Deserialize`]. This prevents us from
/// serializing an unverified document, while also making sure that any document
/// that is deserialized is verified.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawDoc {
    /// Version of the identity document.
    #[serde(default = "missing_version")]
    version: Version,
    /// The payload section.
    pub payload: BTreeMap<PayloadId, Payload>,
    /// The delegates section.
    pub delegates: Vec<Did>,
    /// The signature threshold.
    pub threshold: usize,
    /// Repository visibility.
    #[serde(default)]
    pub visibility: Visibility,
}

impl TryFrom<RawDoc> for Doc {
    type Error = DocError;

    fn try_from(doc: RawDoc) -> Result<Self, Self::Error> {
        doc.verified()
    }
}

impl RawDoc {
    /// Construct a new [`RawDoc`] with an initial [`RawDoc::payload`]
    /// containing the provided [`Project`], and the given `delegates`,
    /// `threshold`, and `visibility`.
    pub fn new(
        project: Project,
        delegates: Vec<Did>,
        threshold: usize,
        visibility: Visibility,
    ) -> Self {
        let project =
            serde_json::to_value(project).expect("Doc::initial: payload must be serializable");

        Self {
            version: IDENTITY_VERSION,
            payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
            delegates,
            threshold,
            visibility,
        }
    }

    /// Get the version of the document.
    pub fn version(&self) -> &Version {
        &self.version
    }

    /// Get the project payload, if it exists and is valid, out of this document.
    pub fn project(&self) -> Result<Project, PayloadError> {
        let value = self
            .payload
            .get(&PayloadId::project())
            .ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?;
        let proj: Project = serde_json::from_value((**value).clone())?;

        Ok(proj)
    }

    /// Check if the given `did` is in the set of [`RawDoc::delegates`].
    pub fn is_delegate(&self, did: &Did) -> bool {
        self.delegates.contains(did)
    }

    /// Add a new delegate to the document.
    ///
    /// Note that if this `Did` is a duplicate, then the resulting set will only
    /// show it once.
    pub fn delegate(&mut self, did: Did) {
        self.delegates.push(did)
    }

    /// Remove the `did` from the set of delegates. Returns `true` if it was
    /// removed.
    pub fn rescind(&mut self, did: &Did) -> Result<bool, DocError> {
        let (matches, delegates) = self.delegates.iter().partition(|d| *d == did);
        self.delegates = delegates;
        Ok(matches.is_empty().not())
    }

    /// Construct the `RawDoc` from the set of `bytes` that are expected to be
    /// in JSON format.
    pub fn from_json(bytes: &[u8]) -> Result<Self, DocError> {
        serde_json::from_slice(bytes).map_err(DocError::from)
    }

    /// Verify the `RawDoc`'s values, converting it into a valid [`Doc`].
    ///
    /// The verifications are as follows:
    ///
    ///  - [`RawDoc::delegates`]: any duplicates are removed, and for the
    ///    remaining set ensure that it is non-empty and does not exceed a
    ///    length of [`MAX_DELEGATES`].
    ///  - [`RawDoc::threshold`]: ensure that it is in the range `[1, delegates.len()]`.
    pub fn verified(self) -> Result<Doc, DocError> {
        let RawDoc {
            version,
            payload,
            delegates,
            threshold,
            visibility,
        } = self;
        let delegates = Delegates::new(delegates)?;
        let threshold = Threshold::new(threshold, &delegates)?;
        Ok(Doc {
            version,
            payload,
            delegates,
            threshold,
            visibility,
        })
    }
}

/// A valid set of delegates for the identity [`Doc`].
///
/// It can only be constructed via [`Delegates::new`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "Vec<Did>")]
pub struct Delegates(NonEmpty<Did>);

impl AsRef<NonEmpty<Did>> for Delegates {
    fn as_ref(&self) -> &NonEmpty<Did> {
        &self.0
    }
}

impl From<Did> for Delegates {
    fn from(did: Did) -> Self {
        Self(NonEmpty::new(did))
    }
}

impl TryFrom<Vec<Did>> for Delegates {
    type Error = DelegatesError;

    fn try_from(dids: Vec<Did>) -> Result<Self, Self::Error> {
        Delegates::new(dids)
    }
}

impl IntoIterator for Delegates {
    type Item = <NonEmpty<Did> as IntoIterator>::Item;
    type IntoIter = <NonEmpty<Did> as IntoIterator>::IntoIter;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl Delegates {
    /// Construct the set of `Delegates` by removing any duplicate [`Did`]s,
    /// ensure that the set is non-empty, and check the length does not exceed
    /// the [`MAX_DELEGATES`].
    pub fn new(delegates: impl IntoIterator<Item = Did>) -> Result<Self, DelegatesError> {
        let delegates = delegates
            .into_iter()
            .try_fold(Vec::<Did>::new(), |mut dids, did| {
                if !dids.contains(&did) {
                    if dids.len() >= MAX_DELEGATES {
                        return Err(DelegatesError("number of delegates cannot exceed 255"));
                    }
                    dids.push(did);
                }
                Ok(dids)
            })?;
        NonEmpty::from_vec(delegates)
            .map(Self)
            .ok_or(DelegatesError("delegate list cannot be empty"))
    }

    /// Get the first delegate in the set.
    pub fn first(&self) -> &Did {
        self.0.first()
    }

    /// Obtain an iterator over the [`Did`]s.
    pub fn iter(&self) -> impl Iterator<Item = &Did> {
        self.0.iter()
    }

    /// Check if the set contains the given `did`.
    pub fn contains(&self, did: &Did) -> bool {
        self.0.contains(did)
    }

    /// Check if the `did` is the only delegate of the repository.
    pub fn is_only(&self, did: &Did) -> bool {
        self.0.tail.is_empty() && &self.0.head == did
    }

    /// Get the number of delegates in the set.
    pub fn len(&self) -> usize {
        self.0.len()
    }

    /// Check if the set is empty. Note that this always returns `false`.
    pub fn is_empty(&self) -> bool {
        false
    }
}

impl<'a> From<&'a Delegates> for &'a NonEmpty<Did> {
    fn from(ds: &'a Delegates) -> Self {
        &ds.0
    }
}

impl From<Delegates> for NonEmpty<Did> {
    fn from(ds: Delegates) -> Self {
        ds.0
    }
}

impl From<Delegates> for Vec<Did> {
    fn from(Delegates(ds): Delegates) -> Self {
        ds.into()
    }
}

/// A valid threshold for the identity [`Doc`].
///
/// It can only be constructed via [`Threshold::new`] or [`Threshold::MIN`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(transparent)]
pub struct Threshold(NonZeroUsize);

impl From<Threshold> for usize {
    fn from(Threshold(t): Threshold) -> Self {
        t.get()
    }
}

impl AsRef<NonZeroUsize> for Threshold {
    fn as_ref(&self) -> &NonZeroUsize {
        &self.0
    }
}

impl fmt::Display for Threshold {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl Threshold {
    /// A threshold of `1`.
    pub const MIN: Threshold = Threshold(NonZeroUsize::MIN);

    /// Construct the `Threshold` by checking that `t` is not greater than
    /// [`MAX_DELEGATES`], that it does not exceed the number of delegates, and
    /// is non-zero.
    pub fn new(t: usize, delegates: &Delegates) -> Result<Self, ThresholdError> {
        if t > MAX_DELEGATES {
            Err(ThresholdError(t, "threshold cannot exceed 255"))
        } else if t > delegates.len() {
            Err(ThresholdError(
                t,
                "threshold cannot exceed number of delegates",
            ))
        } else {
            NonZeroUsize::new(t)
                .map(Self)
                .ok_or(ThresholdError(t, "threshold cannot be zero"))
        }
    }
}

/// `Doc` is a valid identity document.
///
/// To ensure that only valid documents are used, this type is restricted to be
/// read-only. For mutating the document use [`Doc::edit`].
///
/// A valid `Doc` can be constructed in four ways:
///
///   1. [`Doc::initial`]: a safe way to construct the initial document for an identity.
///   2. [`RawDoc::verified`]: validates a [`RawDoc`]'s fields and converts it
///      into a `Doc`
///   3. [`Deserialize`]: will deserialize a `Doc` by first deserializing a
///      [`RawDoc`] and use [`RawDoc::verified`] to construct the `Doc`.
///   4. [`Doc::from_blob`]: construct a `Doc` from a Git blob by deserializing
///      its contents.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(try_from = "RawDoc")]
pub struct Doc {
    #[serde(skip_serializing_if = "Version::skip_serializing")]
    version: Version,
    payload: BTreeMap<PayloadId, Payload>,
    delegates: Delegates,
    threshold: Threshold,
    #[serde(default, skip_serializing_if = "Visibility::is_public")]
    visibility: Visibility,
}

impl Doc {
    /// Construct the initial [`Doc`] for an identity.
    ///
    /// It will begin with the provided `project` in the [`Doc::payload`], the
    /// `delegate` as the sole delegate, a threshold of 1, and the given
    /// `visibility`.
    pub fn initial(project: Project, delegate: Did, visibility: Visibility) -> Self {
        let project =
            serde_json::to_value(project).expect("Doc::initial: payload must be serializable");

        Self {
            version: IDENTITY_VERSION,
            payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
            delegates: Delegates(NonEmpty::new(delegate)),
            threshold: Threshold(NonZeroUsize::MIN),
            visibility,
        }
    }

    /// Construct a [`Doc`] contained in the provided Git blob.
    pub fn from_blob(blob: &git::raw::Blob) -> Result<Self, DocError> {
        RawDoc::from_json(blob.content())?.verified()
    }

    /// Convert the [`Doc`] into a [`RawDoc`] for changing the field values and
    /// re-verifying.
    pub fn edit(self) -> RawDoc {
        let Doc {
            version,
            payload,
            delegates,
            threshold,
            visibility,
        } = self;
        RawDoc {
            version,
            payload,
            delegates: delegates.into(),
            threshold: threshold.into(),
            visibility,
        }
    }

    /// Using the current state of the `Doc`, perform any edits on the `RawDoc`
    /// form and verify the changes.
    pub fn with_edits<F>(self, f: F) -> Result<Self, DocError>
    where
        F: FnOnce(&mut RawDoc),
    {
        let mut raw = self.edit();
        f(&mut raw);
        raw.verified()
    }

    /// Get the version of the document.
    pub fn version(&self) -> &Version {
        &self.version
    }

    /// Return the associated payloads for this [`Doc`].
    pub fn payload(&self) -> &BTreeMap<PayloadId, Payload> {
        &self.payload
    }

    /// Get the project payload, if it exists and is valid, out of this document.
    pub fn project(&self) -> Result<Project, PayloadError> {
        let value = self
            .payload
            .get(&PayloadId::project())
            .ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?;
        let proj: Project = serde_json::from_value((**value).clone())?;

        Ok(proj)
    }

    /// Gets the qualified reference name of the default branch,
    /// according to the project payload in this document.
    pub fn default_branch(&self) -> Result<git::fmt::Qualified<'_>, PayloadError> {
        Ok(git::refs::branch(self.project()?.default_branch()))
    }

    pub fn default_branch_rule(&self) -> Result<rules::Rules, DefaultBranchRuleError> {
        let pattern = git::fmt::refspec::QualifiedPattern::from(git::refs::branch(
            self.project()?.default_branch(),
        ));
        let rule = rules::Rule::new(
            rules::ResolvedDelegates::Delegates(self.delegates.clone()),
            self.threshold,
        );
        Ok(rules::Rules::from_raw(
            rules::RawRules::from_iter([(pattern, rule.into())]),
            &mut || self.delegates.clone(),
        )
        .expect("default rules are valid"))
    }

    /// Construct the canonical references for this document.
    /// The implementation of [`RawCanonicalRefs`] is used to
    /// obtain the payload identified by [`PayloadId::canonical_refs`], if it
    /// exists.
    /// The resulting [`CanonicalRefs`] are constructed by extension with
    /// [`Self::default_branch_rule`].
    ///
    /// [`RawCanonicalRefs`]: super::crefs::RawCanonicalRefs
    pub fn canonical_refs(&self) -> Result<CanonicalRefs, CanonicalRefsError> {
        let raw_crefs = self.raw_canonical_refs()?.unwrap_or_default();

        let mut raw_rules = raw_crefs.raw_rules().clone();
        raw_rules.extend(rules::RawRules::from(self.default_branch_rule()?));

        let raw_crefs = RawCanonicalRefs::new(raw_rules);
        Ok(raw_crefs.try_into_canonical_refs(&mut || self.delegates.clone())?)
    }

    /// Return the associated [`Visibility`] of this document.
    pub fn visibility(&self) -> &Visibility {
        &self.visibility
    }

    /// Check whether the visibility of the document is public.
    pub fn is_public(&self) -> bool {
        self.visibility.is_public()
    }

    /// Check whether the visibility of the document is private.
    pub fn is_private(&self) -> bool {
        self.visibility.is_private()
    }

    /// Return the associated threshold of this document.
    pub fn threshold(&self) -> usize {
        self.threshold.into()
    }

    /// Return the associated threshold of this document in its non-zero format.
    pub fn threshold_nonzero(&self) -> &NonZeroUsize {
        &self.threshold.0
    }

    /// Return the associated delegates of this document.
    pub fn delegates(&self) -> &Delegates {
        &self.delegates
    }

    /// Check if the `did` is part of the [`Doc::delegates`] set.
    pub fn is_delegate(&self, did: &Did) -> bool {
        self.delegates.contains(did)
    }

    /// Check whether this document and the associated repository is visible to
    /// the given peer.
    pub fn is_visible_to(&self, did: &Did) -> bool {
        match &self.visibility {
            Visibility::Public => true,
            Visibility::Private { allow } => allow.contains(did) || self.is_delegate(did),
        }
    }

    /// Validate `signature` using this document's delegates, against a given
    /// document blob.
    pub fn verify_signature(
        &self,
        key: &PublicKey,
        signature: &Signature,
        blob: Oid,
    ) -> Result<(), PublicKey> {
        if !self.is_delegate(&key.into()) {
            return Err(*key);
        }
        if key.verify(AsRef::<[u8]>::as_ref(&blob), signature).is_err() {
            return Err(*key);
        }
        Ok(())
    }

    /// Check the provided `votes` passes the [`Doc::majority`].
    pub fn is_majority(&self, votes: usize) -> bool {
        votes >= self.majority()
    }

    /// Return the majority number based on the size of the delegates set.
    pub fn majority(&self) -> usize {
        self.delegates.len() / 2 + 1
    }

    /// Helper for getting an `embeds` Git blob.
    pub(crate) fn blob_at<R: ReadRepository>(
        commit: Oid,
        repo: &R,
    ) -> Result<git::raw::Blob<'_>, DocError> {
        let path = Path::new("embeds").join(*PATH);
        repo.blob_at(commit, path.as_path()).map_err(DocError::from)
    }

    /// Encode the [`Doc`] as canonical JSON, returning the set of bytes and its
    /// corresponding Git [`Oid`].
    pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), DocError> {
        let mut buf = Vec::new();
        let mut serializer =
            serde_json::Serializer::with_formatter(&mut buf, CanonicalFormatter::new());

        self.serialize(&mut serializer)?;
        let oid = git::raw::Oid::hash_object(git::raw::ObjectType::Blob, &buf)?;

        Ok((oid.into(), buf))
    }

    /// [`Doc::encode`] and sign the [`Doc`], returning the set of bytes, its
    /// corresponding Git [`Oid`] and the [`Signature`] over the [`Oid`].
    pub fn sign<G>(&self, signer: &G) -> Result<(git::Oid, Vec<u8>, Signature), DocError>
    where
        G: crypto::signature::Signer<crypto::Signature>,
    {
        let (oid, bytes) = self.encode()?;
        let sig = signer.sign(oid.as_ref());

        Ok((oid, bytes, sig))
    }

    /// Similar to [`Doc::sign`], but only returning the [`Signature`].
    pub fn signature_of<G>(&self, signer: &G) -> Result<Signature, DocError>
    where
        G: crypto::signature::Signer<crypto::Signature>,
    {
        let (_, _, sig) = self.sign(signer)?;

        Ok(sig)
    }

    /// Load the [`DocAt`] found at the given `commit`. The [`DocAt`] will
    /// contain the corresponding [`Doc`].
    pub fn load_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<DocAt, DocError> {
        let blob = Self::blob_at(commit, repo)?;
        let doc = Self::from_blob(&blob)?;

        Ok(DocAt {
            commit,
            doc,
            blob: blob.id().into(),
        })
    }

    /// Initialize an [`identity::Identity`] with this [`Doc`] as the associated
    /// document.
    pub fn init<G>(
        &self,
        repo: &storage::git::Repository,
        signer: &Device<G>,
    ) -> Result<git::Oid, RepositoryError>
    where
        G: crypto::signature::Signer<crypto::Signature>,
    {
        let cob = identity::Identity::initialize(self, repo, signer)?;
        let id_ref = git::refs::storage::id(signer.public_key());
        let cob_ref = git::refs::storage::cob(
            signer.public_key(),
            &crate::cob::identity::TYPENAME,
            &cob.id,
        );
        // Set `.../refs/rad/id` -> `.../refs/cobs/xyz.radicle.id/<id>`
        repo.backend.reference_symbolic(
            id_ref.as_str(),
            cob_ref.as_str(),
            false,
            "Create `rad/id` reference to point to new identity COB",
        )?;

        Ok(*cob.id)
    }
}

#[derive(Debug, Error)]
pub enum CanonicalRefsError {
    #[error(transparent)]
    Raw(#[from] RawCanonicalRefsError),
    #[error(transparent)]
    CanonicalRefs(#[from] rules::ValidationError),
    #[error(transparent)]
    DefaultBranch(#[from] DefaultBranchRuleError),
}

#[derive(Debug, Error)]
pub enum RawCanonicalRefsError {
    #[error(transparent)]
    Json(#[from] serde_json::Error),
}

pub trait GetRawCanonicalRefs: GetPayload {
    /// Retrieve the [`RawCanonicalRefs`] by deserializing from the payload
    /// (if present).
    fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, RawCanonicalRefsError> {
        let Some(value) = self.get_payload(&PayloadId::canonical_refs()) else {
            return Ok(None);
        };

        Ok(Some(serde_json::from_value(value.to_owned().into_inner())?))
    }
}

impl GetRawCanonicalRefs for Doc {}

impl GetRawCanonicalRefs for RawDoc {}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
    use serde_json::json;

    use crate::assert_matches;
    use crate::rad;
    use crate::storage::git::Storage;
    use crate::storage::git::transport;
    use crate::storage::{ReadStorage as _, RemoteId, WriteStorage as _};
    use crate::test::arbitrary;
    use crate::test::arbitrary::r#gen;
    use crate::test::fixtures;

    use super::*;
    use qcheck_macros::quickcheck;

    #[test]
    fn test_duplicate_dids() {
        let delegate = Device::mock_from_seed([0xff; 32]);
        let did = Did::from(delegate.public_key());
        let mut doc = RawDoc::new(r#gen::<Project>(1), vec![did], 1, Visibility::Public);
        doc.delegate(did);
        let doc = doc.verified().unwrap();
        assert!(doc.delegates().len() == 1, "Duplicate DID was not removed");
        assert!(doc.delegates().first() == &did)
    }

    #[test]
    fn test_max_delegates() {
        // Generate more than the max delegates
        let delegates = (0..MAX_DELEGATES + 1).map(r#gen).collect::<Vec<Did>>();

        // A document with max delegates will be fine
        let doc = RawDoc::new(
            r#gen::<Project>(1),
            delegates[0..MAX_DELEGATES].into(),
            1,
            Visibility::Public,
        );
        assert_matches!(doc.verified(), Ok(_));

        // A document that exceeds max delegates should fail
        let doc = RawDoc::new(r#gen::<Project>(1), delegates, 1, Visibility::Public);
        assert_matches!(doc.verified(), Err(DocError::Delegates(DelegatesError(_))));
    }

    #[test]
    fn test_is_valid_version() {
        // 0 is not a valid version
        assert!(!Version::is_valid_version(&0));

        // Ensures that the latest version is always valid
        let current = IDENTITY_VERSION.number();
        assert!(Version::is_valid_version(&current.into()));

        // Ensures that the next version is not valid because we have not
        // defined it yet
        let next = current.checked_add(1).unwrap();
        assert!(!Version::is_valid_version(&next.into()));
    }

    #[test]
    fn test_future_version_error() {
        let v = Version(NonZeroU32::MAX).to_string();
        assert_eq!(
            serde_json::from_str::<Version>(&v)
                .expect_err("should fail to deserialize")
                .to_string(),
            VersionError::UnknownVersion(NonZeroU32::MAX).to_string(),
        )
    }

    #[test]
    fn test_parse_version() {
        // Original document before introducing the version field
        let v1 = json!(
            {
                "payload": {
                    "xyz.radicle.project": {
                        "defaultBranch": "master",
                        "description": "Radicle Heartwood Protocol & Stack",
                        "name": "heartwood"
                    }
                },
                "delegates": [
                    "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
                    "did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
                    "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
                ],
                "threshold": 1
            }
        );

        // Deserializing the `RawDoc` should not fail and should include the
        // `IDENTITY_VERSION`.
        let doc = serde_json::from_str::<RawDoc>(&v1.to_string()).unwrap();
        let payload = [(
            PayloadId::project(),
            Payload {
                value: json!({
                    "name": "heartwood",
                    "description": "Radicle Heartwood Protocol & Stack",
                    "defaultBranch": "master",
                }),
            },
        )]
        .into_iter()
        .collect::<BTreeMap<_, _>>();
        let delegates = vec![
            "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"
                .parse::<Did>()
                .unwrap(),
            "did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW"
                .parse::<Did>()
                .unwrap(),
            "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
                .parse::<Did>()
                .unwrap(),
        ];
        // And this is the expected outcome of the deserialization
        assert_eq!(
            doc,
            RawDoc {
                version: IDENTITY_VERSION,
                payload: payload.clone(),
                delegates: delegates.clone(),
                threshold: 1,
                visibility: Visibility::Public,
            }
        );

        // Deserializing into `Doc` should also succeed.
        let verified = serde_json::from_str::<Doc>(&v1.to_string()).unwrap();
        let delegates = Delegates(NonEmpty::from_vec(delegates).unwrap());
        assert_eq!(
            verified,
            Doc {
                version: IDENTITY_VERSION,
                threshold: Threshold::new(1, &delegates).unwrap(),
                payload: payload.clone(),
                delegates,
                visibility: Visibility::Public,
            }
        );
    }

    #[test]
    fn test_canonical_example() {
        let tempdir = tempfile::tempdir().unwrap();
        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();

        transport::local::register(storage.clone());

        let delegate = Device::mock_from_seed([0xff; 32]);
        let (repo, _) = fixtures::repository(tempdir.path().join("working"));
        let (id, _, _) = rad::init(
            &repo,
            "heartwood".try_into().unwrap(),
            "Radicle Heartwood Protocol & Stack",
            git::fmt::refname!("master"),
            Visibility::default(),
            &delegate,
            &storage,
        )
        .unwrap();

        assert_eq!(
            delegate.public_key().to_human(),
            String::from("z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi")
        );
        assert_eq!(
            (*id).to_string(),
            "d96f425412c9f8ad5d9a9a05c9831d0728e2338d"
        );
        assert_eq!(id.urn(), String::from("rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji"));
    }

    #[test]
    fn test_not_found() {
        let tempdir = tempfile::tempdir().unwrap();
        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
        let remote = arbitrary::r#gen::<RemoteId>(1);
        let proj = arbitrary::r#gen::<RepoId>(1);
        let repo = storage.create(proj).unwrap();
        let oid = git::raw::Oid::from_str("2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8").unwrap();

        let err = repo.identity_head_of(&remote).unwrap_err();
        {
            use crate::git::raw::ErrorExt as _;
            assert!(err.is_not_found());
        }

        let err = Doc::load_at(oid.into(), &repo).unwrap_err();
        assert!(err.is_not_found());
    }

    #[test]
    fn test_canonical_doc() {
        let tempdir = tempfile::tempdir().unwrap();
        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
        transport::local::register(storage.clone());

        let (working, _) = fixtures::repository(tempdir.path().join("working"));

        let delegate = Device::mock_from_seed([0xff; 32]);
        let (rid, doc, _) = rad::init(
            &working,
            "heartwood".try_into().unwrap(),
            "Radicle Heartwood Protocol & Stack",
            git::fmt::refname!("master"),
            Visibility::default(),
            &delegate,
            &storage,
        )
        .unwrap();
        let repo = storage.repository(rid).unwrap();

        assert_eq!(doc, repo.identity_doc().unwrap().doc);
    }

    #[quickcheck]
    fn prop_encode_decode(doc: Doc) {
        let (_, bytes) = doc.encode().unwrap();
        assert_eq!(RawDoc::from_json(&bytes).unwrap().verified().unwrap(), doc);
    }

    #[test]
    fn test_visibility_json() {
        use std::str::FromStr;

        assert_eq!(
            serde_json::to_value(Visibility::Public).unwrap(),
            serde_json::json!({ "type": "public" })
        );
        assert_eq!(
            serde_json::to_value(Visibility::private([])).unwrap(),
            serde_json::json!({ "type": "private" })
        );
        assert_eq!(
            serde_json::to_value(Visibility::private([Did::from_str(
                "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"
            )
            .unwrap()]))
            .unwrap(),
            serde_json::json!({ "type": "private", "allow": ["did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"] })
        );
    }
}