Skip to main content

prismer_sdk/
lib.rs

1//! Prismer Cloud SDK for Rust
2//!
3//! # Quick Start
4//! ```no_run
5//! use prismer_sdk::PrismerClient;
6//!
7//! #[tokio::main]
8//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
9//!     let client = PrismerClient::new("sk-prismer-live-...", None);
10//!     let result = client.context().load("https://example.com").await?;
11//!     println!("{:?}", result);
12//!     Ok(())
13//! }
14//! ```
15
16pub mod types;
17pub mod context;
18pub mod parse;
19pub mod im;
20pub mod evolution;
21pub mod evolution_cache;
22pub mod evolution_runtime;
23pub mod signal_rules;
24pub mod webhook;
25pub mod memory;
26pub mod knowledge;
27pub mod community;
28pub mod tasks;
29pub mod identity;
30pub mod files;
31pub mod daemon;
32pub mod ui;
33
34use reqwest::Client as HttpClient;
35use ed25519_dalek::{SigningKey, Signer};
36use sha2::{Sha256, Digest};
37
38/// Resolve API key with priority chain:
39///   1. Explicit value passed (non-empty string)
40///   2. `PRISMER_API_KEY` environment variable
41///   3. `~/.prismer/config.toml` api_key field
42///   4. Empty string
43fn resolve_api_key(explicit: &str) -> String {
44    if !explicit.is_empty() {
45        return explicit.to_string();
46    }
47    if let Ok(env_key) = std::env::var("PRISMER_API_KEY") {
48        if !env_key.is_empty() {
49            return env_key;
50        }
51    }
52    if let Some(home) = dirs::home_dir() {
53        let config_path = home.join(".prismer").join("config.toml");
54        if let Ok(raw) = std::fs::read_to_string(&config_path) {
55            if let Some(key) = toml_find(&raw, "api_key") {
56                return key;
57            }
58        }
59    }
60    String::new()
61}
62
63/// Resolve base URL with priority chain:
64///   1. Explicit value passed (Some with non-empty string)
65///   2. `PRISMER_BASE_URL` environment variable
66///   3. `~/.prismer/config.toml` base_url field
67///   4. Default `https://prismer.cloud`
68fn resolve_base_url(explicit: Option<&str>) -> String {
69    if let Some(url) = explicit {
70        if !url.is_empty() {
71            return url.to_string();
72        }
73    }
74    if let Ok(env_url) = std::env::var("PRISMER_BASE_URL") {
75        if !env_url.is_empty() {
76            return env_url;
77        }
78    }
79    if let Some(home) = dirs::home_dir() {
80        let config_path = home.join(".prismer").join("config.toml");
81        if let Ok(raw) = std::fs::read_to_string(&config_path) {
82            if let Some(url) = toml_find(&raw, "base_url") {
83                return url;
84            }
85        }
86    }
87    "https://prismer.cloud".to_string()
88}
89
90/// Parse a TOML-like config field from raw text.
91/// Looks for lines matching: `key = 'value'` or `key = "value"` (whitespace tolerant).
92fn toml_find(haystack: &str, key: &str) -> Option<String> {
93    for line in haystack.lines() {
94        let trimmed = line.trim_start();
95        if trimmed.starts_with(key) {
96            let rest = &trimmed[key.len()..];
97            let rest = rest.trim_start_matches(|c: char| c.is_whitespace());
98            if let Some(rest) = rest.strip_prefix('=') {
99                let rest = rest.trim_start();
100                for quote in ['"', '\''] {
101                    if rest.starts_with(quote) {
102                        let inner = &rest[1..];
103                        if let Some(end) = inner.find(quote) {
104                            return Some(inner[..end].to_string());
105                        }
106                    }
107                }
108            }
109        }
110    }
111    None
112}
113
114/// Main Prismer SDK client.
115pub struct PrismerClient {
116    http: HttpClient,
117    api_key: String,
118    base_url: String,
119    /// v1.8.0 S7: Optional Ed25519 signing key for auto-signing IM messages.
120    signing_key: Option<SigningKey>,
121    /// DID:key identifier derived from signing key.
122    pub identity_did: Option<String>,
123}
124
125impl PrismerClient {
126    /// Create a new client with API key and optional base URL override.
127    /// Fallback chain: explicit → PRISMER_API_KEY env → ~/.prismer/config.toml → ''
128    pub fn new(api_key: &str, base_url: Option<&str>) -> Self {
129        let resolved_key = resolve_api_key(api_key);
130        let resolved_url = resolve_base_url(base_url);
131        Self {
132            http: HttpClient::new(),
133            api_key: resolved_key,
134            base_url: resolved_url,
135            signing_key: None,
136            identity_did: None,
137        }
138    }
139
140    /// Create a client with auto-signing from API key (v1.8.0 S7).
141    /// Derives Ed25519 key via SHA-256(api_key).
142    /// Fallback chain: explicit → PRISMER_API_KEY env → ~/.prismer/config.toml → ''
143    pub fn new_with_identity(api_key: &str, base_url: Option<&str>) -> Self {
144        let resolved_key = resolve_api_key(api_key);
145        let resolved_url = resolve_base_url(base_url);
146        let seed: [u8; 32] = Sha256::digest(resolved_key.as_bytes()).into();
147        let signing_key = SigningKey::from_bytes(&seed);
148        let pub_key = signing_key.verifying_key();
149        let did = public_key_to_did_key(&pub_key.to_bytes());
150        Self {
151            http: HttpClient::new(),
152            api_key: resolved_key,
153            base_url: resolved_url,
154            signing_key: Some(signing_key),
155            identity_did: Some(did),
156        }
157    }
158
159    /// Sign a message payload and return (content_hash, signature, sender_did).
160    /// Sign a message payload (lite format: secVersion|senderDid|type|timestamp|contentHash).
161    /// Returns (content_hash, signature_b64, sender_did, timestamp_ms).
162    pub(crate) fn sign_message(&self, content: &str, msg_type: &str) -> Option<(String, String, String, u64)> {
163        let key = self.signing_key.as_ref()?;
164        let did = self.identity_did.as_ref()?;
165        let content_hash = hex::encode(Sha256::digest(content.as_bytes()));
166        let timestamp = std::time::SystemTime::now()
167            .duration_since(std::time::UNIX_EPOCH).ok()?.as_millis() as u64;
168        let payload = format!("1|{}|{}|{}|{}", did, msg_type, timestamp, content_hash);
169        let sig = key.sign(payload.as_bytes());
170        Some((content_hash, base64::Engine::encode(&base64::engine::general_purpose::STANDARD, sig.to_bytes()), did.clone(), timestamp))
171    }
172
173    /// Get Context API client.
174    pub fn context(&self) -> context::ContextClient<'_> {
175        context::ContextClient { client: self }
176    }
177
178    /// Get Parse API client.
179    pub fn parse(&self) -> parse::ParseClient<'_> {
180        parse::ParseClient { client: self }
181    }
182
183    /// Get IM API client.
184    pub fn im(&self) -> im::IMClient<'_> {
185        im::IMClient::new(self)
186    }
187
188    /// Get Evolution API client.
189    pub fn evolution(&self) -> evolution::EvolutionClient<'_> {
190        evolution::EvolutionClient { client: self }
191    }
192
193    /// Get Memory API client.
194    pub fn memory(&self) -> memory::MemoryClient<'_> {
195        memory::MemoryClient { client: self }
196    }
197
198    /// Get Knowledge Links API client (v1.8.0).
199    pub fn knowledge(&self) -> knowledge::KnowledgeLinkClient<'_> {
200        knowledge::KnowledgeLinkClient { client: self }
201    }
202
203    /// Get Community API client.
204    pub fn community(&self) -> community::CommunityClient<'_> {
205        community::CommunityClient { client: self }
206    }
207
208    /// Get Tasks API client.
209    pub fn tasks(&self) -> tasks::TasksClient<'_> {
210        tasks::TasksClient { client: self }
211    }
212
213    /// Get Identity API client.
214    pub fn identity(&self) -> identity::IdentityClient<'_> {
215        identity::IdentityClient { client: self }
216    }
217
218    /// Get Files API client.
219    pub fn files(&self) -> files::FilesClient<'_> {
220        files::FilesClient { client: self }
221    }
222
223    /// Make an authenticated request. Exposed for CLI and advanced usage.
224    pub async fn request<T: serde::de::DeserializeOwned>(
225        &self,
226        method: reqwest::Method,
227        path: &str,
228        body: Option<serde_json::Value>,
229    ) -> Result<types::ApiResponse<T>, types::PrismerError> {
230        let url = format!("{}{}", self.base_url, path);
231        let mut req = self.http.request(method, &url)
232            .header("Authorization", format!("Bearer {}", self.api_key))
233            .header("Content-Type", "application/json");
234
235        if let Some(b) = body {
236            req = req.json(&b);
237        }
238
239        let resp = req.send().await.map_err(|e| types::PrismerError::Network(e.to_string()))?;
240        let status = resp.status();
241        let text = resp.text().await.map_err(|e| types::PrismerError::Network(e.to_string()))?;
242
243        if !status.is_success() {
244            return Err(types::PrismerError::Api {
245                status: status.as_u16(),
246                message: text,
247            });
248        }
249
250        serde_json::from_str(&text).map_err(|e| types::PrismerError::Parse(e.to_string()))
251    }
252}
253
254/// Convert Ed25519 public key bytes to did:key format.
255fn public_key_to_did_key(pub_key: &[u8; 32]) -> String {
256    // Multicodec ed25519-pub = 0xed, varint = [0xed, 0x01]
257    let mut multicodec = vec![0xed, 0x01];
258    multicodec.extend_from_slice(pub_key);
259    format!("did:key:z{}", bs58::encode(&multicodec).into_string())
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn client_default_base_url() {
268        let client = PrismerClient::new("sk-prismer-live-abc123", None);
269        assert_eq!(client.base_url, "https://prismer.cloud");
270        assert_eq!(client.api_key, "sk-prismer-live-abc123");
271    }
272
273    #[test]
274    fn client_custom_base_url() {
275        let client = PrismerClient::new("sk-test", Some("https://cloud.prismer.dev"));
276        assert_eq!(client.base_url, "https://cloud.prismer.dev");
277    }
278
279    #[test]
280    fn client_empty_api_key() {
281        let client = PrismerClient::new("", None);
282        assert_eq!(client.api_key, "");
283    }
284
285    #[test]
286    fn client_context_returns_context_client() {
287        let client = PrismerClient::new("sk-test", None);
288        let _ctx = client.context();
289    }
290
291    #[test]
292    fn client_parse_returns_parse_client() {
293        let client = PrismerClient::new("sk-test", None);
294        let _p = client.parse();
295    }
296
297    #[test]
298    fn client_im_returns_im_client() {
299        let client = PrismerClient::new("sk-test", None);
300        let _im = client.im();
301    }
302
303    #[test]
304    fn client_evolution_returns_evolution_client() {
305        let client = PrismerClient::new("sk-test", None);
306        let _ev = client.evolution();
307    }
308
309    #[test]
310    fn client_memory_returns_memory_client() {
311        let client = PrismerClient::new("sk-test", None);
312        let _m = client.memory();
313    }
314
315    #[test]
316    fn client_tasks_returns_tasks_client() {
317        let client = PrismerClient::new("sk-test", None);
318        let _t = client.tasks();
319    }
320
321    #[test]
322    fn client_identity_returns_identity_client() {
323        let client = PrismerClient::new("sk-test", None);
324        let _id = client.identity();
325    }
326
327    #[test]
328    fn client_files_returns_files_client() {
329        let client = PrismerClient::new("sk-test", None);
330        let _f = client.files();
331    }
332
333    #[test]
334    fn client_community_returns_community_client() {
335        let client = PrismerClient::new("sk-test", None);
336        let _c = client.community();
337    }
338
339    #[test]
340    fn client_knowledge_returns_knowledge_client() {
341        let client = PrismerClient::new("sk-test", None);
342        let _k = client.knowledge();
343    }
344}