use anyhow::Context;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
const ORG_SCHEME: &str = "mcpmesh-org:";
const JOIN_SCHEME: &str = "mcpmesh-join:";
const DEVICE_SCHEME: &str = "mcpmesh-device:";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrgInviteCode {
pub org_id: String,
pub org_root_pk: String, #[serde(default, skip_serializing_if = "Option::is_none")]
pub roster_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JoinCode {
pub display_name: String,
pub requested_user_id: String,
pub user_pk: String, pub device_endpoint_id: String, pub device_label: String,
pub binding_sig: String, }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeviceCode {
pub device_endpoint_id: String, pub device_label: String,
}
fn encode_code<T: Serialize>(scheme: &str, value: &T) -> String {
let json = serde_json::to_vec(value).expect("enrollment code serializes");
format!("{scheme}{}", data_encoding::BASE32_NOPAD.encode(&json))
}
fn decode_code<T: DeserializeOwned>(scheme: &str, line: &str) -> anyhow::Result<T> {
let payload = line
.strip_prefix(scheme)
.with_context(|| format!("not an {scheme} code (missing scheme)"))?;
let json = data_encoding::BASE32_NOPAD
.decode(payload.as_bytes())
.context("enrollment code payload is not valid base32")?;
serde_json::from_slice(&json).context("enrollment code payload is not the expected shape")
}
impl OrgInviteCode {
pub fn encode(&self) -> String {
encode_code(ORG_SCHEME, self)
}
pub fn decode(line: &str) -> anyhow::Result<Self> {
decode_code(ORG_SCHEME, line)
}
}
impl JoinCode {
pub fn encode(&self) -> String {
encode_code(JOIN_SCHEME, self)
}
pub fn decode(line: &str) -> anyhow::Result<Self> {
decode_code(JOIN_SCHEME, line)
}
}
impl DeviceCode {
pub fn encode(&self) -> String {
encode_code(DEVICE_SCHEME, self)
}
pub fn decode(line: &str) -> anyhow::Result<Self> {
decode_code(DEVICE_SCHEME, line)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn org_invite_code_round_trips_and_rejects_bad_input() {
let code = OrgInviteCode {
org_id: "acme".into(),
org_root_pk: "b64u:AAAA".into(),
roster_url: None,
};
let line = code.encode();
assert!(line.starts_with("mcpmesh-org:"));
assert_eq!(OrgInviteCode::decode(&line).unwrap(), code);
assert!(OrgInviteCode::decode("mcpmesh-join:AAAA").is_err());
assert!(OrgInviteCode::decode("mcpmesh-org:!!!").is_err());
assert!(OrgInviteCode::decode("nope").is_err());
}
#[test]
fn join_code_round_trips_carrying_the_binding() {
let code = JoinCode {
display_name: "Alice Nguyen".into(),
requested_user_id: "alice".into(),
user_pk: "b64u:UUUU".into(),
device_endpoint_id: "b64u:DDDD".into(),
device_label: "laptop".into(),
binding_sig: "b64u:SSSS".into(),
};
let line = code.encode();
assert!(line.starts_with("mcpmesh-join:"));
assert_eq!(JoinCode::decode(&line).unwrap(), code);
let not_join = data_encoding::BASE32_NOPAD.encode(b"{\"nope\":1}");
assert!(JoinCode::decode(&format!("mcpmesh-join:{not_join}")).is_err());
}
#[test]
fn device_code_round_trips() {
let code = DeviceCode {
device_endpoint_id: "b64u:DDDD".into(),
device_label: "desktop".into(),
};
let line = code.encode();
assert!(line.starts_with("mcpmesh-device:"));
assert_eq!(DeviceCode::decode(&line).unwrap(), code);
assert!(DeviceCode::decode("mcpmesh-org:AAAA").is_err());
}
}