final db models wip

This commit is contained in:
timurgordon 2025-06-03 21:51:21 +03:00
parent abbed9a1a1
commit c3f6b91aa0
13 changed files with 1251 additions and 11 deletions

View File

@ -8,16 +8,6 @@ pub trait BaseModelDataOps: Sized {
self self
} }
fn set_base_created_at(mut self, time: i64) -> Self {
self.get_base_data_mut().created_at = time;
self
}
fn set_base_modified_at(mut self, time: i64) -> Self {
self.get_base_data_mut().modified_at = time;
self
}
fn add_base_comment(mut self, comment_id: u32) -> Self { fn add_base_comment(mut self, comment_id: u32) -> Self {
self.get_base_data_mut().comments.push(comment_id); self.get_base_data_mut().comments.push(comment_id);
self self

View File

@ -118,7 +118,7 @@ pub trait Index {
/// // Retrieve the model with the assigned ID /// // Retrieve the model with the assigned ID
/// let db_user = db.collection().get_by_id(user_id).expect("Failed to get user"); /// let db_user = db.collection().get_by_id(user_id).expect("Failed to get user");
/// ``` /// ```
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
pub struct BaseModelData { pub struct BaseModelData {
/// Unique incremental ID - will be auto-generated by OurDB /// Unique incremental ID - will be auto-generated by OurDB
/// ///

151
prompts/new_rhai_rs_gen.md Normal file
View File

@ -0,0 +1,151 @@
# AI Prompt: Generate Rhai Module Integration for Rust Models
## Context
You are tasked with creating a Rhai scripting integration for Rust models in the heromodels crate. The integration should follow the builder pattern and provide a comprehensive set of functions for interacting with the models through Rhai scripts.
## Key Concepts to Understand
1. **Builder Pattern vs. Rhai Mutation**: Rust models use builder pattern (methods consume self and return Self), but Rhai requires functions that mutate in place (&mut self).
2. **Database Access**: Database functions are exposed as global functions in a 'db' module namespace (e.g., `db::save_flow`, not `db.save_flow`).
3. **ID Handling**: New objects should use ID 0 to allow database assignment of real IDs.
4. **Function vs. Method Calls**: Rhai scripts must use function calls (e.g., `status(flow, STATUS_DRAFT)`) not method chaining (e.g., `flow.status(STATUS_DRAFT)`).
## Requirements
1. **Module Structure and Imports**:
- Create a `rhai.rs` file that exports a function named `register_[model]_rhai_module` which takes an Engine and an Arc<OurDB>
- **IMPORTANT**: Import `heromodels_core::Model` trait for access to methods like `get_id`
- Use the `#[export_module]` macro to define the Rhai module
- Register the module globally with the engine using the correct module registration syntax
- Create separate database functions that capture the database reference
2. **Builder Pattern Integration**:
- For each model struct marked with `#[model]`, create constructor functions (e.g., `new_[model]`) that accept ID 0 for new objects
- For each builder method in the model, create a corresponding Rhai function that:
- Takes a mutable reference to the model as first parameter
- Uses `std::mem::take` to get ownership (if the model implements Default) or `std::mem::replace` (if it doesn't)
- Calls the builder method on the owned object
- Assigns the result back to the mutable reference
- Returns a clone of the modified object
3. **Field Access**:
- **IMPORTANT**: Do NOT create getter methods for fields that can be accessed directly
- Use direct field access in example code (e.g., `flow.name`, not `flow.get_name()`)
- Only create getter functions for computed properties or when special conversion is needed
- Handle special types appropriately (e.g., convert timestamps, IDs, etc.)
4. **Database Functions**:
- For each model, implement the following database functions in the `db` module namespace:
- `db::save_[model]`: Save the model to the database and return the saved model with assigned ID
- `db::get_[model]_by_id`: Retrieve a model by ID
- `db::delete_[model]`: Delete a model from the database
- `db::list_[model]s`: List all models of this type
- **IMPORTANT**: Use `db::function_name` syntax in scripts, not `db.function_name`
- Handle errors properly with detailed error messages using debug format `{:?}` for error objects
- Convert between Rhai's i64 and Rust's u32/u64 types for IDs and timestamps
- Return saved objects from save functions to ensure proper ID handling
5. **Error Handling**:
- Use `Box<EvalAltResult>` for error returns
- Provide detailed error messages with proper position information
- **IMPORTANT**: Use `context.call_position()` not the deprecated `context.position()`
- Handle type conversions safely
- Format error messages with debug format for error objects: `format!("Error: {:?}", err)`
6. **Type Conversions**:
- Create helper functions for converting between Rhai and Rust types
- Handle special cases like enums with string representations
## Implementation Pattern
```rust
// Helper functions for type conversion
fn i64_to_u32(val: i64, position: Position, param_name: &str, function_name: &str) -> Result<u32, Box<EvalAltResult>> {
u32::try_from(val).map_err(|_| {
Box::new(EvalAltResult::ErrorArithmetic(
format!("{} must be a positive integer less than 2^32 in {}", param_name, function_name).into(),
position
))
})
}
// Model constructor
#[rhai_fn(name = "new_[model]")]
pub fn new_model() -> Model {
Model::new()
}
// Builder method integration
#[rhai_fn(name = "[method_name]", return_raw, global, pure)]
pub fn model_method(model: &mut Model, param: Type) -> Result<Model, Box<EvalAltResult>> {
let owned_model = std::mem::take(model); // or replace if Default not implemented
*model = owned_model.method_name(param);
Ok(model.clone())
}
// Getter method
#[rhai_fn(name = "[field]", global)]
pub fn get_model_field(model: &mut Model) -> Type {
model.field.clone()
}
// Database function
db_module.set_native_fn("save_[model]", move |model: Model| -> Result<Model, Box<EvalAltResult>> {
db_clone.set(&model)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error: {}", e).into(), Position::NONE)))
.map(|(_, model)| model)
});
```
## Important Notes
1. All models must follow the builder pattern where methods take ownership of `self` and return `Self`
2. If a model doesn't implement `Default`, use `std::mem::replace` instead of `std::mem::take`
3. Ensure proper error handling for all operations
4. Register all functions with appropriate Rhai names
5. Follow the naming conventions established in existing modules
6. Ensure all database operations are properly wrapped with error handling
7. **CRITICAL**: When creating example scripts, use function calls not method chaining
8. **CRITICAL**: Always use ID 0 for new objects in scripts to allow database assignment
9. **CRITICAL**: After saving objects, use the returned objects with assigned IDs for further operations
10. **CRITICAL**: In Rust examples, include the Model trait import and use direct field access
## Example Rhai Script
```rhai
// Create a new flow with ID 0 (database will assign real ID)
let flow = new_flow(0, "12345678-1234-1234-1234-123456789012");
name(flow, "Document Approval Flow");
status(flow, STATUS_DRAFT);
// Save flow to database and get saved object with assigned ID
let saved_flow = db::save_flow(flow);
print(`Flow saved to database with ID: ${get_flow_id(saved_flow)}`);
// Retrieve flow from database using assigned ID
let retrieved_flow = db::get_flow_by_id(get_flow_id(saved_flow));
```
## Example Rust Code
```rust
use heromodels_core::Model; // Import Model trait for get_id()
// Create flow using Rhai
let flow: Flow = engine.eval("new_flow(0, \"12345678-1234-1234-1234-123456789012\")").unwrap();
// Access fields directly
println!("Flow name: {}", flow.name);
// Save to database using Rhai function
let save_script = "fn save_it(f) { return db::save_flow(f); }";
let save_ast = engine.compile(save_script).unwrap();
let saved_flow: Flow = engine.call_fn(&mut scope, &save_ast, "save_it", (flow,)).unwrap();
println!("Saved flow ID: {}", saved_flow.get_id());
```
## Reference Implementations
See the existing implementations for examples:
1. `heromodels/src/models/calendar/rhai.rs` - Calendar module Rhai bindings
2. `heromodels/src/models/flow/rhai.rs` - Flow module Rhai bindings
3. `heromodels_rhai/examples/flow/flow_script.rhai` - Example Rhai script
4. `heromodels_rhai/examples/flow/example.rs` - Example Rust code

298
rhai_client_example/Cargo.lock generated Normal file
View File

@ -0,0 +1,298 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"const-random",
"getrandom 0.3.3",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.16",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "crunchy"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
dependencies = [
"portable-atomic",
]
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rhai"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
dependencies = [
"ahash",
"bitflags",
"instant",
"num-traits",
"once_cell",
"rhai_codegen",
"smallvec",
"smartstring",
"thin-vec",
]
[[package]]
name = "rhai_client_example"
version = "0.1.0"
dependencies = [
"rhai",
"rhai_client_macros",
]
[[package]]
name = "rhai_client_macros"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"rhai",
"syn",
]
[[package]]
name = "rhai_codegen"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"static_assertions",
"version_check",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thin-vec"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "zerocopy"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@ -0,0 +1,8 @@
[package]
name = "rhai_client_example"
version = "0.1.0"
edition = "2021"
[dependencies]
rhai = "1.21.0"
rhai_client_macros = { path = "../rhai_client_macros" }

View File

@ -0,0 +1,62 @@
use rhai::Engine;
use rhai_client_macros::rhai;
// Define a Rust function with the #[rhai] attribute
#[rhai]
fn hello(name: String) -> String {
format!("Hello, {}!", name)
}
// Define another function with multiple parameters
#[rhai]
fn add(a: i32, b: i32) -> i32 {
a + b
}
// Define a function with different parameter types
#[rhai]
fn format_person(name: String, age: i32, is_student: bool) -> String {
let student_status = if is_student { "is" } else { "is not" };
format!("{} is {} years old and {} a student.", name, age, student_status)
}
// Define adapter functions for Rhai that handle i64 to i32 conversion
fn add_rhai(a: i64, b: i64) -> i64 {
add(a as i32, b as i32) as i64
}
fn format_person_rhai(name: String, age: i64, is_student: bool) -> String {
format_person(name, age as i32, is_student)
}
fn main() {
// Create a Rhai engine
let mut engine = Engine::new();
// Register our functions with the Rhai engine
// Note: Rhai uses i64 for integers by default
engine.register_fn("hello", hello);
engine.register_fn("add", add_rhai);
engine.register_fn("format_person", format_person_rhai);
println!("=== Calling Rust functions directly ===");
println!("{}", hello("World".to_string()));
println!("{}", add(5, 10));
println!("{}", format_person("Alice".to_string(), 25, true));
println!("\n=== Calling functions through Rhai engine ===");
let result1: String = engine.eval("hello(\"Rhai World\")").unwrap();
println!("{}", result1);
let result2: i64 = engine.eval("add(20, 30)").unwrap();
println!("{}", result2);
let result3: String = engine.eval("format_person(\"Bob\", 30, false)").unwrap();
println!("{}", result3);
println!("\n=== Calling functions through generated Rhai client functions ===");
// Use the generated client functions
println!("{}", hello_rhai_client(&engine, "Client World".to_string()));
println!("{}", add_rhai_client(&engine, 100, 200));
println!("{}", format_person_rhai_client(&engine, "Charlie".to_string(), 35, true));
}

3
rust-toolchain.toml Normal file
View File

@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"
components = ["rustfmt", "clippy"]

401
sigsocket_architecture.md Normal file
View File

@ -0,0 +1,401 @@
# SigSocket: WebSocket Signing Server Architecture
Based on my analysis of the existing Actix application structure, I've designed a comprehensive architecture for implementing a WebSocket server that handles signing operations. This server will integrate seamlessly with the existing hostbasket application.
## 1. Overview
SigSocket will:
- Accept WebSocket connections from clients
- Allow clients to identify themselves with a public key
- Provide a `send_to_sign()` function that takes a public key and a message
- Forward the message to the appropriate client for signing
- Wait for a signed response (with a 1-minute timeout)
- Verify the signature using the client's public key
- Return the response message and signature
## 2. Component Architecture
```mermaid
graph TD
A[Actix Web Server] --> B[SigSocket Manager]
B --> C[Connection Registry]
B --> D[Message Handler]
D --> E[Signature Verifier]
F[Client] <--> B
G[Application Code] --> H[SigSocketService]
H --> B
```
### Key Components:
1. **SigSocket Manager**
- Handles WebSocket connections
- Manages connection lifecycle
- Routes messages to appropriate handlers
2. **Connection Registry**
- Maps public keys to active WebSocket connections
- Handles connection tracking and cleanup
- Provides lookup functionality
3. **Message Handler**
- Processes incoming messages
- Implements the message protocol
- Manages timeouts for responses
4. **Signature Verifier**
- Verifies signatures using public keys
- Implements cryptographic operations
- Ensures security of the signing process
5. **SigSocket Service**
- Provides a clean API for the application to use
- Abstracts WebSocket complexity from business logic
- Handles error cases and timeouts
## 3. Directory Structure
```
src/
├── lib.rs # Main library exports
├── manager.rs # WebSocket connection manager
├── registry.rs # Connection registry
├── handler.rs # Message handling logic
├── protocol.rs # Message protocol definitions
├── crypto.rs # Cryptographic operations
└── service.rs # Service API
```
## 4. Data Flow
```mermaid
sequenceDiagram
participant Client
participant SigSocketManager
participant Registry
participant Application
participant SigSocketService
Client->>SigSocketManager: Connect
Client->>SigSocketManager: Introduce(public_key)
SigSocketManager->>Registry: Register(connection, public_key)
Application->>SigSocketService: send_to_sign(public_key, message)
SigSocketService->>Registry: Lookup(public_key)
Registry-->>SigSocketService: connection
SigSocketService->>SigSocketManager: Send message to connection
SigSocketManager->>Client: Message to sign
Client->>SigSocketManager: Signed response
SigSocketManager->>SigSocketService: Forward response
SigSocketService->>SigSocketService: Verify signature
SigSocketService-->>Application: Return verified response
```
## 5. Message Protocol
We'll define a minimalist protocol for communication:
```
// Client introduction (first message upon connection)
<base64_encoded_public_key>
// Sign request (sent from server to client)
<base64_encoded_message>
// Sign response (sent from client to server)
<base64_encoded_message>.<base64_encoded_signature>
```
This simplified protocol reduces overhead and complexity:
- The introduction is always the first message, containing only the public key
- Sign requests contain only the message to be signed
- Responses use a simple "message.signature" format
## 6. Required Dependencies
We'll need to add the following dependencies to the project:
```toml
# WebSocket support
actix-web-actors = "4.2.0"
# Cryptography
secp256k1 = "0.28.0" # For secp256k1 signatures (used in Bitcoin/Ethereum)
sha2 = "0.10.8" # For hashing before signing
hex = "0.4.3" # For hex encoding/decoding
base64 = "0.21.0" # For base64 encoding/decoding
rand = "0.8.5" # For generating random data
```
## 7. Implementation Details
### 7.1 SigSocket Manager
The SigSocket Manager will handle the lifecycle of WebSocket connections:
```rust
pub struct SigSocketManager {
registry: Arc<RwLock<ConnectionRegistry>>,
}
impl Actor for SigSocketManager {
type Context = ws::WebsocketContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
// Handle connection start
}
fn stopped(&mut self, ctx: &mut Self::Context) {
// Handle connection close and cleanup
}
}
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for SigSocketManager {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
// Handle different types of WebSocket messages
}
}
```
### 7.2 Connection Registry
The Connection Registry will maintain a mapping of public keys to active connections:
```rust
pub struct ConnectionRegistry {
connections: HashMap<String, Addr<SigSocketManager>>,
}
impl ConnectionRegistry {
pub fn register(&mut self, public_key: String, addr: Addr<SigSocketManager>) {
self.connections.insert(public_key, addr);
}
pub fn unregister(&mut self, public_key: &str) {
self.connections.remove(public_key);
}
pub fn get(&self, public_key: &str) -> Option<&Addr<SigSocketManager>> {
self.connections.get(public_key)
}
}
```
### 7.3 SigSocket Service
The SigSocket Service will provide a clean API for controllers:
```rust
pub struct SigSocketService {
registry: Arc<RwLock<ConnectionRegistry>>,
}
impl SigSocketService {
pub async fn send_to_sign(&self, public_key: &str, message: &[u8])
-> Result<(Vec<u8>, Vec<u8>), SigSocketError> {
// 1. Find the connection for the public key
let connection = self.registry.read().await.get(public_key).cloned();
if let Some(conn) = connection {
// 2. Create a response channel
let (tx, rx) = oneshot::channel();
// 3. Register the response channel with the connection
self.pending_requests.write().await.insert(conn.clone(), tx);
// 4. Send the message to the client (just the raw message)
conn.do_send(base64::encode(message));
// 5. Wait for the response with a timeout
match tokio::time::timeout(Duration::from_secs(60), rx).await {
Ok(Ok(response)) => {
// 6. Parse the response in format "message.signature"
let parts: Vec<&str> = response.split('.').collect();
if parts.len() != 2 {
return Err(SigSocketError::InvalidResponseFormat);
}
let message_b64 = parts[0];
let signature_b64 = parts[1];
// 7. Decode the message and signature
let message_bytes = match base64::decode(message_b64) {
Ok(m) => m,
Err(_) => return Err(SigSocketError::DecodingError),
};
let signature_bytes = match base64::decode(signature_b64) {
Ok(s) => s,
Err(_) => return Err(SigSocketError::DecodingError),
};
// 8. Verify the signature
if self.verify_signature(&signature_bytes, &message_bytes, public_key) {
Ok((message_bytes, signature_bytes))
} else {
Err(SigSocketError::InvalidSignature)
}
},
Ok(Err(_)) => Err(SigSocketError::ChannelClosed),
Err(_) => Err(SigSocketError::Timeout),
}
} else {
Err(SigSocketError::ConnectionNotFound)
}
}
}
```
## 8. Error Handling
We'll define a comprehensive error type for the SigSocket service:
```rust
#[derive(Debug, thiserror::Error)]
pub enum SigSocketError {
#[error("Connection not found for the provided public key")]
ConnectionNotFound,
#[error("Timeout waiting for signature")]
Timeout,
#[error("Invalid signature")]
InvalidSignature,
#[error("Channel closed unexpectedly")]
ChannelClosed,
#[error("Invalid response format, expected 'message.signature'")]
InvalidResponseFormat,
#[error("Error decoding base64 message or signature")]
DecodingError,
#[error("Invalid public key format")]
InvalidPublicKey,
#[error("Invalid signature format")]
InvalidSignature,
#[error("Internal cryptographic error")]
InternalError,
#[error("WebSocket error: {0}")]
WebSocketError(#[from] ws::ProtocolError),
#[error("Base64 decoding error: {0}")]
Base64Error(#[from] base64::DecodeError),
#[error("Hex decoding error: {0}")]
HexError(#[from] hex::FromHexError),
}
```
## 9. Cryptographic Operations
The SigSocket service uses the secp256k1 elliptic curve for cryptographic operations, which is the same curve used in Bitcoin and Ethereum. This makes it compatible with many blockchain applications and wallets.
```rust
use secp256k1::{Secp256k1, Message, PublicKey, Signature};
use sha2::{Sha256, Digest};
pub fn verify_signature(public_key_hex: &str, message: &[u8], signature_hex: &str) -> Result<bool, SigSocketError> {
// 1. Parse the public key
let public_key_bytes = hex::decode(public_key_hex)
.map_err(|_| SigSocketError::InvalidPublicKey)?;
let public_key = PublicKey::from_slice(&public_key_bytes)
.map_err(|_| SigSocketError::InvalidPublicKey)?;
// 2. Parse the signature
let signature_bytes = hex::decode(signature_hex)
.map_err(|_| SigSocketError::InvalidSignature)?;
let signature = Signature::from_compact(&signature_bytes)
.map_err(|_| SigSocketError::InvalidSignature)?;
// 3. Hash the message (secp256k1 requires a 32-byte hash)
let mut hasher = Sha256::new();
hasher.update(message);
let message_hash = hasher.finalize();
// 4. Create a secp256k1 message from the hash
let secp_message = Message::from_slice(&message_hash)
.map_err(|_| SigSocketError::InternalError)?;
// 5. Verify the signature
let secp = Secp256k1::verification_only();
match secp.verify(&secp_message, &signature, &public_key) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
```
This implementation:
1. Decodes the hex-encoded public key and signature
2. Hashes the message using SHA-256 (required for secp256k1)
3. Verifies the signature using the secp256k1 library
4. Returns a boolean indicating whether the signature is valid
## 10. Security Considerations
1. **Public Key Validation**: Validate public keys upon connection to ensure they are properly formatted secp256k1 keys
2. **Message Authentication**: Consider adding a nonce or timestamp to prevent replay attacks
3. **Rate Limiting**: Implement rate limiting to prevent DoS attacks
4. **Connection Timeouts**: Automatically close inactive connections
5. **Error Logging**: Log errors but avoid exposing sensitive information
6. **Input Validation**: Validate all inputs to prevent injection attacks
7. **Secure Hashing**: Always hash messages before signing to prevent length extension attacks
## 11. Testing Strategy
1. **Unit Tests**: Test individual components in isolation
2. **Integration Tests**: Test the interaction between components
3. **End-to-End Tests**: Test the complete flow from client connection to signature verification
4. **Load Tests**: Test the system under high load to ensure stability
5. **Security Tests**: Test for common security vulnerabilities
## 12. Integration with Existing Code
The SigSocketService can be used by any part of the application that needs signing functionality:
```rust
// Create and initialize the service
let registry = Arc::new(RwLock::new(ConnectionRegistry::new()));
let sigsocket_service = Arc::new(SigSocketService::new(registry.clone()));
// Use the service to send a message for signing
async fn sign_message(service: Arc<SigSocketService>, public_key: String, message: Vec<u8>) -> Result<(Vec<u8>, Vec<u8>), SigSocketError> {
service.send_to_sign(&public_key, &message).await
}
// Example usage in an HTTP handler
async fn handle_sign_request(
service: web::Data<Arc<SigSocketService>>,
req: web::Json<SignRequest>,
) -> HttpResponse {
match service.send_to_sign(&req.public_key, &req.message).await {
Ok((response, signature)) => {
HttpResponse::Ok().json(json!({
"response": base64::encode(response),
"signature": base64::encode(signature),
}))
},
Err(e) => {
HttpResponse::InternalServerError().json(json!({
"error": e.to_string(),
}))
}
}
}
```
## 13. Deployment Considerations
1. **Scalability**: The SigSocket server should be designed to scale horizontally
2. **Monitoring**: Implement metrics for connection count, message throughput, and error rates
3. **Logging**: Log important events for debugging and auditing
4. **Documentation**: Document the API and protocol for client implementers

327
websocket/architecture.md Normal file
View File

@ -0,0 +1,327 @@
# WebSocket Signing Server Architecture Plan
Based on my analysis of the existing Actix application structure, I've designed a comprehensive architecture for implementing a WebSocket server that handles signing operations. This server will integrate seamlessly with the existing hostbasket application.
## 1. Overview
The WebSocket Signing Server will:
- Accept WebSocket connections from clients
- Allow clients to identify themselves with a public key
- Provide a `send_to_sign()` function that takes a public key and a message
- Forward the message to the appropriate client for signing
- Wait for a signed response (with a 1-minute timeout)
- Verify the signature using the client's public key
- Return the response message and signature
## 2. Component Architecture
```mermaid
graph TD
A[Actix Web Server] --> B[WebSocket Manager]
B --> C[Connection Registry]
B --> D[Message Handler]
D --> E[Signature Verifier]
F[Client] <--> B
G[Controllers] --> H[SigningService]
H --> B
```
### Key Components:
1. **WebSocket Manager**
- Handles WebSocket connections
- Manages connection lifecycle
- Routes messages to appropriate handlers
2. **Connection Registry**
- Maps public keys to active WebSocket connections
- Handles connection tracking and cleanup
- Provides lookup functionality
3. **Message Handler**
- Processes incoming messages
- Implements the message protocol
- Manages timeouts for responses
4. **Signature Verifier**
- Verifies signatures using public keys
- Implements cryptographic operations
- Ensures security of the signing process
5. **Signing Service**
- Provides a clean API for controllers to use
- Abstracts WebSocket complexity from business logic
- Handles error cases and timeouts
## 3. Directory Structure
```
src/
├── websocket/
│ ├── mod.rs # Module exports
│ ├── manager.rs # WebSocket connection manager
│ ├── registry.rs # Connection registry
│ ├── handler.rs # Message handling logic
│ ├── protocol.rs # Message protocol definitions
│ ├── crypto.rs # Cryptographic operations
│ └── service.rs # Service API for controllers
├── controllers/
│ └── [existing controllers]
│ └── websocket.rs # WebSocket controller (if needed)
└── routes/
└── mod.rs # Updated to include WebSocket routes
```
## 4. Data Flow
```mermaid
sequenceDiagram
participant Client
participant WebSocketManager
participant Registry
participant Controller
participant SigningService
Client->>WebSocketManager: Connect
Client->>WebSocketManager: Introduce(public_key)
WebSocketManager->>Registry: Register(connection, public_key)
Controller->>SigningService: send_to_sign(public_key, message)
SigningService->>Registry: Lookup(public_key)
Registry-->>SigningService: connection
SigningService->>WebSocketManager: Send message to connection
WebSocketManager->>Client: Message to sign
Client->>WebSocketManager: Signed response
WebSocketManager->>SigningService: Forward response
SigningService->>SigningService: Verify signature
SigningService-->>Controller: Return verified response
```
## 5. Message Protocol
We'll define a simple JSON-based protocol for communication:
```json
// Client introduction
{
"type": "introduction",
"public_key": "base64_encoded_public_key"
}
// Sign request
{
"type": "sign_request",
"message": "base64_encoded_message",
"request_id": "unique_request_id"
}
// Sign response
{
"type": "sign_response",
"request_id": "unique_request_id",
"message": "base64_encoded_message",
"signature": "base64_encoded_signature"
}
```
## 6. Required Dependencies
We'll need to add the following dependencies to the project:
```toml
# WebSocket support
actix-web-actors = "4.2.0"
# Cryptography
ed25519-dalek = "2.0.0" # For Ed25519 signatures
base64 = "0.21.0" # For encoding/decoding
rand = "0.8.5" # For generating random data
```
## 7. Implementation Details
### 7.1 WebSocket Manager
The WebSocket Manager will handle the lifecycle of WebSocket connections:
```rust
pub struct WebSocketManager {
registry: Arc<RwLock<ConnectionRegistry>>,
}
impl Actor for WebSocketManager {
type Context = ws::WebsocketContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
// Handle connection start
}
fn stopped(&mut self, ctx: &mut Self::Context) {
// Handle connection close and cleanup
}
}
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketManager {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
// Handle different types of WebSocket messages
}
}
```
### 7.2 Connection Registry
The Connection Registry will maintain a mapping of public keys to active connections:
```rust
pub struct ConnectionRegistry {
connections: HashMap<String, Addr<WebSocketManager>>,
}
impl ConnectionRegistry {
pub fn register(&mut self, public_key: String, addr: Addr<WebSocketManager>) {
self.connections.insert(public_key, addr);
}
pub fn unregister(&mut self, public_key: &str) {
self.connections.remove(public_key);
}
pub fn get(&self, public_key: &str) -> Option<&Addr<WebSocketManager>> {
self.connections.get(public_key)
}
}
```
### 7.3 Signing Service
The Signing Service will provide a clean API for controllers:
```rust
pub struct SigningService {
registry: Arc<RwLock<ConnectionRegistry>>,
}
impl SigningService {
pub async fn send_to_sign(&self, public_key: &str, message: &[u8])
-> Result<(Vec<u8>, Vec<u8>), SigningError> {
// 1. Find the connection for the public key
let connection = self.registry.read().await.get(public_key).cloned();
if let Some(conn) = connection {
// 2. Generate a unique request ID
let request_id = Uuid::new_v4().to_string();
// 3. Create a response channel
let (tx, rx) = oneshot::channel();
// 4. Register the response channel
self.pending_requests.write().await.insert(request_id.clone(), tx);
// 5. Send the message to the client
let sign_request = SignRequest {
request_id: request_id.clone(),
message: message.to_vec(),
};
conn.do_send(sign_request);
// 6. Wait for the response with a timeout
match tokio::time::timeout(Duration::from_secs(60), rx).await {
Ok(Ok(response)) => {
// 7. Verify the signature
if self.verify_signature(&response.signature, message, public_key) {
Ok((response.message, response.signature))
} else {
Err(SigningError::InvalidSignature)
}
},
Ok(Err(_)) => Err(SigningError::ChannelClosed),
Err(_) => Err(SigningError::Timeout),
}
} else {
Err(SigningError::ConnectionNotFound)
}
}
}
```
## 8. Error Handling
We'll define a comprehensive error type for the signing service:
```rust
#[derive(Debug, thiserror::Error)]
pub enum SigningError {
#[error("Connection not found for the provided public key")]
ConnectionNotFound,
#[error("Timeout waiting for signature")]
Timeout,
#[error("Invalid signature")]
InvalidSignature,
#[error("Channel closed unexpectedly")]
ChannelClosed,
#[error("WebSocket error: {0}")]
WebSocketError(#[from] ws::ProtocolError),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
}
```
## 9. Security Considerations
1. **Public Key Validation**: Validate public keys upon connection to ensure they are properly formatted
2. **Message Authentication**: Consider adding a nonce or timestamp to prevent replay attacks
3. **Rate Limiting**: Implement rate limiting to prevent DoS attacks
4. **Connection Timeouts**: Automatically close inactive connections
5. **Error Logging**: Log errors but avoid exposing sensitive information
6. **Input Validation**: Validate all inputs to prevent injection attacks
## 10. Testing Strategy
1. **Unit Tests**: Test individual components in isolation
2. **Integration Tests**: Test the interaction between components
3. **End-to-End Tests**: Test the complete flow from client connection to signature verification
4. **Load Tests**: Test the system under high load to ensure stability
5. **Security Tests**: Test for common security vulnerabilities
## 11. Integration with Existing Controllers
Controllers can use the SigningService through dependency injection:
```rust
pub struct SomeController {
signing_service: Arc<SigningService>,
}
impl SomeController {
pub async fn some_action(&self, public_key: String, message: Vec<u8>) -> HttpResponse {
match self.signing_service.send_to_sign(&public_key, &message).await {
Ok((response, signature)) => {
HttpResponse::Ok().json(json!({
"response": base64::encode(response),
"signature": base64::encode(signature),
}))
},
Err(e) => {
HttpResponse::InternalServerError().json(json!({
"error": e.to_string(),
}))
}
}
}
}
```
## 12. Deployment Considerations
1. **Scalability**: The WebSocket server should be designed to scale horizontally
2. **Monitoring**: Implement metrics for connection count, message throughput, and error rates
3. **Logging**: Log important events for debugging and auditing
4. **Documentation**: Document the API and protocol for client implementers