/// OSIRIS Rhai Engine /// /// Creates a Rhai engine configured with OSIRIS contexts and methods. use crate::context::OsirisContext; use crate::objects::note::rhai::register_note_functions; use crate::objects::event::rhai::register_event_functions; use crate::objects::heroledger::rhai::register_heroledger_modules; use crate::objects::kyc::rhai::register_kyc_modules; use crate::objects::flow::rhai::register_flow_modules; use crate::objects::communication::rhai::register_communication_modules; use crate::objects::money::rhai::register_money_modules; use crate::objects::legal::rhai::register_legal_modules; use rhai::{Engine, def_package, FuncRegistration}; use rhai::packages::{Package, StandardPackage}; /// Register get_context function in a Rhai engine with signatory-based access control /// /// Simple logic: /// - Context is a list of public keys (participants) /// - To get_context, at least one participant must be a signatory /// - No state tracking, no caching - creates fresh context each time pub fn register_context_api(engine: &mut rhai::Engine) { // Register get_context function with signatory-based access control // Usage: get_context(['pk1', 'pk2', 'pk3']) engine.register_fn("get_context", move |context: rhai::NativeCallContext, participants: rhai::Array| -> Result> { // Extract SIGNATORIES from context tag let tag_map = context .tag() .and_then(|tag| tag.read_lock::()) .ok_or_else(|| Box::new(rhai::EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?; let signatories_dynamic = tag_map.get("SIGNATORIES") .ok_or_else(|| Box::new(rhai::EvalAltResult::ErrorRuntime("'SIGNATORIES' not found in context tag Map.".into(), context.position())))?; // Convert SIGNATORIES array to Vec let signatories_array = signatories_dynamic.clone().into_array() .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("SIGNATORIES must be an array: {}", e).into(), context.position())))?; let signatories: Vec = signatories_array.into_iter() .map(|s| s.into_string()) .collect::, _>>() .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("SIGNATORIES must contain strings: {}", e).into(), context.position())))?; // Convert participants array to Vec let participant_keys: Vec = participants.into_iter() .map(|p| p.into_string()) .collect::, _>>() .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("Participants must be strings: {}", e).into(), context.position())))?; // Verify at least one participant is a signatory let has_signatory = participant_keys.iter().any(|p| signatories.contains(p)); if !has_signatory { return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("Access denied: none of the participants are signatories. Signatories: {}", signatories.join(", ")).into(), context.position() ))); } // Create context directly with participants OsirisContext::builder() .participants(participant_keys) .build() .map_err(|e| format!("Failed to create context: {}", e).into()) }); } // Define the OSIRIS package def_package! { /// OSIRIS package with all OSIRIS types and functions pub OsirisPackage(module) : StandardPackage { // Register OsirisContext type with all its methods module.set_custom_type::("OsirisContext"); // Register OsirisContext methods FuncRegistration::new("participants") .set_into_module(module, |ctx: &mut OsirisContext| ctx.participants()); FuncRegistration::new("context_id") .set_into_module(module, |ctx: &mut OsirisContext| ctx.context_id()); // Typed save methods - all named "save" for function overloading using generic save_object FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, note: crate::objects::Note| ctx.save_object(note)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, event: crate::objects::Event| ctx.save_object(event)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, user: crate::objects::heroledger::user::User| ctx.save_object(user)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, group: crate::objects::heroledger::group::Group| ctx.save_object(group)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, account: crate::objects::heroledger::money::Account| ctx.save_object(account)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, zone: crate::objects::heroledger::dnsrecord::DNSZone| ctx.save_object(zone)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, kyc_info: crate::objects::KycInfo| ctx.save_object(kyc_info)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, kyc_session: crate::objects::KycSession| ctx.save_object(kyc_session)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, flow_template: crate::objects::FlowTemplate| ctx.save_object(flow_template)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, flow_instance: crate::objects::FlowInstance| ctx.save_object(flow_instance)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, verification: crate::objects::Verification| ctx.save_object(verification)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, account: crate::objects::Account| ctx.save_object(account)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, asset: crate::objects::Asset| ctx.save_object(asset)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, transaction: crate::objects::Transaction| ctx.save_object(transaction)); FuncRegistration::new("save") .set_into_module(module, |ctx: &mut OsirisContext, contract: crate::objects::Contract| ctx.save_object(contract)); FuncRegistration::new("list") .set_into_module(module, |ctx: &mut OsirisContext, collection: String| ctx.list(collection)); FuncRegistration::new("get") .set_into_module(module, |ctx: &mut OsirisContext, collection: String, id: String| ctx.get(collection, id)); FuncRegistration::new("delete") .set_into_module(module, |ctx: &mut OsirisContext, collection: String, id: String| ctx.delete(collection, id)); // Register Note functions register_note_functions(module); // Register Event functions register_event_functions(module); // Register HeroLedger modules (User, Group, Account, DNSZone) register_heroledger_modules(module); // Register KYC modules (KycClient, KycSession) register_kyc_modules(module); // Register Flow modules (FlowTemplate, FlowInstance) register_flow_modules(module); // Register Communication modules (Verification, EmailClient) register_communication_modules(module); // Register Money modules (Account, Asset, Transaction, PaymentClient) register_money_modules(module); // Register Legal modules (Contract) register_legal_modules(module); // Register get_context function with signatory-based access control FuncRegistration::new("get_context") .set_into_module(module, |context: rhai::NativeCallContext, participants: rhai::Array| -> Result> { // Extract SIGNATORIES from context tag let tag_map = context .tag() .and_then(|tag| tag.read_lock::()) .ok_or_else(|| Box::new(rhai::EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?; let signatories_dynamic = tag_map.get("SIGNATORIES") .ok_or_else(|| Box::new(rhai::EvalAltResult::ErrorRuntime("'SIGNATORIES' not found in context tag Map.".into(), context.position())))?; // Convert SIGNATORIES array to Vec let signatories_array = signatories_dynamic.clone().into_array() .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("SIGNATORIES must be an array: {}", e).into(), context.position())))?; let signatories: Vec = signatories_array.into_iter() .map(|s| s.into_string()) .collect::, _>>() .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("SIGNATORIES must contain strings: {}", e).into(), context.position())))?; // Convert participants array to Vec let participant_keys: Vec = participants.into_iter() .map(|p| p.into_string()) .collect::, _>>() .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("Participants must be strings: {}", e).into(), context.position())))?; // Verify at least one participant is a signatory let has_signatory = participant_keys.iter().any(|p| signatories.contains(p)); if !has_signatory { return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("Access denied: none of the participants are signatories").into(), context.position() ))); } // Create context directly with participants OsirisContext::builder() .participants(participant_keys) .build() .map_err(|e| format!("Failed to create context: {}", e).into()) }); } } /// Register all OSIRIS components into an engine /// This is a convenience function that registers the complete OsirisPackage pub fn register_osiris_full(engine: &mut Engine) { let package = OsirisPackage::new(); package.register_into_engine(engine); } /// Create a single OSIRIS engine (for backward compatibility) pub fn create_osiris_engine() -> Result> { let mut engine = Engine::new_raw(); register_osiris_full(&mut engine); Ok(engine) } #[cfg(test)] mod tests { use super::*; #[test] fn test_create_osiris_engine() { let result = create_osiris_engine(); assert!(result.is_ok()); let mut engine = result.unwrap(); // Set up context tags with SIGNATORIES (like in runner_rust example) let mut tag_map = rhai::Map::new(); // Create a proper Rhai array let signatories: rhai::Array = vec![ rhai::Dynamic::from("pk1".to_string()), rhai::Dynamic::from("pk2".to_string()), rhai::Dynamic::from("pk3".to_string()), ]; tag_map.insert("SIGNATORIES".into(), rhai::Dynamic::from(signatories)); tag_map.insert("DB_PATH".into(), "/tmp/test_db".to_string().into()); tag_map.insert("CONTEXT_ID".into(), "test_context".to_string().into()); engine.set_default_tag(rhai::Dynamic::from(tag_map)); // Test get_context with valid signatories let mut scope = rhai::Scope::new(); let test_result = engine.eval_with_scope::( &mut scope, r#" // All participants must be signatories let ctx = get_context(["pk1", "pk2"]); ctx.context_id() "# ); if let Err(ref e) = test_result { eprintln!("Test error: {}", e); } assert!(test_result.is_ok(), "Failed to get context: {:?}", test_result.err()); assert_eq!(test_result.unwrap().to_string(), "pk1,pk2"); } #[test] fn test_engine_with_manager_access_denied() { let result = create_osiris_engine(); assert!(result.is_ok()); let mut engine = result.unwrap(); // Set up context tags with SIGNATORIES let mut tag_map = rhai::Map::new(); // Create a proper Rhai array let signatories: rhai::Array = vec![ rhai::Dynamic::from("pk1".to_string()), rhai::Dynamic::from("pk2".to_string()), ]; tag_map.insert("SIGNATORIES".into(), rhai::Dynamic::from(signatories)); tag_map.insert("DB_PATH".into(), "/tmp/test_db".to_string().into()); tag_map.insert("CONTEXT_ID".into(), "test_context".to_string().into()); engine.set_default_tag(rhai::Dynamic::from(tag_map)); // Test get_context with invalid participant (not a signatory) let mut scope = rhai::Scope::new(); let test_result = engine.eval_with_scope::( &mut scope, r#" // pk3 is not a signatory, should fail let ctx = get_context(["pk1", "pk3"]); ctx.context_id() "# ); // Should fail because pk3 is not in SIGNATORIES assert!(test_result.is_err()); let err_msg = test_result.unwrap_err().to_string(); assert!(err_msg.contains("Access denied") || err_msg.contains("not a signatory")); } }