use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use polyc_crypto::signing_role::{SigningRole as _, TurnReadRole, TurnReadSigner};
use serde::{Deserialize, Serialize};
pub const GRANT_KIND: &str = "query_conversation_grant.v2";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "id", rename_all = "snake_case")]
pub enum GrantSubject {
Turn(String),
WebSession(String),
Persona(String),
CompositeTrace {
persona_id: String,
trace_statement_digest: String,
memory_statement_digest: String,
routine_statement_digest: String,
},
AdminCompositeTrace {
trace_statement_digest: String,
routine_statement_digest: String,
},
ControlFleet {
purpose: String,
statement_digest: String,
},
AdminFleet {
admin_persona: String,
session: String,
},
}
impl GrantSubject {
#[allow(
clippy::missing_const_for_fn,
reason = "String's Deref to str isn't const (E0015)"
)]
#[must_use]
pub fn turn_id(&self) -> Option<&str> {
match self {
Self::Turn(id) => Some(id),
Self::WebSession(_)
| Self::Persona(_)
| Self::CompositeTrace { .. }
| Self::AdminCompositeTrace { .. }
| Self::ControlFleet { .. }
| Self::AdminFleet { .. } => None,
}
}
#[allow(clippy::missing_const_for_fn, reason = "see turn_id's own doc above")]
#[must_use]
pub fn web_session_id(&self) -> Option<&str> {
match self {
Self::WebSession(id) => Some(id),
Self::Turn(_)
| Self::Persona(_)
| Self::CompositeTrace { .. }
| Self::AdminCompositeTrace { .. }
| Self::ControlFleet { .. }
| Self::AdminFleet { .. } => None,
}
}
#[allow(clippy::missing_const_for_fn, reason = "see turn_id's own doc above")]
#[must_use]
pub fn persona_id(&self) -> Option<&str> {
match self {
Self::Persona(id) => Some(id),
Self::CompositeTrace { persona_id, .. } => Some(persona_id),
Self::Turn(_)
| Self::WebSession(_)
| Self::AdminCompositeTrace { .. }
| Self::ControlFleet { .. }
| Self::AdminFleet { .. } => None,
}
}
#[must_use]
pub fn control_fleet(&self) -> Option<(&str, &str)> {
match self {
Self::ControlFleet {
purpose,
statement_digest,
} => Some((purpose, statement_digest)),
Self::Turn(_)
| Self::WebSession(_)
| Self::Persona(_)
| Self::CompositeTrace { .. }
| Self::AdminCompositeTrace { .. }
| Self::AdminFleet { .. } => None,
}
}
#[must_use]
pub fn admin_fleet(&self) -> Option<(&str, &str)> {
match self {
Self::AdminFleet {
admin_persona,
session,
} => Some((admin_persona, session)),
Self::Turn(_)
| Self::WebSession(_)
| Self::Persona(_)
| Self::CompositeTrace { .. }
| Self::AdminCompositeTrace { .. }
| Self::ControlFleet { .. } => None,
}
}
#[must_use]
pub fn composite_trace(&self) -> Option<(&str, &str, &str, &str)> {
match self {
Self::CompositeTrace {
persona_id,
trace_statement_digest,
memory_statement_digest,
routine_statement_digest,
} => Some((
persona_id,
trace_statement_digest,
memory_statement_digest,
routine_statement_digest,
)),
Self::Turn(_)
| Self::WebSession(_)
| Self::Persona(_)
| Self::AdminCompositeTrace { .. }
| Self::ControlFleet { .. }
| Self::AdminFleet { .. } => None,
}
}
#[must_use]
pub fn admin_composite_trace(&self) -> Option<(&str, &str)> {
match self {
Self::AdminCompositeTrace {
trace_statement_digest,
routine_statement_digest,
} => Some((trace_statement_digest, routine_statement_digest)),
Self::Turn(_)
| Self::WebSession(_)
| Self::Persona(_)
| Self::CompositeTrace { .. }
| Self::ControlFleet { .. }
| Self::AdminFleet { .. } => None,
}
}
#[allow(clippy::missing_const_for_fn, reason = "see turn_id's own doc above")]
#[must_use]
pub fn owner_persona_id(&self) -> Option<&str> {
match self {
Self::Persona(id) | Self::WebSession(id) => Some(id),
Self::CompositeTrace { persona_id, .. } => Some(persona_id),
Self::Turn(_)
| Self::AdminCompositeTrace { .. }
| Self::ControlFleet { .. }
| Self::AdminFleet { .. } => None,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GrantClaims {
kind: String,
issuer: String,
key_id: String,
conversation_id: String,
subject: GrantSubject,
expires_at_ms: u64,
#[serde(default)]
memory_sources: Vec<String>,
}
impl GrantClaims {
#[must_use]
pub fn kind(&self) -> &str {
&self.kind
}
#[must_use]
pub fn issuer(&self) -> &str {
&self.issuer
}
#[must_use]
pub fn key_id(&self) -> &str {
&self.key_id
}
#[must_use]
pub const fn expires_at_ms(&self) -> u64 {
self.expires_at_ms
}
#[must_use]
pub fn into_conversation_and_subject(self) -> (String, GrantSubject) {
(self.conversation_id, self.subject)
}
#[must_use]
pub fn into_parts(self) -> (String, GrantSubject, Vec<String>) {
(self.conversation_id, self.subject, self.memory_sources)
}
}
#[must_use]
pub fn mint_conversation_grant(
signer: &TurnReadSigner,
conversation_id: &str,
subject: GrantSubject,
expires_at_ms: u64,
) -> String {
mint_conversation_grant_for_memory(signer, conversation_id, subject, expires_at_ms, Vec::new())
}
#[must_use]
pub fn mint_conversation_grant_for_memory(
signer: &TurnReadSigner,
conversation_id: &str,
subject: GrantSubject,
expires_at_ms: u64,
memory_sources: Vec<String>,
) -> String {
let claims = GrantClaims {
kind: GRANT_KIND.to_owned(),
issuer: TurnReadRole::ISSUER.to_owned(),
key_id: signer.identity().key_id().to_owned(),
conversation_id: conversation_id.to_owned(),
subject,
expires_at_ms,
memory_sources,
};
let canonical = serde_json::to_vec(&claims).expect("GrantClaims always serializes");
let signature = signer.sign_turn_read_capability(&canonical);
format!(
"{}.{}",
URL_SAFE_NO_PAD.encode(canonical),
URL_SAFE_NO_PAD.encode(signature)
)
}
#[must_use]
pub fn mint_control_fleet_grant(
signer: &TurnReadSigner,
purpose: &str,
statement_digest: &str,
expires_at_ms: u64,
) -> String {
mint_conversation_grant(
signer,
"",
GrantSubject::ControlFleet {
purpose: purpose.to_owned(),
statement_digest: statement_digest.to_owned(),
},
expires_at_ms,
)
}
#[must_use]
pub fn mint_admin_fleet_grant(
signer: &TurnReadSigner,
admin_persona: &str,
session: &str,
expires_at_ms: u64,
) -> String {
mint_conversation_grant(
signer,
"",
GrantSubject::AdminFleet {
admin_persona: admin_persona.to_owned(),
session: session.to_owned(),
},
expires_at_ms,
)
}
#[must_use]
pub fn mint_persona_wide_grant(
signer: &TurnReadSigner,
persona_id: &str,
expires_at_ms: u64,
) -> String {
mint_conversation_grant(
signer,
"",
GrantSubject::Persona(persona_id.to_owned()),
expires_at_ms,
)
}