use serde::{Deserialize, Serialize};
pub mod resolve;
pub fn validate_organization_id(org: &str) -> Result<(), String> {
if org.is_empty()
|| org.len() > 256
|| !org
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
{
return Err(format!(
"`{org}` is not a canonical organization id (letters, digits, `.`, `_`, `-`)"
));
}
Ok(())
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct WorkContext {
pub api_base: String,
pub account_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub organization_id: Option<String>,
}
impl WorkContext {
pub fn new(
api_base: impl Into<String>,
account_id: impl Into<String>,
organization_id: Option<String>,
) -> Result<Self, String> {
let context = Self {
api_base: api_base.into().trim_end_matches('/').to_string(),
account_id: account_id.into(),
organization_id,
};
context.validate()?;
Ok(context)
}
pub fn storage_key(&self, resource: &str) -> String {
use sha2::{Digest, Sha256};
let mut hash = Sha256::new();
hash.update(b"car-work-context-v1");
for value in [
self.api_base.as_str(),
self.account_id.as_str(),
self.organization_id.as_deref().unwrap_or(""),
resource,
] {
hash.update((value.len() as u64).to_be_bytes());
hash.update(value.as_bytes());
}
format!("work-{:x}", hash.finalize())
}
pub fn validate(&self) -> Result<(), String> {
let url = url::Url::parse(&self.api_base).map_err(|_| "invalid credential authority")?;
let loopback = url.host_str().is_some_and(|host| {
host == "localhost"
|| host
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| ip.is_loopback())
});
if (url.scheme() != "https" && !(url.scheme() == "http" && loopback))
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
|| self.api_base.ends_with('/')
|| self.account_id.trim() != self.account_id
|| self.account_id.is_empty()
|| self.account_id.len() > 256
{
return Err("credential context requires a canonical authority and account".into());
}
if self
.organization_id
.as_deref()
.is_some_and(|org| validate_organization_id(org).is_err())
{
return Err("credential context requires a canonical organization id".into());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn private_resource_keys_keep_all_ownership_dimensions_separate() {
let base =
WorkContext::new("https://api.parslee.ai", "matt", Some("parslee".into())).unwrap();
let key = base.storage_key("agent:same-id");
let mut contexts = Vec::new();
let mut other = base.clone();
other.organization_id = Some("flyexclusive".into());
contexts.push(other);
let mut other = base.clone();
other.organization_id = None;
contexts.push(other);
let mut other = base.clone();
other.account_id = "colleague".into();
contexts.push(other);
let mut other = base.clone();
other.api_base = "https://other.example".into();
contexts.push(other);
for other in contexts {
assert_ne!(other.storage_key("agent:same-id"), key);
}
assert_ne!(base.storage_key("agent:other-id"), key);
assert_eq!(
serde_json::from_str::<WorkContext>(&serde_json::to_string(&base).unwrap())
.unwrap()
.storage_key("agent:same-id"),
key
);
}
}