use std::{cell::RefCell, collections::HashMap, fmt::Display, rc::Rc, str::FromStr, time::Instant};
use account::AccountCredentialSource;
use audi::Listener;
use credo::{
ClaimKind, Credential, Credo, MakeClaimError, PermissionKind, ScopeID, WRITE_TO_SCOPE,
};
use futures::{channel::mpsc::channel, future, StreamExt};
use jmbl::Input;
use litl::{impl_debug_as_litl, serde::deserializer::DeserializeError, Litl};
use mofo::Mofo;
use rand07::{rngs::OsRng, RngCore};
use ridl::symm_encr::{DecryptionError, Encrypted, KeyDerivationError, PasswordDerivedKeySalt};
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;
use tlpt::{BlobDiff, Diff, ObjectID, Remote};
#[cfg(feature = "js")]
use wasm_bindgen::prelude::*;
mod account;
mod conventions;
mod managed_jmbl;
use conventions::{
admin_permissions, claim_to_name_doc, read_permissions, write_permissions,
CREDENTIALS_DOC_NAME, NAMED_DOC_PREFIX, READ_CONTENT,
};
pub use managed_jmbl::ManagedJMBL;
use crate::conventions::named_doc_path;
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "js", wasm_bindgen)]
pub struct DocID(ScopeID);
impl_debug_as_litl!(DocID);
impl Display for DocID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&Litl::from_se(self).to_json().to_string())
}
}
impl FromStr for DocID {
type Err = DeserializeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Litl::from_json(s.into()).try_into_de()
}
}
struct JazzInner {
content: tlpt::Node,
credo: Credo,
docs: HashMap<DocID, ManagedJMBL>,
background: Mofo,
}
#[derive(Clone)]
pub struct Jazz(Rc<RefCell<JazzInner>>);
impl Jazz {
pub fn new(name: String, background: Mofo) -> Self {
let node = tlpt::Node::new(background.clone());
let credo = Credo::new_with_telepathy(name, node.clone(), background.clone());
Jazz(Rc::new(RefCell::new(JazzInner {
content: node,
credo,
docs: HashMap::new(),
background,
})))
}
pub fn new_with_account(
name: String,
background: Mofo,
account_id: ScopeID,
account_credential: Credential,
) -> Self {
let node = tlpt::Node::new(background.clone());
Jazz(Rc::new_cyclic(|weak_ref| {
let credo = Credo::new_with_telepathy_and_credential_source(
name,
node.clone(),
AccountCredentialSource::new(account_id, account_credential, weak_ref.clone()),
background.clone(),
);
RefCell::new(JazzInner {
content: node,
credo,
docs: HashMap::new(),
background,
})
}))
}
pub async fn new_with_create_account(
name: String,
background: Mofo,
) -> (Jazz, ScopeID, Credential) {
let node = tlpt::Node::new(background.clone());
let credo = Credo::new_with_telepathy(
format!("{}-create-account", name),
node.clone(),
background.clone(),
);
let jazz = Jazz(Rc::new(RefCell::new(JazzInner {
content: node,
credo,
docs: HashMap::new(),
background: background.clone(),
})));
let account_credential = Credential::new_random();
let account_id = jazz
.create_team_for(
account_credential.clone(),
vec![
PermissionKind::MakeStatement {
path_prefix: NAMED_DOC_PREFIX.to_string(),
},
PermissionKind::DelegateInfintely(Box::new(PermissionKind::MakeStatement {
path_prefix: NAMED_DOC_PREFIX.to_string(),
})),
],
)
.await;
let (credential_doc_id, _) = jazz
.create_document(account_id, Input::CollabMap(vec![]))
.await;
jazz.credo()
.make_claim_after_frontier(
&account_id,
claim_to_name_doc(credential_doc_id, CREDENTIALS_DOC_NAME),
)
.await
.unwrap();
let telepathy_for_credo = jazz.content();
let jazz_for_credo = Rc::downgrade(&jazz.0);
jazz.0.borrow_mut().credo = Credo::new_with_telepathy_and_credential_source(
name,
telepathy_for_credo,
AccountCredentialSource::new(account_id, account_credential.clone(), jazz_for_credo),
background.clone(),
);
(jazz, account_id, account_credential)
}
pub async fn create_document<I: Into<Input>>(
&self,
team: ScopeID,
input: I,
) -> (DocID, ManagedJMBL) {
let credo = self.credo();
let recipient = credo
.credential_for(&team, &ClaimKind::InheritFrom { parent: team })
.await
.expect("Expected credentials for creating documents");
let doc_scope_id = credo.create_scope(recipient, vec![], Some(team)).await;
let doc_id = DocID(doc_scope_id);
let background = { self.0.borrow().background.clone() };
let managed_jmbl =
ManagedJMBL::create(doc_id.clone(), input, self.content(), credo, background).await;
self.0
.borrow_mut()
.docs
.insert(doc_id.clone(), managed_jmbl.clone());
(doc_id, managed_jmbl)
}
pub async fn load_document(&self, doc_id: DocID) -> ManagedJMBL {
let existing = self.0.borrow().docs.get(&doc_id).cloned();
match existing {
Some(managed_jmbl) => managed_jmbl,
None => {
let (managed_jmbl, loaded) = ManagedJMBL::load(
doc_id.clone(),
self.content(),
self.credo(),
self.0.borrow().background.clone(),
);
self.0
.borrow_mut()
.docs
.insert(doc_id, managed_jmbl.clone());
loaded.await.unwrap();
managed_jmbl
}
}
}
pub async fn resolve_named_document(&self, team_id: ScopeID, name: &str) -> DocID {
let (team_update_tx, mut team_update_rx) = channel(100);
self.credo()
.subscribe(
team_id,
Listener::new(
&format!(
"named_doc_{}_{}_{:?}",
Litl::from_se(&team_id).tagged_data_to_raw_str(),
name,
rand07::random::<u64>()
),
team_update_tx,
),
)
.await;
while let Some(update) = team_update_rx.next().await {
if let Some(doc_id_litl) = update.valid_claims.iter().find_map(|(_, (claim, _))| {
if let ClaimKind::Statement { path, value } = &claim.kind {
if path == &named_doc_path(name) {
Some(value.clone())
} else {
None
}
} else {
None
}
}) {
return doc_id_litl.try_into_de::<DocID>().unwrap();
}
}
unreachable!("Team update stream ended before named document was found");
}
fn credo(&self) -> Credo {
self.0.borrow().credo.clone()
}
fn content(&self) -> tlpt::Node {
self.0.borrow().content.clone()
}
pub async fn create_team(&self) -> ScopeID {
let initial_recipient = Credential::new_random();
self.create_team_for(initial_recipient, vec![]).await
}
async fn create_team_for(
&self,
initial_recipient: Credential,
extra_permissions: Vec<PermissionKind>,
) -> ScopeID {
let credo = self.credo();
let team = credo
.create_scope(
initial_recipient.clone(),
admin_permissions()
.into_iter()
.chain(extra_permissions)
.collect(),
None,
)
.await;
credo
.make_claim_after_frontier(
&team,
ClaimKind::AddSharedSecretRecipient {
secret_kind: READ_CONTENT.to_string(),
recipient: initial_recipient.for_accepting_secrets.pub_id(),
},
)
.await
.unwrap();
credo
.rotate_shared_secret(&team, READ_CONTENT)
.await
.unwrap();
team
}
pub async fn create_invitation(
&self,
team: ScopeID,
kind: InvitationKind,
doc: Option<DocID>,
public_meta: Option<Litl>,
private_meta: Option<Litl>,
) -> Result<InvitationToken, MakeClaimError> {
let invitee = Credential::new_random();
let invitation_permissions = match kind {
InvitationKind::Reader => read_permissions(),
InvitationKind::Writer => write_permissions(),
InvitationKind::Admin => admin_permissions(),
};
let credo = self.credo();
for permission in invitation_permissions {
credo
.make_claim_after_frontier(
&team,
ClaimKind::Permission {
to: invitee.for_making_claims.pub_id(),
as_of: ti64::now(),
permitted: permission,
},
)
.await
.unwrap();
}
credo
.make_claim_after_frontier(
&team,
ClaimKind::AddSharedSecretRecipient {
secret_kind: READ_CONTENT.to_owned(),
recipient: invitee.for_accepting_secrets.pub_id(),
},
)
.await?;
credo.re_reveal_shared_secret(&team, READ_CONTENT).await?;
match kind {
InvitationKind::Writer | InvitationKind::Admin => {
credo
.make_claim_after_frontier(
&team,
ClaimKind::AddSharedSecretRecipient {
secret_kind: WRITE_TO_SCOPE.to_owned(),
recipient: invitee.for_accepting_secrets.pub_id(),
},
)
.await?;
credo.re_reveal_shared_secret(&team, WRITE_TO_SCOPE).await?;
}
_ => {}
}
let mut password = vec![0u8; 12];
OsRng {}.fill_bytes(&mut password);
let (secret, salt) = PasswordDerivedKeySalt::create_from_password(&password);
let invitation = Invitation {
public_meta,
salt,
private: secret.encrypt(&InvitationPrivatePart {
team,
credential: invitee,
doc,
private_meta,
}),
};
let invitation_id = self.content().create_blob(Litl::from_se(&invitation));
Ok(InvitationToken {
invitation_id,
password,
})
}
pub async fn join_team(
&self,
invitation_token: InvitationToken,
) -> Result<(Invitation, InvitationPrivatePart), JoinTeamError> {
let (blob_tx, blob_rx) = channel(10);
self.content()
.add_local_listener(
invitation_token.invitation_id,
Listener::new(
&format!("load-{:?}", invitation_token.invitation_id),
blob_tx,
),
)
.await;
let invitation_data = blob_rx
.filter_map(|diff| match diff {
Diff::Blob(BlobDiff { data, .. }) => future::ready(data),
_ => panic!("Unexpected blob diff"),
})
.next()
.await
.ok_or(JoinTeamError::CouldntLoadInvitation)?;
let invitation: Invitation = invitation_data.try_into_de()?;
let invitation_secret = invitation
.salt
.derive_secret_key(&invitation_token.password)?;
let private_part = invitation_secret.decrypt(&invitation.private)?;
self.credo()
.add_credential(private_part.team, private_part.credential.clone())
.await;
Ok((invitation, private_part))
}
pub async fn add_remote(&self, remote: Remote) {
self.credo().add_remote(remote).await;
}
}
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
pub enum InvitationKind {
Reader,
Writer,
Admin,
}
#[derive(Serialize, Deserialize)]
pub struct InvitationPrivatePart {
pub team: ScopeID,
credential: Credential,
pub doc: Option<DocID>,
pub private_meta: Option<Litl>,
}
#[derive(Serialize, Deserialize)]
pub struct Invitation {
private: Encrypted<InvitationPrivatePart>,
pub public_meta: Option<Litl>,
salt: PasswordDerivedKeySalt,
}
#[derive(Serialize, Deserialize)]
pub struct InvitationToken {
pub invitation_id: ObjectID,
#[serde(with = "serde_bytes")]
pub password: Vec<u8>,
}
impl Display for InvitationToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}:{}",
Litl::from_se(&self.invitation_id),
Litl::tagged_data("pw", &self.password)
)
}
}
impl FromStr for InvitationToken {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(':');
let invitation_id = Litl::parse(parts.next().ok_or(())?)
.map_err(|_| ())?
.try_into_de()
.map_err(|_| ())?;
let password = Litl::parse(parts.next().ok_or(())?)
.map_err(|_| ())?
.if_tagged_data("pw")
.map_err(|_| ())?
.to_vec();
Ok(InvitationToken {
invitation_id,
password,
})
}
}
impl_debug_as_litl!(InvitationToken);
#[derive(Error, Debug)]
pub enum JoinTeamError {
#[error("Couldn't load invitation")]
CouldntLoadInvitation,
#[error("Couldn't deserialize invitation")]
CouldntDeserializeInvitation(#[from] DeserializeError),
#[error("Couldn't derive key from password")]
CouldntDeriveKey(#[from] KeyDerivationError),
#[error("Couldn't decrypt invitation")]
CouldntDecryptInvitation(#[from] DecryptionError),
}