systemprompt_cloud/operator/
mod.rs1mod endpoints;
17mod login;
18
19use std::fs;
20use std::path::Path;
21
22use base64::prelude::*;
23use chrono::{DateTime, Utc};
24use serde::{Deserialize, Serialize};
25use systemprompt_identifiers::{ClientId, CloudAuthToken, Email};
26
27use crate::error::{CloudError, CloudResult};
28use crate::private_dir::write_private_json;
29use crate::{CloudApiClient, auth};
30
31pub use login::{LoginIo, OperatorLoginRequest, run_operator_login, run_operator_login_with};
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct OperatorCredentials {
35 pub issuer: String,
36
37 pub client_id: ClientId,
38
39 pub id_token: CloudAuthToken,
40
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub email: Option<Email>,
43
44 pub authenticated_at: DateTime<Utc>,
45}
46
47#[derive(Deserialize)]
48struct IdentityClaims {
49 #[serde(default)]
50 email: Option<String>,
51 #[serde(default)]
52 nonce: Option<String>,
53}
54
55impl OperatorCredentials {
56 #[must_use]
57 pub fn is_expired(&self) -> bool {
58 auth::is_expired(&self.id_token)
59 }
60
61 pub fn load_from_path(path: &Path) -> CloudResult<Self> {
62 if !path.exists() {
63 return Err(CloudError::OperatorNotAuthenticated);
64 }
65 let content = fs::read_to_string(path)?;
66 serde_json::from_str(&content).map_err(|e| CloudError::CredentialsCorrupted {
67 source: Box::new(e),
68 })
69 }
70
71 pub fn load_valid(path: &Path) -> CloudResult<Self> {
72 let creds = Self::load_from_path(path)?;
73 if creds.is_expired() {
74 return Err(CloudError::OperatorNotAuthenticated);
75 }
76 Ok(creds)
77 }
78
79 pub fn save_to_path(&self, path: &Path) -> CloudResult<()> {
80 write_private_json(path, self)
81 }
82
83 pub fn delete_from_path(path: &Path) -> CloudResult<()> {
84 if path.exists() {
85 fs::remove_file(path)?;
86 }
87 Ok(())
88 }
89}
90
91fn identity_claims(id_token: &str) -> CloudResult<IdentityClaims> {
92 let payload = id_token.split('.').nth(1).ok_or(CloudError::JwtDecode)?;
93 let raw = BASE64_URL_SAFE_NO_PAD
94 .decode(payload)
95 .map_err(|_e| CloudError::JwtDecode)?;
96 serde_json::from_slice(&raw).map_err(|_e| CloudError::JwtDecode)
97}
98
99#[derive(Debug, Clone)]
100pub struct TenantSession {
101 pub access_token: String,
102}
103
104pub async fn exchange_for_tenant(
105 tenant_base_url: &str,
106 operator: &OperatorCredentials,
107) -> CloudResult<TenantSession> {
108 let client = CloudApiClient::new(tenant_base_url, operator.id_token.as_str())?;
109 let access_token = client.tenant_session_token().await?;
110 Ok(TenantSession { access_token })
111}