move repos into monorepo

This commit is contained in:
Timur Gordon
2025-11-13 20:44:00 +01:00
commit 4b23e5eb7f
204 changed files with 33737 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
//! Communication methods (queries and commands)
use serde::{Deserialize, Serialize};
use crate::{OsirisClient, OsirisClientError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Verification {
pub id: String,
pub email: String,
pub code: String,
pub transport: String,
pub status: VerificationStatus,
pub created_at: i64,
pub expires_at: i64,
pub verified_at: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VerificationStatus {
Pending,
Verified,
Expired,
Failed,
}
// ========== Request/Response Models ==========
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendVerificationRequest {
pub email: String,
pub verification_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendVerificationResponse {
pub verification_id: String,
pub email: String,
pub expires_at: i64,
}
// ========== Client Methods ==========
impl OsirisClient {
// ========== Query Methods ==========
/// Get verification by ID (query)
pub async fn get_verification(&self, verification_id: &str) -> Result<Verification, OsirisClientError> {
self.get("verification", verification_id).await
}
/// Get verification by email (query)
pub async fn get_verification_by_email(&self, email: &str) -> Result<Vec<Verification>, OsirisClientError> {
self.query("verification", &format!("email={}", email)).await
}
/// Get verification status - alias for get_verification (query)
pub async fn get_verification_status(&self, verification_id: &str) -> Result<Verification, OsirisClientError> {
self.get_verification(verification_id).await
}
// ========== Command Methods ==========
/// Send verification email (command)
pub async fn send_verification_email(
&self,
request: SendVerificationRequest,
) -> Result<SendVerificationResponse, OsirisClientError> {
let email = &request.email;
let verification_url = request.verification_url.as_deref().unwrap_or("");
// Generate verification code
let verification_id = format!("ver_{}", uuid::Uuid::new_v4());
let code = format!("{:06}", (uuid::Uuid::new_v4().as_u128() % 1_000_000));
let script = format!(r#"
// Send email verification
let email = "{}";
let code = "{}";
let verification_url = "{}";
let verification_id = "{}";
// TODO: Implement actual email sending logic
print("Sending verification email to: " + email);
print("Verification code: " + code);
print("Verification URL: " + verification_url);
// Return verification details
verification_id
"#, email, code, verification_url, verification_id);
let _response = self.execute_script(&script).await?;
Ok(SendVerificationResponse {
verification_id,
email: request.email,
expires_at: chrono::Utc::now().timestamp() + 3600, // 1 hour
})
}
}

View File

@@ -0,0 +1,102 @@
//! KYC methods (queries and commands)
use serde::{Deserialize, Serialize};
use crate::{OsirisClient, OsirisClientError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KycSession {
pub id: String,
pub resident_id: String,
pub status: KycSessionStatus,
pub kyc_url: Option<String>,
pub created_at: i64,
pub updated_at: i64,
pub expires_at: i64,
pub verified_at: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum KycSessionStatus {
Pending,
InProgress,
Completed,
Failed,
Expired,
}
// ========== Request/Response Models ==========
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KycVerificationRequest {
pub resident_id: String,
pub callback_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KycVerificationResponse {
pub session_id: String,
pub kyc_url: String,
pub expires_at: i64,
}
// ========== Client Methods ==========
impl OsirisClient {
// ========== Query Methods ==========
/// Get KYC session by ID
pub async fn get_kyc_session(&self, session_id: &str) -> Result<KycSession, OsirisClientError> {
self.get("kyc_session", session_id).await
}
/// List all KYC sessions for a resident
pub async fn list_kyc_sessions_by_resident(&self, resident_id: &str) -> Result<Vec<KycSession>, OsirisClientError> {
self.query("kyc_session", &format!("resident_id={}", resident_id)).await
}
// ========== Command Methods ==========
/// Start KYC verification (command)
pub async fn start_kyc_verification(
&self,
request: KycVerificationRequest,
) -> Result<KycVerificationResponse, OsirisClientError> {
let resident_id = &request.resident_id;
let callback_url = request.callback_url.as_deref().unwrap_or("");
// Generate session ID
let session_id = format!("kyc_{}", uuid::Uuid::new_v4());
let script = format!(r#"
// Start KYC verification
let resident_id = "{}";
let callback_url = "{}";
let session_id = "{}";
// TODO: Implement actual KYC provider integration
print("Starting KYC verification for resident: " + resident_id);
print("Session ID: " + session_id);
print("Callback URL: " + callback_url);
// Return session details
session_id
"#, resident_id, callback_url, session_id);
let _response = self.execute_script(&script).await?;
Ok(KycVerificationResponse {
session_id,
kyc_url: "https://kyc.example.com/verify".to_string(),
expires_at: chrono::Utc::now().timestamp() + 86400,
})
}
/// Check KYC status (query)
pub async fn check_kyc_status(
&self,
session_id: String,
) -> Result<KycSession, OsirisClientError> {
self.get_kyc_session(&session_id).await
}
}

View File

@@ -0,0 +1,439 @@
//! Osiris Client - Unified CQRS Client
//!
//! This client provides both:
//! - Commands (writes) via Rhai scripts to Hero Supervisor
//! - Queries (reads) via REST API to Osiris server
//!
//! Follows CQRS pattern with a single unified interface.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
pub mod kyc;
pub mod payment;
pub mod communication;
pub use kyc::*;
pub use payment::*;
pub use communication::*;
#[derive(Debug, Error)]
pub enum OsirisClientError {
#[error("HTTP request failed: {0}")]
RequestFailed(#[from] reqwest::Error),
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Deserialization failed: {0}")]
DeserializationFailed(String),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Command execution failed: {0}")]
CommandFailed(String),
}
/// Osiris client with CQRS support
#[derive(Clone)]
pub struct OsirisClient {
// Query side (Osiris REST API)
osiris_url: String,
// Command side (Supervisor + Rhai)
supervisor_client: Option<hero_supervisor_openrpc_client::SupervisorClient>,
runner_name: String,
timeout: u64,
// HTTP client
client: reqwest::Client,
}
/// Builder for OsirisClient
#[derive(Clone, Debug, Default)]
pub struct OsirisClientBuilder {
osiris_url: Option<String>,
supervisor_url: Option<String>,
runner_name: Option<String>,
supervisor_secret: Option<String>,
timeout: u64,
}
impl OsirisClientBuilder {
/// Create a new builder
pub fn new() -> Self {
Self {
osiris_url: None,
supervisor_url: None,
runner_name: None,
supervisor_secret: None,
timeout: 30,
}
}
/// Set the Osiris server URL (for queries)
pub fn osiris_url(mut self, url: impl Into<String>) -> Self {
self.osiris_url = Some(url.into());
self
}
/// Set the Supervisor URL (for commands)
pub fn supervisor_url(mut self, url: impl Into<String>) -> Self {
self.supervisor_url = Some(url.into());
self
}
/// Set the runner name (default: "osiris")
pub fn runner_name(mut self, name: impl Into<String>) -> Self {
self.runner_name = Some(name.into());
self
}
/// Set the supervisor authentication secret
pub fn supervisor_secret(mut self, secret: impl Into<String>) -> Self {
self.supervisor_secret = Some(secret.into());
self
}
/// Set the timeout in seconds (default: 30)
pub fn timeout(mut self, timeout: u64) -> Self {
self.timeout = timeout;
self
}
/// Build the OsirisClient
pub fn build(self) -> Result<OsirisClient, OsirisClientError> {
let osiris_url = self.osiris_url
.ok_or_else(|| OsirisClientError::ConfigError("osiris_url is required".to_string()))?;
// Build supervisor client if URL and secret are provided
let supervisor_client = if let (Some(url), Some(secret)) = (self.supervisor_url, self.supervisor_secret) {
Some(
hero_supervisor_openrpc_client::SupervisorClient::builder()
.url(url)
.secret(secret)
.build()
.map_err(|e| OsirisClientError::ConfigError(format!("Failed to create supervisor client: {:?}", e)))?
)
} else {
None
};
Ok(OsirisClient {
osiris_url,
supervisor_client,
runner_name: self.runner_name.unwrap_or_else(|| "osiris".to_string()),
timeout: self.timeout,
client: reqwest::Client::new(),
})
}
}
impl OsirisClient {
/// Create a new Osiris client (query-only)
pub fn new(osiris_url: impl Into<String>) -> Self {
Self {
osiris_url: osiris_url.into(),
supervisor_client: None,
runner_name: "osiris".to_string(),
timeout: 30,
client: reqwest::Client::new(),
}
}
/// Create a builder for full CQRS configuration
pub fn builder() -> OsirisClientBuilder {
OsirisClientBuilder::new()
}
/// Generic GET request for any struct by ID
pub async fn get<T>(&self, struct_name: &str, id: &str) -> Result<T, OsirisClientError>
where
T: for<'de> Deserialize<'de>,
{
let url = format!("{}/api/{}/{}", self.osiris_url, struct_name, id);
let response = self.client
.get(&url)
.send()
.await?;
if response.status() == 404 {
return Err(OsirisClientError::NotFound(format!("{}/{}", struct_name, id)));
}
let data = response
.json::<T>()
.await
.map_err(|e| OsirisClientError::DeserializationFailed(e.to_string()))?;
Ok(data)
}
/// Generic LIST request for all instances of a struct
pub async fn list<T>(&self, struct_name: &str) -> Result<Vec<T>, OsirisClientError>
where
T: for<'de> Deserialize<'de>,
{
let url = format!("{}/api/{}", self.osiris_url, struct_name);
let response = self.client
.get(&url)
.send()
.await?;
let data = response
.json::<Vec<T>>()
.await
.map_err(|e| OsirisClientError::DeserializationFailed(e.to_string()))?;
Ok(data)
}
/// Generic QUERY request with filters
pub async fn query<T>(&self, struct_name: &str, query: &str) -> Result<Vec<T>, OsirisClientError>
where
T: for<'de> Deserialize<'de>,
{
let url = format!("{}/api/{}?{}", self.osiris_url, struct_name, query);
let response = self.client
.get(&url)
.send()
.await?;
let data = response
.json::<Vec<T>>()
.await
.map_err(|e| OsirisClientError::DeserializationFailed(e.to_string()))?;
Ok(data)
}
// ========== Command Methods (Supervisor + Rhai) ==========
// Commands are write operations that execute Rhai scripts via the supervisor
// to modify state in Osiris
/// Execute a Rhai script via the Supervisor
pub async fn execute_script(&self, script: &str) -> Result<RunJobResponse, OsirisClientError> {
let supervisor_client = self.supervisor_client.as_ref()
.ok_or_else(|| OsirisClientError::ConfigError("supervisor_client not configured for commands".to_string()))?;
// Use JobBuilder from supervisor client (which re-exports from hero-job)
use hero_supervisor_openrpc_client::JobBuilder;
let job = JobBuilder::new()
.caller_id("osiris-client")
.context_id("command-execution")
.runner(&self.runner_name)
.payload(script)
.executor("rhai")
.timeout(self.timeout)
.build()
.map_err(|e| OsirisClientError::CommandFailed(format!("Failed to build job: {}", e)))?;
// Use job_run method which returns JobRunResponse
// Secret is sent via Authorization header (configured during client creation)
let result = supervisor_client.job_run(job, Some(self.timeout))
.await
.map_err(|e| OsirisClientError::CommandFailed(format!("{:?}", e)))?;
// Convert JobRunResponse to our RunJobResponse
Ok(RunJobResponse {
job_id: result.job_id,
status: result.status,
})
}
/// Execute a Rhai script template with variable substitution
pub async fn execute_template(&self, template: &str, variables: &HashMap<String, String>) -> Result<RunJobResponse, OsirisClientError> {
let script = substitute_variables(template, variables);
self.execute_script(&script).await
}
// ========== Supervisor-specific CQRS Methods ==========
/// Create an API key (Command - via Rhai)
pub async fn create_api_key(&self, key: String, name: String, scope: String) -> Result<RunJobResponse, OsirisClientError> {
let script = format!(
r#"
let api_key = new_api_key("{}", "{}", "{}", "{}");
save_api_key(api_key);
"#,
self.get_namespace(),
key,
name,
scope
);
self.execute_script(&script).await
}
/// Get an API key by key value (Query - via REST)
pub async fn get_api_key(&self, key: &str) -> Result<Option<serde_json::Value>, OsirisClientError> {
// Query by indexed field
let results: Vec<serde_json::Value> = self.query("ApiKey", &format!("key={}", key)).await?;
Ok(results.into_iter().next())
}
/// List all API keys (Query - via REST)
pub async fn list_api_keys(&self) -> Result<Vec<serde_json::Value>, OsirisClientError> {
self.list("ApiKey").await
}
/// Delete an API key (Command - via Rhai)
pub async fn delete_api_key(&self, key: String) -> Result<RunJobResponse, OsirisClientError> {
let script = format!(
r#"
delete_api_key("{}");
"#,
key
);
self.execute_script(&script).await
}
/// Create a runner (Command - via Rhai)
pub async fn create_runner(&self, runner_id: String, name: String, queue: String, registered_by: String) -> Result<RunJobResponse, OsirisClientError> {
let script = format!(
r#"
let runner = new_runner("{}", "{}", "{}", "{}", "{}");
save_runner(runner);
"#,
self.get_namespace(),
runner_id,
name,
queue,
registered_by
);
self.execute_script(&script).await
}
/// Get a runner by ID (Query - via REST)
pub async fn get_runner(&self, runner_id: &str) -> Result<Option<serde_json::Value>, OsirisClientError> {
let results: Vec<serde_json::Value> = self.query("Runner", &format!("runner_id={}", runner_id)).await?;
Ok(results.into_iter().next())
}
/// List all runners (Query - via REST)
pub async fn list_runners(&self) -> Result<Vec<serde_json::Value>, OsirisClientError> {
self.list("Runner").await
}
/// Delete a runner (Command - via Rhai)
pub async fn delete_runner(&self, runner_id: String) -> Result<RunJobResponse, OsirisClientError> {
let script = format!(
r#"
delete_runner("{}");
"#,
runner_id
);
self.execute_script(&script).await
}
/// Create job metadata (Command - via Rhai)
pub async fn create_job_metadata(&self, job_id: String, runner: String, created_by: String, payload: String) -> Result<RunJobResponse, OsirisClientError> {
let script = format!(
r#"
let job = new_job_metadata("{}", "{}", "{}", "{}", "{}");
save_job_metadata(job);
"#,
self.get_namespace(),
job_id,
runner,
created_by,
payload
);
self.execute_script(&script).await
}
/// Get job metadata by ID (Query - via REST)
pub async fn get_job_metadata(&self, job_id: &str) -> Result<Option<serde_json::Value>, OsirisClientError> {
let results: Vec<serde_json::Value> = self.query("JobMetadata", &format!("job_id={}", job_id)).await?;
Ok(results.into_iter().next())
}
/// List all job metadata (Query - via REST)
pub async fn list_job_metadata(&self) -> Result<Vec<serde_json::Value>, OsirisClientError> {
self.list("JobMetadata").await
}
/// List jobs by runner (Query - via REST)
pub async fn list_jobs_by_runner(&self, runner: &str) -> Result<Vec<serde_json::Value>, OsirisClientError> {
self.query("JobMetadata", &format!("runner={}", runner)).await
}
/// List jobs by creator (Query - via REST)
pub async fn list_jobs_by_creator(&self, creator: &str) -> Result<Vec<serde_json::Value>, OsirisClientError> {
self.query("JobMetadata", &format!("created_by={}", creator)).await
}
// Helper method to get namespace
fn get_namespace(&self) -> &str {
"supervisor"
}
}
// ========== Helper Structures ==========
#[derive(Serialize)]
struct RunJobRequest {
runner_name: String,
script: String,
timeout: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
env: Option<HashMap<String, String>>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct RunJobResponse {
pub job_id: String,
pub status: String,
}
/// Helper function to substitute variables in a Rhai script template
pub fn substitute_variables(template: &str, variables: &HashMap<String, String>) -> String {
let mut result = template.to_string();
for (key, value) in variables {
let placeholder = format!("{{{{ {} }}}}", key);
result = result.replace(&placeholder, value);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_creation() {
let client = OsirisClient::new("http://localhost:8080");
assert_eq!(client.osiris_url, "http://localhost:8080");
}
#[test]
fn test_builder() {
let client = OsirisClient::builder()
.osiris_url("http://localhost:8081")
.supervisor_url("http://localhost:3030")
.supervisor_secret("test_secret")
.runner_name("osiris")
.build()
.unwrap();
assert_eq!(client.osiris_url, "http://localhost:8081");
assert_eq!(client.supervisor_url, Some("http://localhost:3030".to_string()));
assert_eq!(client.runner_name, "osiris");
}
#[test]
fn test_substitute_variables() {
let template = "let x = {{ value }}; let y = {{ name }};";
let mut vars = HashMap::new();
vars.insert("value".to_string(), "42".to_string());
vars.insert("name".to_string(), "\"test\"".to_string());
let result = substitute_variables(template, &vars);
assert_eq!(result, "let x = 42; let y = \"test\";");
}
}

View File

@@ -0,0 +1,39 @@
//! Payment query methods
use serde::{Deserialize, Serialize};
use crate::{OsirisClient, OsirisClientError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Payment {
pub id: String,
pub amount: f64,
pub currency: String,
pub status: PaymentStatus,
pub description: String,
pub payment_url: Option<String>,
pub created_at: i64,
pub updated_at: i64,
pub completed_at: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PaymentStatus {
Pending,
Processing,
Completed,
Failed,
Cancelled,
}
impl OsirisClient {
/// Get payment by ID
pub async fn get_payment(&self, payment_id: &str) -> Result<Payment, OsirisClientError> {
self.get("payment", payment_id).await
}
/// List all payments
pub async fn list_payments(&self) -> Result<Vec<Payment>, OsirisClientError> {
self.list("payment").await
}
}

View File

@@ -0,0 +1,37 @@
// KYC verification script template
// Variables: {{resident_id}}, {{callback_url}}
print("=== Starting KYC Verification ===");
print("Resident ID: {{resident_id}}");
// Get freezone context
let freezone_pubkey = "04e58314c13ea3f9caed882001a5090797b12563d5f9bbd7f16efe020e060c780b446862311501e2e9653416527d2634ff8a8050ff3a085baccd7ddcb94185ff56";
let freezone_ctx = get_context([freezone_pubkey]);
// Get KYC client from context
let kyc_client = freezone_ctx.get("kyc_client");
if kyc_client == () {
print("ERROR: KYC client not configured");
return #{
success: false,
error: "KYC client not configured"
};
}
// Create KYC session
let session = kyc_client.create_session(
"{{resident_id}}",
"{{callback_url}}"
);
print("✓ KYC session created");
print(" Session ID: " + session.session_id);
print(" KYC URL: " + session.kyc_url);
// Return response
#{
success: true,
session_id: session.session_id,
kyc_url: session.kyc_url,
expires_at: session.expires_at
}