From 93977bad7a3e4feaaa7b561e3806f96d7c50f848 Mon Sep 17 00:00:00 2001
From: Timur Gordon <31495328+timurgordon@users.noreply.github.com>
Date: Tue, 8 Jul 2025 22:49:47 +0200
Subject: [PATCH] implement stripe and idenfy webhooks support
---
.cargo/config.toml | 5 +
.gitignore | 3 +-
ARCHITECTURE.md | 460 +++
CONTRIBUTING.md | 27 +
Cargo.lock | 542 ++-
Cargo.toml | 10 +-
README.md | 23 +-
cmd/dispatcher.rs | 189 ++
docs/ARCHITECTURE.md | 137 -
docs/aidocs/IMPLEMENTATION_PLAN.md | 351 ++
docs/aidocs/README.md | 1 +
.../aidocs/URL_ROUTING_STRATEGY.md | 0
docs/aidocs/WSS_IMPLEMENTATION_PLAN.md | 382 +++
docs/design.md | 59 +
docs/rationale.md | 34 +
docs/system_requirements_specification.md | 50 +
examples/.gitkeep | 0
examples/ourworld/README.md | 68 -
examples/ourworld/circles.json | 51 -
examples/ourworld/main.rs | 90 -
examples/ourworld/ourworld_output.json | 65 -
.../ourworld/scripts/dunia_cybercity.rhai | 249 --
examples/ourworld/scripts/freezone.rhai | 249 --
examples/ourworld/scripts/geomind.rhai | 249 --
examples/ourworld/scripts/kristof.rhai | 15 -
examples/ourworld/scripts/mbweni.rhai | 249 --
examples/ourworld/scripts/ourworld.rhai | 257 --
examples/ourworld/scripts/sikana.rhai | 249 --
examples/ourworld/scripts/threefold.rhai | 249 --
examples/ourworld/scripts/timur.rhai | 15 -
examples/server_e2e_rhai_flow.rs | 2 +-
examples/server_timeout_demonstration.rs | 2 +-
examples/wss_auth_example.rs | 83 +
examples/wss_basic_example.rs | 69 +
examples/wss_demo/.gitignore | 7 +
examples/wss_demo/Cargo.lock | 2958 +++++++++++++++++
examples/wss_demo/Cargo.toml | 35 +
examples/wss_demo/README.md | 204 ++
examples/wss_demo/wss_client.rs | 94 +
examples/wss_demo/wss_server.rs | 91 +
examples/wss_test_client.rs | 136 +
index.html | 11 -
{src => research}/launcher/.gitignore | 0
{src => research}/launcher/ARCHITECTURE.md | 2 +-
{src => research}/launcher/Cargo.lock | 0
{src => research}/launcher/Cargo.toml | 19 +-
research/launcher/README.md | 254 ++
{src => research}/launcher/circles.json | 0
research/launcher/examples/README.md | 144 +
.../examples/circle_launcher_example.rs | 146 +
research/launcher/examples/cleanup_example.rs | 13 +
research/launcher/examples/confirm_launch.rs | 111 +
.../launcher/examples/ourworld/circles.json | 0
.../launcher/examples/ourworld/main.rs | 50 +-
.../ourworld/scripts/dunia_cybercity.rhai | 0
.../examples/ourworld/scripts/freezone.rhai | 0
.../examples/ourworld/scripts/geomind.rhai | 0
.../examples/ourworld/scripts/mbweni.rhai | 0
.../examples/ourworld/scripts/ourworld.rhai | 0
.../examples/ourworld/scripts/sikana.rhai | 0
.../examples/ourworld/scripts/threefold.rhai | 0
research/launcher/examples/test_circles.json | 5 +
research/launcher/examples/test_script.rhai | 13 +
research/launcher/src/builder.rs | 153 +
research/launcher/src/cmd/main.rs | 98 +
research/launcher/src/lib.rs | 236 ++
research/launcher/tests/spawn_test.rs | 173 +
src/app/Cargo.toml | 5 +-
.../views => _archive}/intelligence_view.rs | 2 +-
.../views => _archive}/publishing_view.rs | 0
{content => src/app/content}/intro.md | 0
src/app/src/app.rs | 22 -
.../src/components/inspector_network_tab.rs | 29 +-
src/app/src/components/nav_island.rs | 8 +-
.../src/components/network_animation_view.rs | 8 +-
src/app/src/rhai_executor.rs | 4 +-
src/app/src/views/mod.rs | 2 -
src/client_ws/Cargo.toml | 14 +-
src/client_ws/README.md | 118 +-
src/client_ws/cmd/README.md | 89 +
src/client_ws/cmd/main.rs | 249 ++
src/client_ws/src/lib.rs | 100 +-
src/launcher/.DS_Store | Bin 6148 -> 0 bytes
src/launcher/README.md | 75 -
src/launcher/examples/confirm_launch.rs | 114 -
src/launcher/examples/test_circles.json | 3 -
src/launcher/examples/test_script.rhai | 2 -
.../circle_db_test_circle.db/data/lookup/.inc | 1 -
.../circle_db_test_circle.db/data/lookup/data | Bin 4000000 -> 0 bytes
.../circle_db_test_circle.db/index/0.db | Bin 35 -> 0 bytes
.../index/lookup/.inc | 1 -
.../index/lookup/data | Bin 4000000 -> 0 bytes
src/launcher/src/cmd/main.rs | 49 -
src/launcher/src/lib.rs | 358 --
src/launcher/tests/spawn_test.rs | 120 -
src/server/.env.example | 10 +
src/server/.gitignore | 3 +
src/{server_ws => server}/Cargo.lock | 0
src/{server_ws => server}/Cargo.toml | 38 +-
src/server/README.md | 76 +
src/server/cmd/README.md | 142 +
src/server/cmd/main.rs | 132 +
src/server/docs/ARCHITECTURE.md | 133 +
.../docs/authentication.md} | 43 +-
src/server/docs/webhooks.md | 357 ++
src/{server_ws => server}/openrpc.json | 0
.../src/auth/auth_middleware.rs | 0
src/{server_ws => server}/src/auth/mod.rs | 0
.../src/auth/signature_verifier.rs | 0
src/{server_ws => server}/src/lib.rs | 283 +-
src/server/src/webhook/handlers/common.rs | 113 +
src/server/src/webhook/handlers/idenfy.rs | 237 ++
src/server/src/webhook/handlers/mod.rs | 6 +
src/server/src/webhook/handlers/stripe.rs | 235 ++
src/server/src/webhook/mod.rs | 31 +
src/server/src/webhook/types.rs | 87 +
src/server/src/webhook/verifiers.rs | 191 ++
.../tests/basic_integration_test.rs | 27 +-
.../tests/connection_test.rs | 17 +-
.../tests/timeout_integration_test.rs | 17 +-
src/server/tests/wss_integration_test.rs | 85 +
src/server_ws/.DS_Store | Bin 6148 -> 0 bytes
src/server_ws/.gitignore | 2 -
src/server_ws/ARCHITECTURE.md | 74 -
src/server_ws/README.md | 45 -
src/server_ws/cert.pem | 17 -
src/server_ws/cmd/main.rs | 52 -
src/server_ws/key.pem | 28 -
tests/end_to_end_integration.rs | 4 +-
129 files changed, 9655 insertions(+), 3640 deletions(-)
create mode 100644 .cargo/config.toml
create mode 100644 ARCHITECTURE.md
create mode 100644 CONTRIBUTING.md
create mode 100644 cmd/dispatcher.rs
delete mode 100644 docs/ARCHITECTURE.md
create mode 100644 docs/aidocs/IMPLEMENTATION_PLAN.md
create mode 100644 docs/aidocs/README.md
rename URL_ROUTING_STRATEGY.md => docs/aidocs/URL_ROUTING_STRATEGY.md (100%)
create mode 100644 docs/aidocs/WSS_IMPLEMENTATION_PLAN.md
create mode 100644 docs/design.md
create mode 100644 docs/rationale.md
create mode 100644 docs/system_requirements_specification.md
delete mode 100644 examples/.gitkeep
delete mode 100644 examples/ourworld/README.md
delete mode 100644 examples/ourworld/circles.json
delete mode 100644 examples/ourworld/main.rs
delete mode 100644 examples/ourworld/ourworld_output.json
delete mode 100644 examples/ourworld/scripts/dunia_cybercity.rhai
delete mode 100644 examples/ourworld/scripts/freezone.rhai
delete mode 100644 examples/ourworld/scripts/geomind.rhai
delete mode 100644 examples/ourworld/scripts/kristof.rhai
delete mode 100644 examples/ourworld/scripts/mbweni.rhai
delete mode 100644 examples/ourworld/scripts/ourworld.rhai
delete mode 100644 examples/ourworld/scripts/sikana.rhai
delete mode 100644 examples/ourworld/scripts/threefold.rhai
delete mode 100644 examples/ourworld/scripts/timur.rhai
create mode 100644 examples/wss_auth_example.rs
create mode 100644 examples/wss_basic_example.rs
create mode 100644 examples/wss_demo/.gitignore
create mode 100644 examples/wss_demo/Cargo.lock
create mode 100644 examples/wss_demo/Cargo.toml
create mode 100644 examples/wss_demo/README.md
create mode 100644 examples/wss_demo/wss_client.rs
create mode 100644 examples/wss_demo/wss_server.rs
create mode 100644 examples/wss_test_client.rs
delete mode 100644 index.html
rename {src => research}/launcher/.gitignore (100%)
rename {src => research}/launcher/ARCHITECTURE.md (95%)
rename {src => research}/launcher/Cargo.lock (100%)
rename {src => research}/launcher/Cargo.toml (75%)
create mode 100644 research/launcher/README.md
rename {src => research}/launcher/circles.json (100%)
create mode 100644 research/launcher/examples/README.md
create mode 100644 research/launcher/examples/circle_launcher_example.rs
create mode 100644 research/launcher/examples/cleanup_example.rs
create mode 100644 research/launcher/examples/confirm_launch.rs
rename {src => research}/launcher/examples/ourworld/circles.json (100%)
rename {src => research}/launcher/examples/ourworld/main.rs (64%)
rename {src => research}/launcher/examples/ourworld/scripts/dunia_cybercity.rhai (100%)
rename {src => research}/launcher/examples/ourworld/scripts/freezone.rhai (100%)
rename {src => research}/launcher/examples/ourworld/scripts/geomind.rhai (100%)
rename {src => research}/launcher/examples/ourworld/scripts/mbweni.rhai (100%)
rename {src => research}/launcher/examples/ourworld/scripts/ourworld.rhai (100%)
rename {src => research}/launcher/examples/ourworld/scripts/sikana.rhai (100%)
rename {src => research}/launcher/examples/ourworld/scripts/threefold.rhai (100%)
create mode 100644 research/launcher/examples/test_circles.json
create mode 100644 research/launcher/examples/test_script.rhai
create mode 100644 research/launcher/src/builder.rs
create mode 100644 research/launcher/src/cmd/main.rs
create mode 100644 research/launcher/src/lib.rs
create mode 100644 research/launcher/tests/spawn_test.rs
rename src/app/{src/views => _archive}/intelligence_view.rs (99%)
rename src/app/{src/views => _archive}/publishing_view.rs (100%)
rename {content => src/app/content}/intro.md (100%)
create mode 100644 src/client_ws/cmd/README.md
create mode 100644 src/client_ws/cmd/main.rs
delete mode 100644 src/launcher/.DS_Store
delete mode 100644 src/launcher/README.md
delete mode 100644 src/launcher/examples/confirm_launch.rs
delete mode 100644 src/launcher/examples/test_circles.json
delete mode 100644 src/launcher/examples/test_script.rhai
delete mode 100644 src/launcher/launch_data/circle_db_test_circle.db/data/lookup/.inc
delete mode 100644 src/launcher/launch_data/circle_db_test_circle.db/data/lookup/data
delete mode 100644 src/launcher/launch_data/circle_db_test_circle.db/index/0.db
delete mode 100644 src/launcher/launch_data/circle_db_test_circle.db/index/lookup/.inc
delete mode 100644 src/launcher/launch_data/circle_db_test_circle.db/index/lookup/data
delete mode 100644 src/launcher/src/cmd/main.rs
delete mode 100644 src/launcher/src/lib.rs
delete mode 100644 src/launcher/tests/spawn_test.rs
create mode 100644 src/server/.env.example
create mode 100644 src/server/.gitignore
rename src/{server_ws => server}/Cargo.lock (100%)
rename src/{server_ws => server}/Cargo.toml (60%)
create mode 100644 src/server/README.md
create mode 100644 src/server/cmd/README.md
create mode 100644 src/server/cmd/main.rs
create mode 100644 src/server/docs/ARCHITECTURE.md
rename src/{server_ws/README_AUTH.md => server/docs/authentication.md} (85%)
create mode 100644 src/server/docs/webhooks.md
rename src/{server_ws => server}/openrpc.json (100%)
rename src/{server_ws => server}/src/auth/auth_middleware.rs (100%)
rename src/{server_ws => server}/src/auth/mod.rs (100%)
rename src/{server_ws => server}/src/auth/signature_verifier.rs (100%)
rename src/{server_ws => server}/src/lib.rs (67%)
create mode 100644 src/server/src/webhook/handlers/common.rs
create mode 100644 src/server/src/webhook/handlers/idenfy.rs
create mode 100644 src/server/src/webhook/handlers/mod.rs
create mode 100644 src/server/src/webhook/handlers/stripe.rs
create mode 100644 src/server/src/webhook/mod.rs
create mode 100644 src/server/src/webhook/types.rs
create mode 100644 src/server/src/webhook/verifiers.rs
rename src/{server_ws => server}/tests/basic_integration_test.rs (78%)
rename src/{server_ws => server}/tests/connection_test.rs (59%)
rename src/{server_ws => server}/tests/timeout_integration_test.rs (88%)
create mode 100644 src/server/tests/wss_integration_test.rs
delete mode 100644 src/server_ws/.DS_Store
delete mode 100644 src/server_ws/.gitignore
delete mode 100644 src/server_ws/ARCHITECTURE.md
delete mode 100644 src/server_ws/README.md
delete mode 100644 src/server_ws/cert.pem
delete mode 100644 src/server_ws/cmd/main.rs
delete mode 100644 src/server_ws/key.pem
diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..62c2576
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,5 @@
+[env]
+# Set the C compiler for the wasm32 target. This ensures that build scripts
+# which compile C code (like the one in secp256k1-sys) use the correct
+# version of clang that can target WebAssembly.
+CC_wasm32-unknown-unknown = "/opt/homebrew/opt/llvm/bin/clang"
diff --git a/.gitignore b/.gitignore
index c40214e..daa26b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@ target
dump.rdb
worker_rhai_temp_db
launch_data
-.DS_Store
\ No newline at end of file
+.DS_Store
+.env
\ No newline at end of file
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 0000000..3925fa5
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,460 @@
+# Circles: Peer-Centric Application Layer Architecture
+
+This document provides a comprehensive overview of the Circles project architecture - a **distributed, peer-contextual application layer runtime** where peers can send scripts to each other for execution within isolated contexts.
+
+## 1. System Overview
+
+### 1.1. Core Concept
+
+Circles implements a **peer-centric backend architecture** where each user is treated as an autonomous execution context rather than a passive database entry. The system enables **cross-peer script execution** where:
+
+- **Peer A** sends a script to **Peer B**
+- The script executes within **Peer B's context** (filesystem namespace, data isolation)
+- The script runs with awareness that **Peer A** initiated the execution
+- **Peer B** controls what scripts it accepts and how they execute
+
+### 1.2. Key Characteristics
+
+- **Peer-Centric Execution**: Each peer is an isolated context, not a row in a global database
+- **Context-Based Routing**: Execution flows are scoped by peer identity (public keys)
+- **Decentralizable**: Circle nodes can be deployed independently and serve separate peer sets
+- **Script-Triggered Workflows**: Cross-peer interactions happen via script execution
+- **Namespace Isolation**: Workers enforce isolation through filesystem namespacing
+
+## 2. Core Components
+
+### 2.1. Peer Context
+
+A **peer context** represents an isolated execution environment identified by a secp256k1 public key:
+
+- **Identity**: Unique secp256k1 public key
+- **Filesystem Namespace**: Isolated root directory (`DB_PATH/{peer_public_key}/`)
+- **Execution Environment**: Rhai script engine with peer-specific variables
+- **Policy Boundaries**: Controls which peers can execute scripts and what operations are allowed
+
+### 2.2. Circle (WebSocket Gateway)
+
+The [`server`](src/server) crate provides the WebSocket server that acts as a gateway for peer interactions:
+
+- **Peer Authentication**: secp256k1 signature-based authentication via JSON-RPC
+- **Script Routing**: Routes incoming script execution requests to Redis queues
+- **Session Management**: Maintains authenticated WebSocket connections per peer
+- **Multi-Peer Support**: Single server instance can handle multiple peer contexts
+
+**Key Features:**
+- Built on Actix Web with WebSocket actors ([`CircleWs`](src/server/src/lib.rs:100))
+- JSON-RPC 2.0 API for authentication and script execution
+- Redis integration for decoupled worker communication
+
+### 2.3. Worker Engine
+
+The [`worker`](src/worker) crate implements the script execution engine:
+
+- **Context-Aware Execution**: Loads and executes scripts within specific peer contexts
+- **Namespace Isolation**: Each peer gets isolated filesystem access
+- **Caller Awareness**: Scripts execute with knowledge of both caller and target peer
+- **Multiplexing Support**: Single worker can serve multiple peer contexts
+
+**Execution Environment Variables:**
+```rhai
+CALLER_PUBLIC_KEY // The peer who sent the script
+CIRCLE_PUBLIC_KEY // The peer in whose context the script runs
+DB_PATH // Namespaced database path for this peer
+```
+
+### 2.4. Client Library
+
+The [`client_ws`](src/client_ws) crate provides cross-platform WebSocket client functionality:
+
+- **Cross-Platform**: Native (tokio-tungstenite) and WASM (gloo-net) support
+- **Authentication**: Automated secp256k1 signature-based authentication
+- **Script Execution**: High-level API for sending scripts to other peers
+- **Builder Pattern**: Flexible client construction with optional authentication
+
+## 3. Communication Topology
+
+### 3.1. Peer-to-Peer Script Execution Flow
+
+```mermaid
+sequenceDiagram
+ participant PeerA as Peer A (Client)
+ participant Circle as Circle Node (WebSocket)
+ participant Redis as Redis Queue
+ participant Worker as Worker Engine
+ participant FS as Peer B Filesystem
+
+ Note over PeerA,FS: Peer A sends script to execute in Peer B's context
+
+ PeerA->>Circle: WebSocket: JSON-RPC "play" request
+ Note right of PeerA: Script + Target Peer B's public key
+
+ Circle->>Circle: Validate authentication
+ Circle->>Redis: LPUSH to peer B's queue
+ Note right of Circle: Queue: rhailib:{peer_b_pubkey}
+
+ Worker->>Redis: BLPOP from peer B's queue
+ Worker->>Worker: Load Peer B's context
+ Note right of Worker: Set CALLER_PUBLIC_KEY=peer_a
Set CIRCLE_PUBLIC_KEY=peer_b
Set DB_PATH=peer_b_namespace/
+
+ Worker->>FS: Execute script in Peer B's namespace
+ FS-->>Worker: Script execution result
+
+ Worker->>Redis: LPUSH result to reply queue
+ Redis-->>Circle: Result notification
+ Circle-->>PeerA: WebSocket: JSON-RPC response
+```
+
+### 3.2. System Component Diagram
+
+```mermaid
+graph TB
+ subgraph "Peer A Environment"
+ ClientA[Client A
client_ws]
+ KeyA[Private Key A]
+ ClientA -.-> KeyA
+ end
+
+ subgraph "Peer B Environment"
+ ClientB[Client B
client_ws]
+ KeyB[Private Key B]
+ ClientB -.-> KeyB
+ end
+
+ subgraph "Circle Infrastructure"
+ Circle[Circle Node
server]
+ Redis[(Redis
Task Queues)]
+ Worker1[Worker Engine
Peer A Context]
+ Worker2[Worker Engine
Peer B Context]
+
+ Circle <--> Redis
+ Redis <--> Worker1
+ Redis <--> Worker2
+ end
+
+ subgraph "Isolated Storage"
+ FSA[Peer A Namespace
DB_PATH/peer_a/]
+ FSB[Peer B Namespace
DB_PATH/peer_b/]
+
+ Worker1 <--> FSA
+ Worker2 <--> FSB
+ end
+
+ ClientA <-->|WebSocket
Authenticated| Circle
+ ClientB <-->|WebSocket
Authenticated| Circle
+
+ style ClientA fill:#e1f5fe
+ style ClientB fill:#e8f5e8
+ style Circle fill:#fff3e0
+ style Worker1 fill:#e1f5fe
+ style Worker2 fill:#e8f5e8
+```
+
+### 3.3. Multi-Circle Deployment Architecture
+
+```mermaid
+graph TB
+ subgraph "Launcher Configuration"
+ Launcher[Launcher
circles.json]
+ Config[["OurWorld: port 8090
Threefold: port 8091
Sikana: port 8092
..."]]
+ Launcher --> Config
+ end
+
+ subgraph "Circle Nodes"
+ Circle1[Circle 1
:8090]
+ Circle2[Circle 2
:8091]
+ Circle3[Circle 3
:8092]
+ end
+
+ subgraph "Worker Pool"
+ Worker1[Worker
OurWorld Context]
+ Worker2[Worker
Threefold Context]
+ Worker3[Worker
Sikana Context]
+ end
+
+ subgraph "Shared Infrastructure"
+ Redis[(Redis
Coordination)]
+ Storage[(Namespaced Storage
peer_contexts/)]
+ end
+
+ Launcher --> Circle1
+ Launcher --> Circle2
+ Launcher --> Circle3
+
+ Circle1 <--> Redis
+ Circle2 <--> Redis
+ Circle3 <--> Redis
+
+ Redis <--> Worker1
+ Redis <--> Worker2
+ Redis <--> Worker3
+
+ Worker1 <--> Storage
+ Worker2 <--> Storage
+ Worker3 <--> Storage
+```
+
+## 4. Authentication and Security
+
+### 4.1. Peer Authentication Flow
+
+```mermaid
+sequenceDiagram
+ participant Client as Peer Client
+ participant Circle as Circle Node
+
+ Note over Client: Has secp256k1 keypair
+
+ Client->>+Circle: JSON-RPC "fetch_nonce" (pubkey)
+ Circle->>Circle: generate_nonce()
+ Circle->>Circle: store_nonce(pubkey, nonce)
+ Circle-->>-Client: nonce + expiration
+
+ Client->>Client: sign(nonce, private_key)
+
+ Client->>+Circle: JSON-RPC "authenticate" (pubkey, signature)
+ Circle->>Circle: verify_signature(nonce, signature, pubkey)
+
+ alt Signature Valid
+ Circle->>Circle: Mark session as authenticated
+ Circle-->>Client: {"authenticated": true}
+ Note over Circle: Subsequent "play" requests include authenticated pubkey
+ else Signature Invalid
+ Circle-->>-Client: JSON-RPC Error: Invalid Credentials
+ Circle->>Circle: Close connection
+ end
+```
+
+### 4.2. Context Isolation Model
+
+Each peer context is isolated through:
+
+1. **Filesystem Namespacing**: Each peer gets a dedicated root directory
+2. **Environment Variables**: Scripts execute with peer-specific context variables
+3. **Redis Queue Isolation**: Each peer has dedicated task queues
+4. **Authentication Boundaries**: Only authenticated peers can send scripts
+
+```
+worker_rhai_temp_db/
+├── peer_a_public_key/ # Peer A's isolated namespace
+│ ├── data/
+│ ├── config/
+│ └── logs/
+├── peer_b_public_key/ # Peer B's isolated namespace
+│ ├── data/
+│ ├── config/
+│ └── logs/
+└── ...
+```
+
+## 5. Script Execution Model
+
+### 5.1. Cross-Peer Script Execution
+
+When Peer A sends a script to Peer B:
+
+1. **Authentication**: Peer A authenticates with their private key
+2. **Script Routing**: Circle routes the script to Peer B's Redis queue
+3. **Context Loading**: Worker loads Peer B's execution context
+4. **Environment Setup**: Worker sets context variables:
+ ```rhai
+ CALLER_PUBLIC_KEY = "peer_a_public_key"
+ CIRCLE_PUBLIC_KEY = "peer_b_public_key"
+ DB_PATH = "/path/to/peer_b_namespace"
+ ```
+5. **Script Execution**: Rhai engine executes script in Peer B's context
+6. **Result Return**: Execution result is returned to Peer A via Redis/WebSocket
+
+### 5.2. Example Script Execution
+
+```rhai
+// Script sent by Peer A to Peer B
+print("Script running in context of: " + CIRCLE_PUBLIC_KEY);
+print("Initiated by: " + CALLER_PUBLIC_KEY);
+
+// Access Peer B's data (isolated namespace)
+let peer_b_data = load_data("user_preferences.json");
+
+// Perform operations in Peer B's context
+let result = process_data(peer_b_data);
+
+// Return result to Peer A
+result
+```
+
+## 6. API Reference
+
+### 6.1. JSON-RPC Methods
+
+The Circle WebSocket API provides three core methods:
+
+#### `fetch_nonce`
+Requests a cryptographic nonce for authentication.
+
+**Parameters:**
+- `pubkey` (string): Client's secp256k1 public key
+
+**Response:**
+```json
+{
+ "nonce": "base64_encoded_nonce",
+ "expires_at": 1234567890
+}
+```
+
+#### `authenticate`
+Authenticates the client using a signed nonce.
+
+**Parameters:**
+- `pubkey` (string): Client's public key
+- `signature` (string): Nonce signed with client's private key
+
+**Response:**
+```json
+{
+ "authenticated": true
+}
+```
+
+#### `play`
+Executes a script in a target peer's context.
+
+**Parameters:**
+- `script` (string): Rhai script to execute
+
+**Response:**
+```json
+{
+ "output": "script_execution_result"
+}
+```
+
+### 6.2. Client Library Usage
+
+```rust
+use circle_client_ws::{CircleWsClientBuilder, CircleWsClient};
+
+// Create authenticated client
+let mut client = CircleWsClientBuilder::new("ws://localhost:8090/peer_b_pubkey".to_string())
+ .with_keypair(private_key)
+ .build();
+
+// Connect and authenticate
+client.connect().await?;
+client.authenticate().await?;
+
+// Send script to execute in peer B's context
+let result = client.play(r#"
+ print("Hello from " + CALLER_PUBLIC_KEY + " to " + CIRCLE_PUBLIC_KEY);
+ "Script executed successfully"
+"#.to_string()).await?;
+
+println!("Result: {}", result.output);
+```
+
+## 7. Deployment Models
+
+### 7.1. Single-Peer Worker
+
+Each peer runs in its own dedicated worker process:
+
+```bash
+# Start worker for specific peer
+./worker --circle-public-key peer_a_public_key --redis-url redis://localhost:6379
+```
+
+### 7.2. Multi-Peer Worker
+
+Single worker serves multiple peer contexts with namespace isolation:
+
+```bash
+# Worker handles multiple peers with context switching
+./worker --redis-url redis://localhost:6379
+```
+
+### 7.3. Launcher-Managed Deployment
+
+Use the launcher to manage multiple circles:
+
+```json
+[
+ {
+ "name": "OurWorld",
+ "port": 8090,
+ "script_path": "scripts/ourworld.rhai"
+ },
+ {
+ "name": "Threefold",
+ "port": 8091,
+ "script_path": "scripts/threefold.rhai"
+ }
+]
+```
+
+```bash
+./launcher --config circles.json
+```
+
+## 8. Technical Implementation Details
+
+### 8.1. Redis Protocol
+
+**Task Queue Pattern:**
+- Queue Key: `rhailib:{peer_public_key}`
+- Task Details: `rhailib:{task_id}` (hash)
+- Reply Queue: `rhailib:reply:{task_id}`
+
+**Task Lifecycle:**
+1. Client submits script → Redis queue
+2. Worker picks up task → Updates status to "processing"
+3. Worker executes script → Updates status to "completed"/"error"
+4. Worker publishes result → Reply queue
+5. Client receives result → Task cleanup (optional)
+
+### 8.2. Worker Context Management
+
+```rust
+// Worker sets up peer context before script execution
+let mut db_config = rhai::Map::new();
+db_config.insert("DB_PATH".into(), db_path.clone().into());
+db_config.insert("CALLER_PUBLIC_KEY".into(), caller_id.clone().into());
+db_config.insert("CIRCLE_PUBLIC_KEY".into(), circle_public_key.clone().into());
+engine.set_default_tag(Dynamic::from(db_config));
+```
+
+### 8.3. Filesystem Isolation
+
+Each peer context gets an isolated filesystem namespace:
+
+```rust
+// Example namespace structure
+let peer_namespace = format!("{}/{}", base_db_path, peer_public_key);
+std::fs::create_dir_all(&peer_namespace)?;
+
+// All file operations are relative to this namespace
+let data_file = format!("{}/data/user_data.json", peer_namespace);
+```
+
+## 9. Comparison to Traditional Architectures
+
+| Feature | Traditional Backend | Circles (Peer-Centric) |
+|---------|-------------------|------------------------|
+| User Representation | Row in database | Autonomous execution context |
+| Request Routing | Global endpoints | Peer-scoped script execution |
+| State Management | Shared application state | Isolated peer contexts |
+| Communication | HTTP/REST, JSON-RPC | WebSocket + Redis coordination |
+| Distribution | Centralized | Decentralized, per-peer scalable |
+| Execution Model | Threaded request handlers | Context-aware script processors |
+| Data Isolation | Application-level | Filesystem namespacing |
+| Cross-User Operations | Database transactions | Cross-peer script execution |
+
+## 10. Summary
+
+Circles implements a **distributed, peer-contextual application layer runtime** where:
+
+- Each peer is an isolated execution context identified by secp256k1 public keys
+- Peers can send scripts to other peers for execution within the target's context
+- Workers enforce isolation through filesystem namespacing and environment variables
+- The system supports both single-peer and multi-peer worker deployments
+- Communication flows through WebSocket gateways coordinated via Redis queues
+
+This architecture enables **decentralized application logic** where peers maintain autonomy over their execution environments while supporting secure cross-peer interactions through script-based workflows.
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..a8197b1
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,27 @@
+# Contributing
+
+
+## Development Pipeline
+
+### Before committing
+
+1.
+
+## Cargo.toml
+
+## Folder, crate, library, and binary naming
+
+As every crate in this repository is part of the same `circles` workspace, it would be reduntant to name every folder circles_*. However for naming the libraries, for external projects, simply the folder name in this workspace would not be unique, so we use circles_* (following rust naming conventions). Finally, for crate names, as underscores are reserved for rust crates, we use circles-*.
+
+For example, we have a websocket server crate for circles:
+- folder: `server`
+- library: `circles_server_ws`
+- crate: `circles-server-ws`
+
+## Development with LLMs
+
+Each project is specified with three files:
+
+- `README.md`: A high-level overview of the project, its purpose, and how to use it.
+- `ARCHITECTURE.md`: A detailed description of the system's architecture, including its components and how they interact.
+- `CONTRIBUTING.md`: Guidelines for contributing to the project, including how to set up the development environment and how to submit pull requests.
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 9c9d2d1..84f1246 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11,7 +11,7 @@ dependencies = [
"actix-macros",
"actix-rt",
"actix_derive",
- "bitflags",
+ "bitflags 2.9.1",
"bytes",
"crossbeam-channel",
"futures-core",
@@ -33,7 +33,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
dependencies = [
- "bitflags",
+ "bitflags 2.9.1",
"bytes",
"futures-core",
"futures-sink",
@@ -55,8 +55,8 @@ dependencies = [
"actix-service",
"actix-tls",
"actix-utils",
- "base64",
- "bitflags",
+ "base64 0.22.1",
+ "bitflags 2.9.1",
"brotli",
"bytes",
"bytestring",
@@ -131,7 +131,7 @@ dependencies = [
"futures-core",
"futures-util",
"mio",
- "socket2",
+ "socket2 0.5.10",
"tokio",
"tracing",
]
@@ -213,7 +213,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"smallvec",
- "socket2",
+ "socket2 0.5.10",
"time",
"tracing",
"url",
@@ -445,6 +445,12 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
[[package]]
name = "base64"
version = "0.22.1"
@@ -486,7 +492,7 @@ version = "0.69.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
dependencies = [
- "bitflags",
+ "bitflags 2.9.1",
"cexpr",
"clang-sys",
"itertools",
@@ -519,6 +525,12 @@ dependencies = [
"hex-conservative",
]
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
[[package]]
name = "bitflags"
version = "2.9.1"
@@ -634,6 +646,9 @@ name = "circle_client_ws"
version = "0.1.0"
dependencies = [
"circle_ws_lib",
+ "clap",
+ "dotenv",
+ "env_logger",
"futures-channel",
"futures-util",
"gloo-console 0.3.0",
@@ -644,14 +659,13 @@ dependencies = [
"log",
"native-tls",
"rand 0.8.5",
- "secp256k1 0.29.1",
+ "secp256k1",
"serde",
"serde_json",
"sha3",
"thiserror",
"tokio",
- "tokio-native-tls",
- "tokio-tungstenite 0.19.0",
+ "tokio-tungstenite 0.23.1",
"url",
"uuid",
"wasm-bindgen",
@@ -666,25 +680,32 @@ dependencies = [
"actix",
"actix-web",
"actix-web-actors",
+ "bytes",
"chrono",
"clap",
- "engine",
+ "dotenv",
"env_logger",
"futures-util",
"heromodels",
"hex",
+ "hmac",
"log",
+ "native-tls",
"once_cell",
"rand 0.8.5",
- "redis",
+ "redis 0.23.3",
+ "redis 0.25.4",
"rhai_client",
+ "rhailib_engine",
"rhailib_worker",
"rustls",
- "rustls-pemfile",
- "secp256k1 0.29.1",
+ "rustls-pemfile 2.2.0",
+ "secp256k1",
"serde",
"serde_json",
+ "sha2",
"sha3",
+ "thiserror",
"tokio",
"tokio-tungstenite 0.19.0",
"url",
@@ -697,15 +718,12 @@ version = "0.1.0"
dependencies = [
"circle_client_ws",
"circle_ws_lib",
- "engine",
"env_logger",
"heromodels",
"hex",
- "launcher",
"log",
- "redis",
- "rhailib_worker",
- "secp256k1 0.29.1",
+ "redis 0.25.4",
+ "secp256k1",
"serde_json",
"tempfile",
"tokio",
@@ -717,8 +735,6 @@ version = "0.1.0"
dependencies = [
"chrono",
"circle_client_ws",
- "common_models",
- "engine",
"futures",
"futures-channel",
"futures-util",
@@ -735,7 +751,8 @@ dependencies = [
"log",
"rand 0.8.5",
"rhai",
- "secp256k1 0.29.1",
+ "rhailib_engine",
+ "secp256k1",
"serde",
"serde_json",
"sha3",
@@ -838,25 +855,6 @@ dependencies = [
"tokio-util",
]
-[[package]]
-name = "comfy-table"
-version = "7.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a"
-dependencies = [
- "crossterm",
- "unicode-segmentation",
- "unicode-width 0.2.1",
-]
-
-[[package]]
-name = "common_models"
-version = "0.1.0"
-dependencies = [
- "serde",
- "serde_derive",
-]
-
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
@@ -947,28 +945,6 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
-[[package]]
-name = "crossterm"
-version = "0.28.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
-dependencies = [
- "bitflags",
- "crossterm_winapi",
- "parking_lot",
- "rustix 0.38.44",
- "winapi",
-]
-
-[[package]]
-name = "crossterm_winapi"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
-dependencies = [
- "winapi",
-]
-
[[package]]
name = "crunchy"
version = "0.2.3"
@@ -1000,6 +976,14 @@ dependencies = [
"powerfmt",
]
+[[package]]
+name = "derive"
+version = "0.1.0"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "derive_more"
version = "2.0.1"
@@ -1029,6 +1013,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
+ "subtle",
]
[[package]]
@@ -1042,6 +1027,12 @@ dependencies = [
"syn 2.0.103",
]
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
[[package]]
name = "dunce"
version = "1.0.5"
@@ -1069,17 +1060,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
-[[package]]
-name = "engine"
-version = "0.1.0"
-dependencies = [
- "chrono",
- "heromodels",
- "heromodels-derive",
- "heromodels_core",
- "rhai",
-]
-
[[package]]
name = "env_logger"
version = "0.10.2"
@@ -1712,6 +1692,7 @@ version = "0.1.0"
dependencies = [
"bincode 2.0.1",
"chrono",
+ "derive",
"heromodels-derive",
"heromodels_core",
"ourdb",
@@ -1754,6 +1735,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20"
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
[[package]]
name = "home"
version = "0.5.11"
@@ -1785,6 +1775,17 @@ dependencies = [
"itoa",
]
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http 0.2.12",
+ "pin-project-lite",
+]
+
[[package]]
name = "httparse"
version = "1.10.1"
@@ -1803,6 +1804,43 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
+[[package]]
+name = "hyper"
+version = "0.14.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http 0.2.12",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2 0.5.10",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
[[package]]
name = "iana-time-zone"
version = "0.1.63"
@@ -1979,6 +2017,12 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
[[package]]
name = "is-terminal"
version = "0.4.16"
@@ -2046,35 +2090,6 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
-[[package]]
-name = "launcher"
-version = "0.1.0"
-dependencies = [
- "actix-web",
- "circle_ws_lib",
- "clap",
- "comfy-table",
- "engine",
- "env_logger",
- "futures-util",
- "heromodels",
- "log",
- "once_cell",
- "ourdb",
- "rand 0.8.5",
- "redis",
- "rhai",
- "rhai_client",
- "rhailib_worker",
- "secp256k1 0.28.2",
- "serde",
- "serde_json",
- "tempfile",
- "tokio",
- "tokio-tungstenite 0.23.1",
- "url",
-]
-
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -2154,6 +2169,16 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+[[package]]
+name = "macros"
+version = "0.1.0"
+dependencies = [
+ "heromodels",
+ "heromodels_core",
+ "rhai",
+ "serde",
+]
+
[[package]]
name = "matchers"
version = "0.1.0"
@@ -2244,7 +2269,7 @@ version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
- "bitflags",
+ "bitflags 2.9.1",
"cfg-if",
"libc",
]
@@ -2333,7 +2358,7 @@ version = "0.10.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
dependencies = [
- "bitflags",
+ "bitflags 2.9.1",
"cfg-if",
"foreign-types",
"libc",
@@ -2649,6 +2674,27 @@ dependencies = [
"getrandom 0.3.3",
]
+[[package]]
+name = "redis"
+version = "0.23.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f49cdc0bb3f412bf8e7d1bd90fe1d9eb10bc5c399ba90973c14662a27b3f8ba"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "combine",
+ "futures-util",
+ "itoa",
+ "percent-encoding",
+ "pin-project-lite",
+ "ryu",
+ "sha1_smol",
+ "socket2 0.4.10",
+ "tokio",
+ "tokio-util",
+ "url",
+]
+
[[package]]
name = "redis"
version = "0.25.4"
@@ -2664,7 +2710,7 @@ dependencies = [
"pin-project-lite",
"ryu",
"sha1_smol",
- "socket2",
+ "socket2 0.5.10",
"tokio",
"tokio-util",
"url",
@@ -2676,7 +2722,7 @@ version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
dependencies = [
- "bitflags",
+ "bitflags 2.9.1",
]
[[package]]
@@ -2730,13 +2776,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
-name = "rhai"
-version = "1.22.2"
+name = "reqwest"
+version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2780e813b755850e50b178931aaf94ed24f6817f46aaaf5d21c13c12d939a249"
+checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
+dependencies = [
+ "base64 0.21.7",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http 0.2.12",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pemfile 1.0.4",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "rhai"
+version = "1.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
dependencies = [
"ahash",
- "bitflags",
+ "bitflags 2.9.1",
"instant",
"no-std-compat",
"num-traits",
@@ -2753,8 +2839,10 @@ name = "rhai_client"
version = "0.1.0"
dependencies = [
"chrono",
+ "clap",
+ "env_logger",
"log",
- "redis",
+ "redis 0.25.4",
"serde",
"serde_json",
"tokio",
@@ -2782,19 +2870,49 @@ dependencies = [
"syn 2.0.103",
]
+[[package]]
+name = "rhailib_dsl"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "derive",
+ "dotenv",
+ "heromodels",
+ "heromodels-derive",
+ "heromodels_core",
+ "macros",
+ "reqwest",
+ "rhai",
+ "serde",
+ "serde_json",
+ "tokio",
+]
+
+[[package]]
+name = "rhailib_engine"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "heromodels",
+ "heromodels-derive",
+ "heromodels_core",
+ "rhai",
+ "rhailib_dsl",
+]
+
[[package]]
name = "rhailib_worker"
version = "0.1.0"
dependencies = [
"chrono",
"clap",
- "engine",
"env_logger",
"heromodels",
"log",
- "redis",
+ "redis 0.25.4",
"rhai",
"rhai_client",
+ "rhailib_engine",
"serde",
"serde_json",
"tokio",
@@ -2843,7 +2961,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
- "bitflags",
+ "bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys 0.4.15",
@@ -2856,7 +2974,7 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
- "bitflags",
+ "bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys 0.9.4",
@@ -2878,6 +2996,15 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
+dependencies = [
+ "base64 0.21.7",
+]
+
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
@@ -2920,7 +3047,7 @@ version = "13.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86"
dependencies = [
- "bitflags",
+ "bitflags 2.9.1",
"cfg-if",
"clipboard-win",
"fd-lock",
@@ -2932,7 +3059,7 @@ dependencies = [
"radix_trie",
"rustyline-derive",
"unicode-segmentation",
- "unicode-width 0.1.14",
+ "unicode-width",
"utf8parse",
"winapi",
]
@@ -2978,16 +3105,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-[[package]]
-name = "secp256k1"
-version = "0.28.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10"
-dependencies = [
- "rand 0.8.5",
- "secp256k1-sys 0.9.2",
-]
-
[[package]]
name = "secp256k1"
version = "0.29.1"
@@ -2996,16 +3113,7 @@ checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
dependencies = [
"bitcoin_hashes",
"rand 0.8.5",
- "secp256k1-sys 0.10.1",
-]
-
-[[package]]
-name = "secp256k1-sys"
-version = "0.9.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb"
-dependencies = [
- "cc",
+ "secp256k1-sys",
]
[[package]]
@@ -3023,7 +3131,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
- "bitflags",
+ "bitflags 2.9.1",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -3123,6 +3231,17 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
[[package]]
name = "sha3"
version = "0.10.8"
@@ -3180,6 +3299,16 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "socket2"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
[[package]]
name = "socket2"
version = "0.5.10"
@@ -3246,6 +3375,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
+ "quote",
"unicode-ident",
]
@@ -3260,6 +3390,12 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
[[package]]
name = "synstructure"
version = "0.13.2"
@@ -3271,6 +3407,27 @@ dependencies = [
"syn 2.0.103",
]
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "tempfile"
version = "3.20.0"
@@ -3391,7 +3548,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
- "socket2",
+ "socket2 0.5.10",
"tokio-macros",
"windows-sys 0.52.0",
]
@@ -3496,6 +3653,12 @@ dependencies = [
"winnow",
]
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
[[package]]
name = "tracing"
version = "0.1.41"
@@ -3558,6 +3721,12 @@ dependencies = [
"tracing-log",
]
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
[[package]]
name = "tst"
version = "0.1.0"
@@ -3644,12 +3813,6 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
-[[package]]
-name = "unicode-width"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
-
[[package]]
name = "unicode-xid"
version = "0.2.6"
@@ -3749,6 +3912,15 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@@ -3982,6 +4154,15 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -4000,6 +4181,21 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -4032,6 +4228,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -4044,6 +4246,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -4056,6 +4264,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -4080,6 +4294,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -4092,6 +4312,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -4104,6 +4330,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -4116,6 +4348,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@@ -4137,13 +4375,23 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
- "bitflags",
+ "bitflags 2.9.1",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 17e1d01..d1892bc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,8 +7,7 @@ edition = "2021"
resolver = "2"
members = [
"src/client_ws",
- "src/server_ws",
- "src/launcher",
+ "src/server",
"src/ui_repl",
"src/app",
]
@@ -46,9 +45,9 @@ uuid = { version = "1.6", features = ["v4", "serde", "js"] }
thiserror = "1.0"
# Path dependencies to other local crates from outside this repo
heromodels = { path = "../db/heromodels" }
-engine = { path = "../rhailib/src/engine" }
+rhailib_engine = { path = "../rhailib/src/engine" }
rhailib_worker = { path = "../rhailib/src/worker" }
-circle_ws_lib = { path = "src/server_ws" }
+circle_ws_lib = { path = "src/server" }
# Dev dependencies
@@ -59,12 +58,9 @@ tempfile = "3.10.1"
log = "0.4"
circle_ws_lib = { workspace = true }
heromodels = { workspace = true }
-engine = { workspace = true }
-rhailib_worker = { workspace = true }
redis = { workspace = true }
secp256k1 = { workspace = true }
hex = { workspace = true }
-launcher = { path = "src/launcher" }
diff --git a/README.md b/README.md
index 617cf15..511c9c0 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,28 @@
-# Circles Project
+# Circles
-Welcome to the `circles` project, a full-stack system featuring a WebSocket server, a cross-platform client, and a launcher to manage multiple instances. This project is designed for executing Rhai scripts in isolated environments, with an optional layer of `secp256k1` cryptographic authentication.
+Welcome to `circles`, a system designed to accomodate a peer-centric backend infrastructure for
+
+This system is a distributed, context-aware application layer runtime where peers trigger script executions in each other’s logical environments. Execution always occurs within the target peer’s context, with the caller peer’s identity provided for authorization and provenance. Workers — whether serving single or multiple peers — enforce context isolation via namespaced filesystems and runtime boundaries. The system enables cross-peer logic to run safely and scalably without assuming global state or centralized coordination.
+
+
+
+
+featuring a WebSocket server, a cross-platform client, and a launcher to manage multiple instances. This project is designed for executing Rhai scripts in isolated environments, with an optional layer of `secp256k1` cryptographic authentication.
## Overview
The `circles` project provides two core library crates and a utility application:
-- **`server_ws`**: The core WebSocket server library, built with `Actix`. It handles client connections, processes JSON-RPC messages, and executes Rhai scripts.
+- **`server`**: The core WebSocket server library, built with `Actix`. It handles client connections, processes JSON-RPC messages, and executes Rhai scripts.
- **`client_ws`**: The core cross-platform WebSocket client library, compatible with both native Rust and WebAssembly (WASM) environments.
-- **`launcher`**: A convenient command-line utility that uses the `server_ws` library to read a `circles.json` configuration file and spawn multiple, isolated "Circle" instances.
+- **`launcher`**: A convenient command-line utility that uses the `server` library to read a `circles.json` configuration file and spawn multiple, isolated "Circle" instances.
- **`openrpc.json`**: An OpenRPC specification that formally defines the JSON-RPC 2.0 API used for client-server communication.
## Architecture
-The system is designed around a client-server model, with `client_ws` and `server_ws` as the core components. The `launcher` is provided as a utility for orchestrating multiple server instances, each configured as an isolated "Circle" environment.
+The system is designed around a client-server model, with `client_ws` and `server` as the core components. The `launcher` is provided as a utility for orchestrating multiple server instances, each configured as an isolated "Circle" environment.
-Clients connect to a `server_ws` instance via WebSocket and interact with it using the JSON-RPC protocol. The server can be configured to require authentication, in which case the client must complete a signature-based challenge-response flow over the WebSocket connection before it can execute protected methods like `play`.
+Clients connect to a `server` instance via WebSocket and interact with it using the JSON-RPC protocol. The server can be configured to require authentication, in which case the client must complete a signature-based challenge-response flow over the WebSocket connection before it can execute protected methods like `play`.
For a more detailed explanation of the system's design, please see the [ARCHITECTURE.md](ARCHITECTURE.md) file.
@@ -54,10 +61,10 @@ For a complete definition of the API, including request parameters and response
## Crates
-- **[server_ws](server_ws/README.md)**: Detailed documentation for the server library.
+- **[server](server/README.md)**: Detailed documentation for the server library.
- **[client_ws](client_ws/README.md)**: Detailed documentation for the client library.
- **[launcher](launcher/README.md)**: Detailed documentation for the launcher utility.
-- **[app](src/app/README.md)**: A Yew frontend application that uses the `client_ws` to interact with the `server_ws`.
+- **[app](src/app/README.md)**: A Yew frontend application that uses the `client_ws` to interact with the `server`.
## Running the App
diff --git a/cmd/dispatcher.rs b/cmd/dispatcher.rs
new file mode 100644
index 0000000..97fa8af
--- /dev/null
+++ b/cmd/dispatcher.rs
@@ -0,0 +1,189 @@
+use clap::Parser;
+use rhai_client::{RhaiClient, RhaiClientBuilder};
+use log::{error, info};
+use std::io::{self, Write};
+use std::time::Duration;
+
+#[derive(Parser, Debug)]
+#[command(author, version, about = "Circles Client - Rhai script execution client", long_about = None)]
+struct Args {
+ /// Caller public key (caller ID)
+ #[arg(short = 'c', long = "caller-key", help = "Caller public key (your identity)")]
+ caller_public_key: String,
+
+ /// Circle public key (context ID)
+ #[arg(short = 'k', long = "circle-key", help = "Circle public key (execution context)")]
+ circle_public_key: String,
+
+ /// Worker public key (defaults to circle public key if not provided)
+ #[arg(short = 'w', long = "worker-key", help = "Worker public key (defaults to circle key)")]
+ worker_public_key: Option,
+
+ /// Redis URL
+ #[arg(short, long, default_value = "redis://localhost:6379", help = "Redis connection URL")]
+ redis_url: String,
+
+ /// Rhai script to execute
+ #[arg(short, long, help = "Rhai script to execute")]
+ script: Option,
+
+ /// Path to Rhai script file
+ #[arg(short, long, help = "Path to Rhai script file")]
+ file: Option,
+
+ /// Timeout for script execution (in seconds)
+ #[arg(short, long, default_value = "30", help = "Timeout for script execution in seconds")]
+ timeout: u64,
+
+ /// Increase verbosity (can be used multiple times)
+ #[arg(short, long, action = clap::ArgAction::Count, help = "Increase verbosity (-v for debug, -vv for trace)")]
+ verbose: u8,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ let args = Args::parse();
+
+ // Configure logging based on verbosity level
+ let log_config = match args.verbose {
+ 0 => "warn,circles_client=info,rhai_client=info",
+ 1 => "info,circles_client=debug,rhai_client=debug",
+ 2 => "debug",
+ _ => "trace",
+ };
+
+ std::env::set_var("RUST_LOG", log_config);
+ env_logger::init();
+
+ // Use worker key or default to circle key
+ let worker_key = args.worker_public_key.unwrap_or_else(|| args.circle_public_key.clone());
+
+ info!("🔗 Starting Circles Client");
+ info!("📋 Configuration:");
+ info!(" Caller Key: {}", args.caller_public_key);
+ info!(" Circle Key: {}", args.circle_public_key);
+ info!(" Worker Key: {}", worker_key);
+ info!(" Redis URL: {}", args.redis_url);
+ info!(" Timeout: {}s", args.timeout);
+ info!();
+
+ // Create the Rhai client
+ let client = RhaiClientBuilder::new()
+ .caller_id(&args.caller_public_key)
+ .redis_url(&args.redis_url)
+ .build()?;
+
+ info!("✅ Connected to Redis at {}", args.redis_url);
+
+ // Determine execution mode
+ if let Some(script_content) = args.script {
+ // Execute inline script
+ info!("📜 Executing inline script");
+ execute_script(&client, &worker_key, script_content, args.timeout).await?;
+ } else if let Some(file_path) = args.file {
+ // Execute script from file
+ info!("📁 Loading script from file: {}", file_path);
+ let script_content = std::fs::read_to_string(&file_path)
+ .map_err(|e| format!("Failed to read script file '{}': {}", file_path, e))?;
+ execute_script(&client, &worker_key, script_content, args.timeout).await?;
+ } else {
+ // Interactive mode
+ info!("🎮 Entering interactive mode");
+ info!("Type Rhai scripts and press Enter to execute. Type 'exit' or 'quit' to close.");
+ run_interactive_mode(&client, &worker_key, args.timeout).await?;
+ }
+
+ Ok(())
+}
+
+async fn execute_script(
+ client: &RhaiClient,
+ worker_key: &str,
+ script: String,
+ timeout_secs: u64,
+) -> Result<(), Box> {
+ info!("⚡ Executing script: {:.50}...", script);
+
+ let timeout = Duration::from_secs(timeout_secs);
+
+ match client
+ .new_play_request()
+ .recipient_id(worker_key)
+ .script(&script)
+ .timeout(timeout)
+ .await_response()
+ .await
+ {
+ Ok(result) => {
+ info!("✅ Script execution completed");
+ println!("Status: {}", result.status);
+ if let Some(output) = result.output {
+ println!("Output: {}", output);
+ }
+ if let Some(error) = result.error {
+ println!("Error: {}", error);
+ }
+ }
+ Err(e) => {
+ error!("❌ Script execution failed: {}", e);
+ return Err(Box::new(e));
+ }
+ }
+
+ Ok(())
+}
+
+async fn run_interactive_mode(
+ client: &RhaiClient,
+ worker_key: &str,
+ timeout_secs: u64,
+) -> Result<(), Box> {
+ let timeout = Duration::from_secs(timeout_secs);
+
+ loop {
+ print!("rhai> ");
+ io::stdout().flush()?;
+
+ let mut input = String::new();
+ io::stdin().read_line(&mut input)?;
+
+ let input = input.trim();
+
+ if input.is_empty() {
+ continue;
+ }
+
+ if input == "exit" || input == "quit" {
+ info!("👋 Goodbye!");
+ break;
+ }
+
+ info!("⚡ Executing: {}", input);
+
+ match client
+ .new_play_request()
+ .recipient_id(worker_key)
+ .script(input)
+ .timeout(timeout)
+ .await_response()
+ .await
+ {
+ Ok(result) => {
+ println!("Status: {}", result.status);
+ if let Some(output) = result.output {
+ println!("Output: {}", output);
+ }
+ if let Some(error) = result.error {
+ println!("Error: {}", error);
+ }
+ }
+ Err(e) => {
+ error!("❌ Execution failed: {}", e);
+ }
+ }
+
+ println!(); // Add blank line for readability
+ }
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
deleted file mode 100644
index cb71373..0000000
--- a/docs/ARCHITECTURE.md
+++ /dev/null
@@ -1,137 +0,0 @@
-# System Architecture
-
-This document provides a detailed overview of the `circles` project architecture. The project is composed of two core library crates, `server_ws` and `client_ws`, and a convenient `launcher` utility.
-
-## 1. High-Level Overview
-
-The `circles` project provides the core components for a client-server system designed to execute Rhai scripts in isolated environments. The `launcher` application is a utility that demonstrates how to use the `server_ws` and `client_ws` libraries to manage multiple server instances, but the libraries themselves are the fundamental building blocks.
-
-The core functionality revolves around:
-- **Orchestration**: The `launcher` starts and stops multiple, independent WebSocket servers.
-- **Client-Server Communication**: A JSON-RPC 2.0 API over WebSockets allows clients to execute scripts and authenticate.
-- **Authentication**: An optional, robust `secp256k1` signature-based authentication mechanism secures the script execution endpoint.
-
-## 2. Component Architecture
-
-### 2.1. `server_ws` (Library)
-
-The `server_ws` crate provides the WebSocket server that handles client connections and API requests. Its key features include:
-- **Web Framework**: Built using `Actix`, a powerful actor-based web framework for Rust.
-- **WebSocket Handling**: Uses `actix-web-actors` to manage individual WebSocket sessions. Each client connection is handled by a `CircleWs` actor, ensuring that sessions are isolated from one another.
-- **JSON-RPC API**: Exposes a JSON-RPC 2.0 API with methods for script execution (`play`) and authentication (`fetch_nonce`, `authenticate`).
-- **Authentication Service**: The authentication flow is handled entirely within the WebSocket connection using the dedicated JSON-RPC methods.
-
-### 2.2. `client_ws` (Library)
-
-The `client_ws` crate is a WebSocket client library designed for interacting with the `server_ws`. It is engineered to be cross-platform:
-- **Native**: For native Rust applications, it uses `tokio-tungstenite` for WebSocket communication.
-- **WebAssembly (WASM)**: For browser-based applications, it uses `gloo-net` to integrate with the browser's native WebSocket API.
-- **API**: Provides a flexible builder pattern for client construction and a high-level API (`CircleWsClient`) that abstracts the complexities of the WebSocket connection and the JSON-RPC protocol.
-
-### 2.3. `launcher` (Utility)
-
-The `launcher` is a command-line utility that demonstrates how to use the `server_ws` library. It is responsible for:
-- **Configuration**: Reading a `circles.json` file that defines a list of Circle instances to run.
-- **Orchestration**: Spawning a dedicated `server_ws` instance for each configured circle.
-- **Lifecycle Management**: Managing the lifecycle of all spawned servers and their associated Rhai workers.
-
-### 2.2. `server_ws`
-
-The `server_ws` crate provides the WebSocket server that handles client connections and API requests. Its key features include:
-- **Web Framework**: Built using `Actix`, a powerful actor-based web framework for Rust.
-- **WebSocket Handling**: Uses `actix-web-actors` to manage individual WebSocket sessions. Each client connection is handled by a `CircleWs` actor, ensuring that sessions are isolated from one another.
-- **JSON-RPC API**: Exposes a JSON-RPC 2.0 API with methods for script execution (`play`) and authentication (`fetch_nonce`, `authenticate`).
-- **Authentication Service**: The authentication flow is handled entirely within the WebSocket connection using the dedicated JSON-RPC methods.
-
-### 2.3. `client_ws`
-
-The `client_ws` crate is a WebSocket client library designed for interacting with the `server_ws`. It is engineered to be cross-platform:
-- **Native**: For native Rust applications, it uses `tokio-tungstenite` for WebSocket communication.
-- **WebAssembly (WASM)**: For browser-based applications, it uses `gloo-net` to integrate with the browser's native WebSocket API.
-- **API**: Provides a flexible builder pattern for client construction and a high-level API (`CircleWsClient`) that abstracts the complexities of the WebSocket connection and the JSON-RPC protocol.
-
-## 3. Communication and Protocols
-
-### 3.1. JSON-RPC 2.0
-
-All client-server communication, including authentication, uses the JSON-RPC 2.0 protocol over the WebSocket connection. This provides a unified, lightweight, and well-defined structure for all interactions. The formal API contract is defined in the [openrpc.json](openrpc.json) file.
-
-### 3.2. Authentication Flow
-
-The authentication mechanism is designed to verify that a client possesses the private key corresponding to a given public key, without ever exposing the private key. The entire flow happens over the established WebSocket connection.
-
-**Sequence of Events:**
-1. **Keypair**: The client is instantiated with a `secp256k1` keypair.
-2. **Nonce Request**: The client sends a `fetch_nonce` JSON-RPC request containing its public key.
-3. **Nonce Issuance**: The server generates a unique, single-use nonce, stores it in the actor's state, and returns it to the client in a JSON-RPC response.
-4. **Signature Creation**: The client signs the received nonce with its private key.
-5. **Authentication Request**: The client sends an `authenticate` JSON-RPC message, containing the public key and the generated signature.
-6. **Signature Verification**: The server's WebSocket actor retrieves the stored nonce for the given public key and cryptographically verifies the signature.
-7. **Session Update**: If verification is successful, the server marks the client's WebSocket session as "authenticated," granting it access to protected methods like `play`.
-
-## 4. Diagrams
-
-### 4.1. System Component Diagram
-
-```mermaid
-graph TD
- subgraph "User Machine"
- Launcher[🚀 launcher]
- CirclesConfig[circles.json]
- Launcher -- Reads --> CirclesConfig
- end
-
- subgraph "Spawned Processes"
- direction LR
- subgraph "Circle 1"
- Server1[🌐 server_ws on port 9001]
- end
- subgraph "Circle 2"
- Server2[🌐 server_ws on port 9002]
- end
- end
-
- Launcher -- Spawns & Manages --> Server1
- Launcher -- Spawns & Manages --> Server2
-
- subgraph "Clients"
- Client1[💻 client_ws]
- Client2[💻 client_ws]
- end
-
- Client1 -- Connects via WebSocket --> Server1
- Client2 -- Connects via WebSocket --> Server2
-```
-
-### 4.2. Authentication Sequence Diagram
-
-```mermaid
-sequenceDiagram
- participant Client as client_ws
- participant WsActor as CircleWs Actor (WebSocket)
-
- Client->>Client: Instantiate with keypair
-
- Note over Client: Has public_key, private_key
-
- Client->>+WsActor: JSON-RPC "fetch_nonce" (pubkey)
- WsActor->>WsActor: generate_nonce()
- WsActor->>WsActor: store_nonce(pubkey, nonce)
- WsActor-->>-Client: JSON-RPC Response ({"nonce": "..."})
-
- Client->>Client: sign(nonce, private_key)
-
- Note over Client: Has signature
-
- Client->>+WsActor: JSON-RPC "authenticate" (pubkey, signature)
- WsActor->>WsActor: retrieve_nonce(pubkey)
- WsActor->>WsActor: verify_signature(nonce, signature, pubkey)
-
- alt Signature is Valid
- WsActor->>WsActor: Set session as authenticated
- WsActor-->>-Client: JSON-RPC Response ({"authenticated": true})
- else Signature is Invalid
- WsActor-->>-Client: JSON-RPC Error (Invalid Credentials)
- end
-
- Note over WsActor: Subsequent "play" requests will include the authenticated public key.
\ No newline at end of file
diff --git a/docs/aidocs/IMPLEMENTATION_PLAN.md b/docs/aidocs/IMPLEMENTATION_PLAN.md
new file mode 100644
index 0000000..61d8037
--- /dev/null
+++ b/docs/aidocs/IMPLEMENTATION_PLAN.md
@@ -0,0 +1,351 @@
+# Multi-Circle Single-Server Implementation Plan
+
+## Overview
+Transform the current launcher from a JSON-config based multi-server system to a command-line driven single-server system that handles multiple circles via path-based routing.
+
+## Architecture Changes
+
+### Current Architecture:
+```
+Launcher → circles.json → Multiple Servers (one per circle, different ports)
+```
+
+### New Architecture:
+```
+Launcher → CLI args → Single Server (one port, path-based routing)
+```
+
+## Phase 1: Update Launcher Interface
+
+### 1.1 New Command Line Interface
+```bash
+# New launcher usage
+cargo run --bin launcher -- --port 8080 --public-keys "pk1,pk2,pk3"
+# or with multiple -k flags
+cargo run --bin launcher -- --port 8080 -k "pk1" -k "pk2" -k "pk3"
+```
+
+### 1.2 Updated Args Structure
+Replace current `Args` struct with:
+```rust
+#[derive(Parser, Debug, Clone)]
+#[command(author, version, about, long_about = None)]
+pub struct Args {
+ /// Port for the WebSocket server
+ #[arg(short, long, default_value = "8080")]
+ pub port: u16,
+
+ /// Circle public keys (hex format, can be specified multiple times)
+ #[arg(short = 'k', long = "public-key")]
+ pub public_keys: Vec,
+
+ /// Redis URL
+ #[arg(long, default_value = "redis://127.0.0.1:6379")]
+ pub redis_url: String,
+
+ /// Enable authentication
+ #[arg(long)]
+ pub enable_auth: bool,
+
+ /// Enable debug mode
+ #[arg(short, long)]
+ pub debug: bool,
+
+ /// Verbosity level
+ #[arg(short, long, action = clap::ArgAction::Count)]
+ pub verbose: u8,
+}
+```
+
+### 1.3 Remove Obsolete Code
+- Remove `CircleConfig` struct
+- Remove `SimpleArgs` struct
+- Remove `run_simple_launcher` function
+- Remove JSON parsing logic from `main.rs`
+- Remove `simple.rs` binary entirely
+
+## Phase 2: Multi-Circle Server Architecture
+
+### 2.1 New Server Configuration
+Update `ServerConfig` to support multiple circles:
+```rust
+#[derive(Clone)]
+pub struct MultiCircleServerConfig {
+ pub host: String,
+ pub port: u16,
+ pub redis_url: String,
+ pub circle_public_keys: Vec, // List of allowed circles
+ pub enable_auth: bool,
+ pub enable_tls: bool,
+ pub cert_path: Option,
+ pub key_path: Option,
+}
+```
+
+### 2.2 Path-Based Routing
+Update WebSocket routing from `/ws` to `/{circle_pk}`:
+```rust
+// New route pattern in spawn_circle_server
+.route("/{circle_pk}", web::get().to(ws_handler))
+
+// Updated handler signature
+async fn ws_handler(
+ req: HttpRequest,
+ stream: web::Payload,
+ path: web::Path, // circle_pk from URL
+ server_config: web::Data,
+) -> Result
+```
+
+### 2.3 Circle Validation
+Add validation logic in `ws_handler`:
+```rust
+let circle_pk = path.into_inner();
+
+// Validate circle_pk is in allowed list
+if !server_config.circle_public_keys.contains(&circle_pk) {
+ return Ok(HttpResponse::NotFound()
+ .json(json!({
+ "error": "Circle not found",
+ "message": format!("Circle '{}' is not available on this server", circle_pk)
+ })));
+}
+```
+
+## Phase 3: Per-Circle Authentication
+
+### 3.1 Update CircleWs Actor
+Modify `CircleWs` to work with specific circle from URL:
+```rust
+struct CircleWs {
+ circle_public_key: String, // From URL path
+ redis_url: String,
+ nonce_store: HashMap,
+ auth_enabled: bool,
+ authenticated_pubkey: Option,
+}
+
+impl CircleWs {
+ fn new_for_circle(
+ circle_public_key: String,
+ redis_url: String,
+ auth_enabled: bool,
+ ) -> Self {
+ Self {
+ circle_public_key,
+ redis_url,
+ nonce_store: HashMap::new(),
+ auth_enabled,
+ authenticated_pubkey: None,
+ }
+ }
+}
+```
+
+### 3.2 Circle-Specific Authentication
+Update authentication logic in `handle_authenticate`:
+- Authentication challenges are specific to the circle from URL path
+- Signature verification uses the circle's public key context
+- Each circle maintains separate authentication state
+
+## Phase 4: Worker Communication Updates
+
+### 4.1 Update Launcher Worker Spawning
+Modify `setup_and_spawn_circles` to:
+- Accept list of public keys instead of CircleConfig
+- Spawn one worker per public key
+- Launch single server instance with all public keys
+
+### 4.2 Play Request Routing
+Update `handle_play` to route to correct worker:
+```rust
+// Use circle_public_key from URL path for worker routing
+rhai_client
+ .new_play_request()
+ .recipient_id(&self.circle_public_key) // From URL path
+ .script_path(&script_content)
+ .timeout(TASK_TIMEOUT_DURATION)
+ .await_response()
+ .await
+```
+
+## Phase 5: Updated Launcher Logic
+
+### 5.1 New Setup Function
+Replace `setup_and_spawn_circles` with:
+```rust
+pub async fn setup_multi_circle_server(
+ public_keys: Vec,
+ port: u16,
+ redis_url: String,
+ enable_auth: bool,
+) -> Result<(Vec>, ServerHandle), Box>
+```
+
+### 5.2 Single Server Spawn
+Launch one server instance that handles all circles:
+```rust
+let server_config = MultiCircleServerConfig {
+ host: "127.0.0.1".to_string(),
+ port,
+ redis_url: redis_url.clone(),
+ circle_public_keys: public_keys.clone(),
+ enable_auth,
+ enable_tls: false,
+ cert_path: None,
+ key_path: None,
+};
+
+let (server_task, server_handle) = spawn_multi_circle_server(server_config)?;
+```
+
+### 5.3 Worker Spawning Per Circle
+Spawn one worker per public key:
+```rust
+let mut worker_handles = Vec::new();
+for public_key in &public_keys {
+ let worker_handle = spawn_rhai_worker(
+ public_key.clone(),
+ public_key.clone(),
+ engine.clone(),
+ redis_url.clone(),
+ worker_shutdown_rx,
+ preserve_tasks,
+ );
+ worker_handles.push(worker_handle);
+}
+```
+
+## Phase 6: File Structure Changes
+
+### 6.1 Remove Files
+- Delete `src/launcher/src/cmd/simple.rs`
+- Remove simple binary from `Cargo.toml`
+
+### 6.2 Update Main Binary
+Update `src/launcher/src/cmd/main.rs` to use new CLI args instead of JSON config
+
+### 6.3 Update Library Exports
+Remove exports for:
+- `SimpleArgs`
+- `run_simple_launcher`
+- `CircleConfig`
+
+## Architecture Diagram
+
+```mermaid
+graph TB
+ CLI[Launcher CLI
--port 8080
-k pk1 -k pk2 -k pk3
--enable-auth]
+
+ CLI --> L[Launcher Process]
+
+ L --> W1[Worker: pk1
Redis Queue: rhai_tasks:pk1]
+ L --> W2[Worker: pk2
Redis Queue: rhai_tasks:pk2]
+ L --> W3[Worker: pk3
Redis Queue: rhai_tasks:pk3]
+
+ L --> S[Single Server Instance
Port 8080
MultiCircleServerConfig]
+
+ C1[Client 1] --> |wss://127.0.0.1:8080/pk1| S
+ C2[Client 2] --> |wss://127.0.0.1:8080/pk2| S
+ C3[Client 3] --> |wss://127.0.0.1:8080/pk3| S
+
+ S --> |Validate pk1 in allowed list| V1[Circle Validation]
+ S --> |Validate pk2 in allowed list| V2[Circle Validation]
+ S --> |Validate pk3 in allowed list| V3[Circle Validation]
+
+ V1 --> |Create CircleWs for pk1| WS1[CircleWs Actor: pk1]
+ V2 --> |Create CircleWs for pk2| WS2[CircleWs Actor: pk2]
+ V3 --> |Create CircleWs for pk3| WS3[CircleWs Actor: pk3]
+
+ WS1 --> |Route play requests| W1
+ WS2 --> |Route play requests| W2
+ WS3 --> |Route play requests| W3
+
+ subgraph "Redis Queues"
+ R1[rhai_tasks:pk1]
+ R2[rhai_tasks:pk2]
+ R3[rhai_tasks:pk3]
+ end
+
+ W1 <--> R1
+ W2 <--> R2
+ W3 <--> R3
+
+ subgraph "Authentication Per Circle"
+ A1[Auth State: pk1]
+ A2[Auth State: pk2]
+ A3[Auth State: pk3]
+ end
+
+ WS1 <--> A1
+ WS2 <--> A2
+ WS3 <--> A3
+```
+
+## Implementation Steps Summary
+
+### Step 1: Update Launcher Args
+- Replace `Args` struct with new CLI interface
+- Remove JSON config dependencies
+- Add public key validation
+
+### Step 2: Create Multi-Circle Server Config
+- Replace `ServerConfig` with `MultiCircleServerConfig`
+- Support list of circle public keys
+- Maintain TLS and auth options
+
+### Step 3: Implement Path-Based Routing
+- Update WebSocket route from `/ws` to `/{circle_pk}`
+- Add circle validation before WebSocket upgrade
+- Return HTTP 404 for invalid circles
+
+### Step 4: Update CircleWs Actor
+- Extract circle public key from URL path
+- Implement per-circle authentication
+- Route to correct worker based on circle
+
+### Step 5: Update Launcher Logic
+- Remove JSON parsing
+- Spawn single server with multiple circles
+- Spawn one worker per circle public key
+
+### Step 6: Clean Up Code
+- Remove `SimpleArgs`, `CircleConfig`, `run_simple_launcher`
+- Delete simple binary
+- Update exports and documentation
+
+## Benefits of This Architecture
+
+1. **Simplified Deployment**: Single command, single port, multiple circles
+2. **Resource Efficiency**: Shared server infrastructure
+3. **Clear Separation**: Per-circle authentication and worker routing
+4. **Scalable**: Easy to add/remove circles via CLI
+5. **Maintainable**: Less complex than multi-server approach
+
+## Example Usage
+
+```bash
+# Launch server with 3 circles on port 8080 with auth enabled
+cargo run --bin launcher -- \
+ --port 8080 \
+ --enable-auth \
+ -k "02a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789ab" \
+ -k "03b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789abc1" \
+ -k "02c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789abcd12"
+
+# Clients connect to specific circles:
+# wss://127.0.0.1:8080/02a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789ab
+# wss://127.0.0.1:8080/03b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789abc1
+# wss://127.0.0.1:8080/02c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789abcd12
+```
+
+## Key Requirements Satisfied
+
+1. ✅ **Remove JSON config functionality** - Completely replaced with CLI args
+2. ✅ **Accept list of circle public keys** - Via `-k` flags or comma-separated
+3. ✅ **Single port for multiple circles** - One server handles all circles
+4. ✅ **Path-based routing** - `wss://127.0.0.1:port/circle_pk`
+5. ✅ **Per-circle authentication** - Auth against specific circle's public key
+6. ✅ **Route to appropriate worker** - Based on circle public key from URL
+7. ✅ **Use hex format everywhere** - Consistent secp256k1 hex encoding
+8. ✅ **HTTP 404 for invalid circles** - Before WebSocket upgrade
\ No newline at end of file
diff --git a/docs/aidocs/README.md b/docs/aidocs/README.md
new file mode 100644
index 0000000..7d3bb37
--- /dev/null
+++ b/docs/aidocs/README.md
@@ -0,0 +1 @@
+Docs generated by ai that need to be organized.
\ No newline at end of file
diff --git a/URL_ROUTING_STRATEGY.md b/docs/aidocs/URL_ROUTING_STRATEGY.md
similarity index 100%
rename from URL_ROUTING_STRATEGY.md
rename to docs/aidocs/URL_ROUTING_STRATEGY.md
diff --git a/docs/aidocs/WSS_IMPLEMENTATION_PLAN.md b/docs/aidocs/WSS_IMPLEMENTATION_PLAN.md
new file mode 100644
index 0000000..a39f7db
--- /dev/null
+++ b/docs/aidocs/WSS_IMPLEMENTATION_PLAN.md
@@ -0,0 +1,382 @@
+# WSS (WebSocket Secure) Implementation Plan
+
+## Overview
+This document outlines the complete implementation plan for adding WSS support to both the server and client_ws components, with optional configuration and comprehensive examples.
+
+## Current State Analysis
+
+### Server (src/server)
+- ✅ Basic TLS infrastructure with `rustls` and `rustls-pemfile`
+- ✅ Certificate loading functionality (`load_rustls_config`)
+- ✅ Optional TLS configuration in `ServerConfig`
+- ✅ Existing cert.pem and key.pem files for testing
+- ⚠️ TLS configuration could be enhanced with better error handling
+- ⚠️ Missing WSS-specific examples and tests
+
+### Client (src/client_ws)
+- ✅ Basic TLS support with `native-tls` and `tokio-native-tls`
+- ✅ Cross-platform WebSocket support (native + WASM)
+- ⚠️ TLS connector accepts invalid certificates (development mode)
+- ⚠️ No automatic WSS URL detection
+- ⚠️ Missing WSS-specific configuration options
+
+## Implementation Phases
+
+### Phase 1: Server WSS Enhancement ✅ READY TO IMPLEMENT
+
+#### 1.1 Enhanced ServerConfig
+```rust
+#[derive(Clone)]
+pub struct ServerConfig {
+ pub circle_name: String,
+ pub circle_public_key: String,
+ pub host: String,
+ pub port: u16,
+ pub redis_url: String,
+ pub enable_auth: bool,
+
+ // Enhanced TLS Configuration
+ pub enable_tls: bool, // Explicit TLS enable flag
+ pub cert_path: Option, // Path to certificate file
+ pub key_path: Option, // Path to private key file
+ pub tls_port: Option, // Optional separate TLS port
+ pub tls_accept_invalid_certs: bool, // For development
+}
+```
+
+#### 1.2 Improved TLS Loading
+- Better error messages for certificate loading failures
+- Validation of certificate and key file existence
+- Support for different certificate formats
+- Logging of TLS configuration status
+
+#### 1.3 Enhanced spawn_circle_server
+- Clear logging of HTTP vs HTTPS mode
+- Better error handling for TLS configuration
+- Support for running both HTTP and HTTPS simultaneously (if needed)
+
+### Phase 2: Client WSS Enhancement
+
+#### 2.1 WSS URL Detection
+```rust
+impl CircleWsClient {
+ fn is_wss_url(&self) -> bool {
+ self.ws_url.starts_with("wss://")
+ }
+
+ fn requires_tls(&self) -> bool {
+ self.is_wss_url()
+ }
+}
+```
+
+#### 2.2 Enhanced TLS Configuration
+```rust
+pub struct TlsConfig {
+ pub accept_invalid_certs: bool,
+ pub accept_invalid_hostnames: bool,
+ pub ca_cert_path: Option,
+ pub client_cert_path: Option,
+ pub client_key_path: Option,
+}
+
+impl CircleWsClientBuilder {
+ pub fn with_tls_config(mut self, tls_config: TlsConfig) -> Self {
+ self.tls_config = Some(tls_config);
+ self
+ }
+}
+```
+
+#### 2.3 Cross-Platform WSS Support
+- Native: Enhanced `tokio-tungstenite` with `rustls` support
+- WASM: Ensure `gloo-net` handles WSS URLs correctly
+- Consistent error handling across platforms
+
+### Phase 3: Examples and Testing
+
+#### 3.1 Basic WSS Example
+```rust
+// examples/wss_basic_example.rs
+// Demonstrates basic WSS connection without authentication
+```
+
+#### 3.2 WSS + Authentication Example
+```rust
+// examples/wss_auth_example.rs
+// Demonstrates WSS connection with secp256k1 authentication
+```
+
+#### 3.3 End-to-End Secure Example
+```rust
+// examples/wss_end_to_end_example.rs
+// Complete server + client WSS with authentication
+```
+
+#### 3.4 Certificate Generation Helper
+```rust
+// examples/wss_cert_generation.rs
+// Helper to generate self-signed certificates for development
+```
+
+### Phase 4: Documentation and Integration
+
+#### 4.1 Update README files
+- Server WSS configuration guide
+- Client WSS usage examples
+- Certificate management instructions
+
+#### 4.2 Integration Tests
+- WSS connection establishment
+- WSS + authentication flow
+- Certificate validation scenarios
+
+## Technical Implementation Details
+
+### Server Enhancements
+
+#### Enhanced Certificate Loading
+```rust
+fn load_rustls_config(
+ cert_path: &str,
+ key_path: &str,
+) -> Result {
+ // Validate file existence
+ if !std::path::Path::new(cert_path).exists() {
+ return Err(TlsConfigError::CertificateNotFound(cert_path.to_string()));
+ }
+
+ if !std::path::Path::new(key_path).exists() {
+ return Err(TlsConfigError::PrivateKeyNotFound(key_path.to_string()));
+ }
+
+ // Enhanced error handling for certificate loading
+ // Support for different key formats (PKCS8, RSA, etc.)
+ // Validation of certificate chain
+}
+```
+
+#### TLS Error Types
+```rust
+#[derive(Error, Debug)]
+pub enum TlsConfigError {
+ #[error("Certificate file not found: {0}")]
+ CertificateNotFound(String),
+ #[error("Private key file not found: {0}")]
+ PrivateKeyNotFound(String),
+ #[error("Invalid certificate format: {0}")]
+ InvalidCertificate(String),
+ #[error("Invalid private key format: {0}")]
+ InvalidPrivateKey(String),
+ #[error("TLS configuration error: {0}")]
+ ConfigurationError(String),
+}
+```
+
+### Client Enhancements
+
+#### WSS Connection Logic
+```rust
+impl CircleWsClient {
+ async fn create_tls_connector(&self) -> Result {
+ let mut builder = TlsConnector::builder();
+
+ if let Some(tls_config) = &self.tls_config {
+ builder.danger_accept_invalid_certs(tls_config.accept_invalid_certs);
+ builder.danger_accept_invalid_hostnames(tls_config.accept_invalid_hostnames);
+
+ // Load custom CA certificates if provided
+ if let Some(ca_cert_path) = &tls_config.ca_cert_path {
+ // Load and add CA certificate
+ }
+ }
+
+ builder.build().map_err(|e| {
+ CircleWsClientError::ConnectionError(format!("TLS configuration failed: {}", e))
+ })
+ }
+}
+```
+
+### Example Structure
+
+#### Basic WSS Example
+```rust
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ // Start WSS server
+ let config = ServerConfig {
+ enable_tls: true,
+ cert_path: Some("cert.pem".to_string()),
+ key_path: Some("key.pem".to_string()),
+ port: 8443,
+ // ... other config
+ };
+
+ let (server_task, _handle) = spawn_circle_server(config)?;
+
+ // Connect WSS client
+ let mut client = CircleWsClientBuilder::new("wss://localhost:8443/ws".to_string())
+ .with_tls_config(TlsConfig {
+ accept_invalid_certs: true, // For development
+ ..Default::default()
+ })
+ .build();
+
+ client.connect().await?;
+
+ // Test basic functionality
+ let result = client.play("print('Hello WSS!')".to_string()).await?;
+ println!("Result: {}", result.output);
+
+ client.disconnect().await;
+ Ok(())
+}
+```
+
+#### WSS + Authentication Example
+```rust
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ // Start authenticated WSS server
+ let config = ServerConfig {
+ enable_tls: true,
+ enable_auth: true,
+ cert_path: Some("cert.pem".to_string()),
+ key_path: Some("key.pem".to_string()),
+ port: 8443,
+ // ... other config
+ };
+
+ // Connect authenticated WSS client
+ let private_key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
+
+ let mut client = CircleWsClientBuilder::new("wss://localhost:8443/ws".to_string())
+ .with_keypair(private_key.to_string())
+ .with_tls_config(TlsConfig {
+ accept_invalid_certs: true,
+ ..Default::default()
+ })
+ .build();
+
+ client.connect().await?;
+
+ // Authenticate over secure connection
+ match client.authenticate().await? {
+ true => println!("Authenticated successfully over WSS"),
+ false => println!("Authentication failed"),
+ }
+
+ // Test authenticated request over secure connection
+ let result = client.play("print('Authenticated WSS request!')".to_string()).await?;
+ println!("Secure result: {}", result.output);
+
+ client.disconnect().await;
+ Ok(())
+}
+```
+
+## Testing Strategy
+
+### Unit Tests
+- TLS configuration validation
+- Certificate loading error handling
+- WSS URL detection
+- TLS connector creation
+
+### Integration Tests
+- WSS server startup with valid certificates
+- WSS client connection establishment
+- WSS + authentication flow
+- Error scenarios (invalid certificates, connection failures)
+
+### End-to-End Tests
+- Complete WSS server + client communication
+- Authentication over WSS
+- Multiple concurrent WSS connections
+- Certificate validation scenarios
+
+## Security Considerations
+
+### Development vs Production
+- **Development**: Accept self-signed certificates, invalid hostnames
+- **Production**: Strict certificate validation, proper CA chains
+
+### Certificate Management
+- Clear documentation for certificate generation
+- Support for Let's Encrypt certificates
+- Certificate rotation considerations
+
+### TLS Configuration
+- Modern TLS versions (1.2+)
+- Secure cipher suites
+- HSTS headers for web clients
+
+## File Changes Required
+
+### New Files
+- `examples/wss_basic_example.rs`
+- `examples/wss_auth_example.rs`
+- `examples/wss_end_to_end_example.rs`
+- `examples/wss_cert_generation.rs`
+- `src/server/src/tls_config.rs` (optional)
+- `src/client_ws/src/tls_config.rs` (optional)
+
+### Modified Files
+- `src/server/src/lib.rs` - Enhanced TLS support
+- `src/server/cmd/main.rs` - TLS CLI options
+- `src/client_ws/src/lib.rs` - WSS support
+- `src/client_ws/Cargo.toml` - Additional TLS dependencies
+- `src/server/Cargo.toml` - Enhanced TLS dependencies
+- `src/server/README.md` - WSS documentation
+- `src/client_ws/README.md` - WSS usage guide
+
+## Dependencies
+
+### Server Additional Dependencies
+```toml
+# Enhanced TLS support
+rustls-webpki = "0.103"
+rustls-native-certs = "0.7"
+```
+
+### Client Additional Dependencies
+```toml
+# Enhanced TLS support for native
+rustls = { version = "0.23", optional = true }
+tokio-rustls = { version = "0.26", optional = true }
+rustls-native-certs = { version = "0.7", optional = true }
+
+[features]
+default = ["crypto"]
+crypto = ["secp256k1", "sha3"]
+rustls-tls = ["rustls", "tokio-rustls", "rustls-native-certs"]
+```
+
+## Implementation Order
+
+1. **Phase 1a**: Enhance server TLS configuration and error handling
+2. **Phase 1b**: Create basic WSS server example and test
+3. **Phase 1c**: Validate server WSS functionality with manual testing
+4. **Phase 2a**: Enhance client WSS support
+5. **Phase 2b**: Create client WSS examples
+6. **Phase 2c**: Test client WSS connectivity
+7. **Phase 3**: Create end-to-end WSS examples
+8. **Phase 4**: Integration tests and documentation
+
+## Success Criteria
+
+- ✅ Server can start with WSS enabled using existing certificates
+- ✅ Client can connect to WSS server with proper TLS validation
+- ✅ Authentication works over WSS connections
+- ✅ Examples demonstrate all WSS functionality
+- ✅ Tests validate WSS behavior
+- ✅ Documentation explains WSS configuration
+- ✅ Cross-platform compatibility (native + WASM)
+
+## Next Steps
+
+1. Switch to Code mode for implementation
+2. Start with Phase 1a: Server TLS enhancements
+3. Create and test basic WSS server example
+4. Validate functionality before proceeding to client
\ No newline at end of file
diff --git a/docs/design.md b/docs/design.md
new file mode 100644
index 0000000..7e44b6b
--- /dev/null
+++ b/docs/design.md
@@ -0,0 +1,59 @@
+# Design
+
+## Overview
+
+This document outlines a system design that satisfies the specified requirements for decentralized backend ownership. It describes how to implement core capabilities like isolation, delegation, and open logic control — without introducing tight coupling or central dependencies.
+
+## Design Principles
+
+### 1. **Contextual Execution**
+- Define a runtime model where each peer context is a named environment.
+- Execution is scoped to a context, and all operations are resolved within it.
+
+**Implementation Strategy:**
+- Use a unified worker engine that can load and execute within a namespaced peer context.
+- Contexts are mounted via a virtual filesystem abstraction, one directory per peer.
+
+### 2. **Logical Isolation via Filesystem Namespacing**
+- Each peer's execution environment is backed by a namespaced root directory.
+- All storage operations are relative to that root.
+
+**Advantages:**
+- Easy enforcement of data boundaries
+- Works across shared processes
+
+### 3. **Script-Based Delegated Execution**
+- Scripts are the unit of cross-peer interaction.
+- A script includes the `caller` (originating peer), parameters, and logic.
+
+**Design Feature:**
+- A script sent to another peer is evaluated with both `caller` and `target` contexts available to the runtime.
+- Target peer decides whether to accept and how to interpret it.
+
+### 4. **Policy-Driven Acceptance**
+- Each context has policies determining:
+ - Which peers may send scripts
+ - Which actions are allowed
+
+**Example:** Policies written as declarative access control rules, tied to peer IDs, namespaces, or capabilities.
+
+### 5. **Open, Modifiable Logic**
+- Use an embedded domain-specific language (e.g. Rhai) that allows:
+ - Peer owners to define and inspect their logic
+ - Script modules to be composed, extended, or overridden
+
+### 6. **Worker Multiplexing**
+- Use a single worker binary that can handle one or many peer contexts.
+- The context is dynamically determined at runtime.
+
+**Design Note:**
+- All workers enforce namespacing, even when only one peer is active per process.
+- Supports both isolated (1 peer per worker) and shared (many peers per worker) deployments.
+
+## Optional Enhancements
+
+- Pluggable transport layer (WebSocket, HTTP/2, NATS, etc.)
+- Pluggable storage backends for namespace-mounting (FS, S3, SQLite, etc.)
+- Declarative schema binding between DSL and structured data
+
+This design enables decentralized application runtime control while supporting a scalable and secure execution model.
\ No newline at end of file
diff --git a/docs/rationale.md b/docs/rationale.md
new file mode 100644
index 0000000..1c0e385
--- /dev/null
+++ b/docs/rationale.md
@@ -0,0 +1,34 @@
+# Rethinking Backend Ownership
+
+## Motivation
+
+Modern applications are powered by backends that run on infrastructure and systems controlled by centralized entities. Whether it's social platforms, collaboration tools, or data-driven apps, the backend is almost always a black box — hosted, maintained, and operated by someone else.
+
+This has profound implications:
+
+- **Loss of autonomy:** Users are locked out of the logic, rules, and data structures that govern their digital experience.
+- **Opaque control:** Application behavior can change without the user’s consent — and often without visibility.
+- **Vendor lock-in:** Switching providers or migrating data is often non-trivial, risky, or impossible.
+- **Security and privacy risks:** Centralized backends present single points of failure and attack.
+
+In this model, users are not participants in their computing environment — they are clients of someone else's backend.
+
+## The Vision
+
+The purpose of this initiative is to invert that dynamic. We aim to establish a paradigm where users and organizations **own and control their own backend logic and data**, without sacrificing connectivity, collaboration, or scalability.
+
+This means:
+
+- **Local authority:** Each user or organization should have full control over how their backend behaves — what code runs, what data is stored, and who can access it.
+- **Portable and interoperable:** Ownership must not mean isolation. User-owned backends should be able to interact with one another on equal footing.
+- **Transparent logic:** Application behavior should be visible, inspectable, and modifiable by the user.
+- **Delegation, not dependence:** Users should be able to cooperate and interact by delegating execution to each other — not by relying on a central server.
+
+## What We Stand For
+
+- **Agency:** You control your digital environment.
+- **Decentralization:** No central chokepoint for computation or data.
+- **Modularity:** Users compose their backend behavior, not inherit it from a monolith.
+- **Resilience:** Systems should degrade gracefully, fail independently, and recover without central orchestration.
+
+This is about building a more equitable and open computing model — one where the backend serves you, not the other way around.
diff --git a/docs/system_requirements_specification.md b/docs/system_requirements_specification.md
new file mode 100644
index 0000000..e2a56c2
--- /dev/null
+++ b/docs/system_requirements_specification.md
@@ -0,0 +1,50 @@
+# System Requirements Specification
+
+## Objective
+
+To define the core requirements for a system that fulfills the goals of decentralized backend ownership — enabling individuals and organizations to control, operate, and interact through their own backend environments without relying on centralized infrastructure.
+
+## Functional Requirements
+
+### 1. **Isolated Execution Contexts**
+- Each user or peer must operate within a distinct, logically isolated execution context.
+- Contexts must not be able to interfere with each other's state or runtime.
+
+### 2. **Cross-Context Communication**
+- Peers must be able to initiate interactions with other peers.
+- Communication must include origin metadata (who initiated it), and be authorized by the target context.
+
+### 3. **Delegated Execution**
+- A peer must be able to send code or instructions to another peer for execution, under the recipient's policies.
+- The recipient must treat the execution as contextualized by the caller, but constrained by its own local rules.
+
+### 4. **Ownership of Logic and Data**
+- Users must be able to inspect, modify, and extend the logic that governs their backend.
+- Data storage and access policies must be under the control of the peer.
+
+### 5. **Composability and Modifiability**
+- System behavior must be defined by open, composable modules or scripts.
+- Users must be able to override default behavior or extend it with minimal coupling.
+
+## Non-Functional Requirements
+
+### 6. **Security and Isolation**
+- Scripts or instructions from external peers must be sandboxed and policy-checked.
+- Each execution context must enforce boundaries between data and logic.
+
+### 7. **Resilience and Redundancy**
+- Failure of one peer or node must not impact others.
+- Communication must be asynchronous and fault-tolerant.
+
+### 8. **Portability**
+- A peer’s logic and data must be portable across environments and host infrastructure.
+- No assumption of persistent centralized hosting.
+
+### 9. **Transparency**
+- All logic must be auditable by its owner.
+- Communications between peers must be observable and traceable.
+
+### 10. **Scalability**
+- The system must support large numbers of peer contexts, potentially hosted on shared infrastructure without compromising logical separation.
+
+These requirements define the baseline for any system that claims to decentralize backend control and empower users to operate their own programmable, connected environments.
diff --git a/examples/.gitkeep b/examples/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/examples/ourworld/README.md b/examples/ourworld/README.md
deleted file mode 100644
index 4eed443..0000000
--- a/examples/ourworld/README.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# OurWorld Example
-
-This directory contains a complete example demonstrating a simulated "OurWorld" network, consisting of multiple interconnected "circles" (nodes). Each circle runs its own WebSocket server and a Rhai script worker, all managed by a central launcher.
-
-This example is designed to showcase:
-1. **Multi-Circle Configuration**: How to define and configure multiple circles in a single `circles.json` file.
-2. **Programmatic Launching**: How to use the `launcher` library to start, manage, and monitor these circles from within a Rust application.
-3. **Dynamic Key Generation**: The launcher generates unique cryptographic keypairs for each circle upon startup.
-4. **Output Generation**: How to use the `--output` functionality to get a JSON file containing the connection details (public keys, WebSocket URLs, etc.) for each running circle.
-5. **Graceful Shutdown**: How the launcher handles a `Ctrl+C` signal to shut down all running circles cleanly.
-
-## Directory Contents
-
-- `circles.json`: The main configuration file that defines the 7 circles in the OurWorld network, including their names, ports, and associated Rhai scripts.
-- `scripts/`: This directory contains the individual Rhai scripts that define the behavior of each circle.
-- `ourworld_output.json` (Generated): This file is created after running the example and contains the runtime details of each circle.
-
-## How to Run the Example
-
-There are two ways to run this example, each demonstrating a different way to use the launcher.
-
-### 1. As a Root Example (Recommended)
-
-This method runs the launcher programmatically from the root of the workspace and is the simplest way to see the system in action. It uses the `examples/ourworld.rs` file.
-
-```sh
-# From the root of the workspace
-cargo run --example ourworld
-```
-
-### 2. As a Crate-Level Example
-
-This method runs a similar launcher, but as an example *within* the `launcher` crate itself. It uses the `src/launcher/examples/ourworld/main.rs` file. This is useful for testing the launcher in a more isolated context.
-
-```sh
-# Navigate to the launcher's crate directory
-cd src/launcher
-
-# Run the 'ourworld' example using cargo
-cargo run --example ourworld
-```
-
-### 3. Using the Launcher Binary
-
-This method uses the main `launcher` binary to run the configuration, which is useful for testing the command-line interface.
-
-```sh
-# From the root of the workspace
-cargo run -p launcher -- --config examples/ourworld/circles.json --output examples/ourworld/ourworld_output.json
-```
-
-## What to Expect
-
-When you run the example, you will see log output indicating that the launcher is starting up, followed by a table summarizing the running circles:
-
-```
-+-----------------+------------------------------------------------------------------+------------------------------------------+-----------------------+
-| Name | Public Key | Worker Queue | WS URL |
-+=================+==================================================================+==========================================+=======================+
-| OurWorld | 02... | rhai_tasks:02... | ws://127.0.0.1:9000/ws|
-+-----------------+------------------------------------------------------------------+------------------------------------------+-----------------------+
-| Dunia Cybercity | 03... | rhai_tasks:03... | ws://127.0.0.1:9001/ws|
-+-----------------+------------------------------------------------------------------+------------------------------------------+-----------------------+
-| ... (and so on for all 7 circles) |
-+-----------------+------------------------------------------------------------------+------------------------------------------+-----------------------+
-```
-
-The launcher will then wait for you to press `Ctrl+C` to initiate a graceful shutdown of all services.
diff --git a/examples/ourworld/circles.json b/examples/ourworld/circles.json
deleted file mode 100644
index 6dc805a..0000000
--- a/examples/ourworld/circles.json
+++ /dev/null
@@ -1,51 +0,0 @@
-[
- {
- "name": "Timur Gordon",
- "port": 9100,
- "script_path": "scripts/test_script.rhai",
- "public_key": "023b0a9d409506f41f5782353857dea6abc16ae4643661cd94d8155fdb498642e3",
- "secret_key": "7a7074c59ccfa3465686277e9e9da34867b36a1e256271b893b6c22fbc82929e"
- },
- {
- "name": "Kristof de Spiegeleer",
- "port": 9101,
- "script_path": "scripts/test_script.rhai",
- "public_key": "030b62236efa67855b3379a9d4add1facbe8a545bafa86e1d6fbac06caae5b5b12",
- "secret_key": "04225fbb41d8c397581d7ec19ded8aaf02d8b9daf27fed9617525e4f8114a382"
- },
- {
- "name": "OurWorld",
- "port": 9000,
- "script_path": "scripts/ourworld.rhai"
- },
- {
- "name": "Dunia Cybercity",
- "port": 9001,
- "script_path": "scripts/dunia_cybercity.rhai"
- },
- {
- "name": "Sikana",
- "port": 9002,
- "script_path": "scripts/sikana.rhai"
- },
- {
- "name": "Threefold",
- "port": 9003,
- "script_path": "scripts/threefold.rhai"
- },
- {
- "name": "Mbweni",
- "port": 9004,
- "script_path": "scripts/mbweni.rhai"
- },
- {
- "name": "Geomind",
- "port": 9005,
- "script_path": "scripts/geomind.rhai"
- },
- {
- "name": "Freezone",
- "port": 9006,
- "script_path": "scripts/freezone.rhai"
- }
-]
\ No newline at end of file
diff --git a/examples/ourworld/main.rs b/examples/ourworld/main.rs
deleted file mode 100644
index 50212b9..0000000
--- a/examples/ourworld/main.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-//! Example of launching multiple circles and outputting their details to a file.
-//!
-//! This example demonstrates how to use the launcher library to start circles
-//! programmatically, similar to how the `launcher` binary works.
-//!
-//! # Usage
-//!
-//! ```sh
-//! cargo run --example ourworld
-//! ```
-//!
-//! This will:
-//! 1. Read the `circles.json` file in the `examples/ourworld` directory.
-//! 2. Launch all 7 circles defined in the config.
-//! 3. Create a `ourworld_output.json` file in the same directory with the details.
-//! 4. The launcher will run until you stop it with Ctrl+C.
-
-use launcher::{run_launcher, Args, CircleConfig};
-use log::{error, info};
-use std::error::Error as StdError;
-use std::fs;
-use std::path::PathBuf;
-
-#[tokio::main]
-async fn main() -> Result<(), Box> {
- println!("--- Launching OurWorld Example Programmatically ---");
-
- // The example is now at the root of the `examples` directory,
- // so we can reference its assets directly.
- let example_dir = PathBuf::from("./examples/ourworld");
- let config_path = example_dir.join("circles.json");
- let output_path = example_dir.join("ourworld_output.json");
-
- println!("Using config file: {:?}", config_path);
- println!("Output will be written to: {:?}", output_path);
-
- // Manually construct the arguments instead of parsing from command line.
- // This is useful when embedding the launcher logic in another application.
- let args = Args {
- config_path: config_path.clone(),
- output: Some(output_path),
- debug: true, // Enable debug logging for the example
- verbose: 2, // Set verbosity to max
- };
-
- if !config_path.exists() {
- let msg = format!("Configuration file not found at {:?}", config_path);
- error!("{}", msg);
- return Err(msg.into());
- }
-
- let config_content = fs::read_to_string(&config_path)?;
-
- let mut circle_configs: Vec = match serde_json::from_str(&config_content) {
- Ok(configs) => configs,
- Err(e) => {
- error!(
- "Failed to parse {}: {}. Ensure it's a valid JSON array of CircleConfig.",
- config_path.display(),
- e
- );
- return Err(Box::new(e) as Box);
- }
- };
-
- // Make script paths relative to the project root by prepending the example directory path.
- for config in &mut circle_configs {
- if let Some(script_path) = &config.script_path {
- let full_script_path = example_dir.join(script_path);
- config.script_path = Some(full_script_path.to_string_lossy().into_owned());
- }
- }
-
- if circle_configs.is_empty() {
- info!(
- "No circle configurations found in {}. Exiting.",
- config_path.display()
- );
- return Ok(());
- }
-
- println!("Starting launcher... Press Ctrl+C to exit.");
-
- // The run_launcher function will setup logging, spawn circles, print the table,
- // and wait for a shutdown signal (Ctrl+C).
- run_launcher(args, circle_configs).await?;
-
- println!("--- OurWorld Example Finished ---");
- Ok(())
-}
diff --git a/examples/ourworld/ourworld_output.json b/examples/ourworld/ourworld_output.json
deleted file mode 100644
index 3a65f9e..0000000
--- a/examples/ourworld/ourworld_output.json
+++ /dev/null
@@ -1,65 +0,0 @@
-[
- {
- "name": "Timur Gordon",
- "public_key": "023b0a9d409506f41f5782353857dea6abc16ae4643661cd94d8155fdb498642e3",
- "secret_key": "7a7074c59ccfa3465686277e9e9da34867b36a1e256271b893b6c22fbc82929e",
- "worker_queue": "rhai_tasks:023b0a9d409506f41f5782353857dea6abc16ae4643661cd94d8155fdb498642e3",
- "ws_url": "ws://127.0.0.1:9100"
- },
- {
- "name": "Kristof de Spiegeleer",
- "public_key": "030b62236efa67855b3379a9d4add1facbe8a545bafa86e1d6fbac06caae5b5b12",
- "secret_key": "04225fbb41d8c397581d7ec19ded8aaf02d8b9daf27fed9617525e4f8114a382",
- "worker_queue": "rhai_tasks:030b62236efa67855b3379a9d4add1facbe8a545bafa86e1d6fbac06caae5b5b12",
- "ws_url": "ws://127.0.0.1:9101"
- },
- {
- "name": "OurWorld",
- "public_key": "02c9e7cb76e19adc3b579091d923ef273282070bc4864c1a07e94ca34a78aea8ef",
- "secret_key": "cb58d003eae0a5168b6f6fc4e822879b17e7b5987e5a76b870b75fe659e3cc60",
- "worker_queue": "rhai_tasks:02c9e7cb76e19adc3b579091d923ef273282070bc4864c1a07e94ca34a78aea8ef",
- "ws_url": "ws://127.0.0.1:9000"
- },
- {
- "name": "Dunia Cybercity",
- "public_key": "02a19f52bdde937a3f05c1bcc9b58c467d5084586a5a0b832617e131886c961771",
- "secret_key": "c2a9a0c35b7c85cadfaf9e3123829324e4b4b116239833123d73da14b13dfbde",
- "worker_queue": "rhai_tasks:02a19f52bdde937a3f05c1bcc9b58c467d5084586a5a0b832617e131886c961771",
- "ws_url": "ws://127.0.0.1:9001"
- },
- {
- "name": "Sikana",
- "public_key": "032db92879e51d5adf17a99df0cedba49d69e051ffb7b224e5a50e5d02a8f3c68f",
- "secret_key": "3718bc30c8d3ee1e88f1d88e06ed7637197c2f9b422a5757201acf572e9c7345",
- "worker_queue": "rhai_tasks:032db92879e51d5adf17a99df0cedba49d69e051ffb7b224e5a50e5d02a8f3c68f",
- "ws_url": "ws://127.0.0.1:9002"
- },
- {
- "name": "Threefold",
- "public_key": "021b452667f0c73a9f96c65dfceb0810e36109ad2408e0693a90fd4cdf7d8de0f6",
- "secret_key": "9e9532bdc279570f22b8bc853eadf8f7cdf5cacd3e506ae06b3a9f34778a372f",
- "worker_queue": "rhai_tasks:021b452667f0c73a9f96c65dfceb0810e36109ad2408e0693a90fd4cdf7d8de0f6",
- "ws_url": "ws://127.0.0.1:9003"
- },
- {
- "name": "Mbweni",
- "public_key": "029423b664660e9d9bcdbde244fda7d2064b17089463ddfb6eed301e34fb115969",
- "secret_key": "b70c510765d1a3e315871355fb6b902662f465ef317ddbabf146163ad8b83937",
- "worker_queue": "rhai_tasks:029423b664660e9d9bcdbde244fda7d2064b17089463ddfb6eed301e34fb115969",
- "ws_url": "ws://127.0.0.1:9004"
- },
- {
- "name": "Geomind",
- "public_key": "03af7abb8737dfb0d3e7eb22d4e57d5566a82be0ca7c4fe7f7cf1b37ad595044f7",
- "secret_key": "22a4b7514b3f88a697566f5c1aa12178e0974ae3f7ae6eb7054c4c9cbd30a8fa",
- "worker_queue": "rhai_tasks:03af7abb8737dfb0d3e7eb22d4e57d5566a82be0ca7c4fe7f7cf1b37ad595044f7",
- "ws_url": "ws://127.0.0.1:9005"
- },
- {
- "name": "Freezone",
- "public_key": "03a00761250dd79294ccbc4de916454736ff5a6488d8bb93c759d2dae5abf20b03",
- "secret_key": "a09d158e6f2b6706bca97a320bbc64b6278fe795820a0759f658f230fd071003",
- "worker_queue": "rhai_tasks:03a00761250dd79294ccbc4de916454736ff5a6488d8bb93c759d2dae5abf20b03",
- "ws_url": "ws://127.0.0.1:9006"
- }
-]
\ No newline at end of file
diff --git a/examples/ourworld/scripts/dunia_cybercity.rhai b/examples/ourworld/scripts/dunia_cybercity.rhai
deleted file mode 100644
index a084bb7..0000000
--- a/examples/ourworld/scripts/dunia_cybercity.rhai
+++ /dev/null
@@ -1,249 +0,0 @@
-// OurWorld Circle and Library Data
-
-new_circle()
- .title("Dunia Cybercity")
- .description("Creating a better world.")
- .ws_url("ws://localhost:8091/ws")
- .logo("🌍")
- .save_circle();
-
-let circle = get_circle();
-
-print("--- Creating OurWorld Library ---");
-
-// === IMAGES ===
-print("Creating images...");
-
-let nature1 = save_image(new_image()
- .title("Mountain Sunrise")
- .description("Breathtaking sunrise over mountain peaks")
- .url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
- .width(800).height(600));
-
-let nature2 = save_image(new_image()
- .title("Ocean Waves")
- .description("Powerful ocean waves crashing on rocks")
- .url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
- .width(800).height(600));
-
-let nature3 = save_image(new_image()
- .title("Forest Path")
- .description("Peaceful path through ancient forest")
- .url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
- .width(800).height(600));
-
-let tech1 = save_image(new_image()
- .title("Solar Panels")
- .description("Modern solar panel installation")
- .url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
- .width(800).height(600));
-
-let tech2 = save_image(new_image()
- .title("Wind Turbines")
- .description("Wind turbines generating clean energy")
- .url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
- .width(800).height(600));
-
-let space1 = save_image(new_image()
- .title("Earth from Space")
- .description("Our beautiful planet from orbit")
- .url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
- .width(800).height(600));
-
-let space2 = save_image(new_image()
- .title("Galaxy Spiral")
- .description("Stunning spiral galaxy in deep space")
- .url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
- .width(800).height(600));
-
-let city1 = save_image(new_image()
- .title("Smart City")
- .description("Futuristic smart city at night")
- .url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
- .width(800).height(600));
-
-// === PDFs ===
-print("Creating PDFs...");
-
-let pdf1 = save_pdf(new_pdf()
- .title("Climate Action Report 2024")
- .description("Comprehensive analysis of global climate initiatives")
- .url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
- .page_count(42));
-
-let pdf2 = save_pdf(new_pdf()
- .title("Sustainable Development Goals")
- .description("UN SDG implementation guide")
- .url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
- .page_count(35));
-
-let pdf3 = save_pdf(new_pdf()
- .title("Renewable Energy Handbook")
- .description("Technical guide to renewable energy systems")
- .url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
- .page_count(280));
-
-let pdf4 = save_pdf(new_pdf()
- .title("Blockchain for Good")
- .description("How blockchain technology can solve global challenges")
- .url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
- .page_count(24));
-
-let pdf5 = save_pdf(new_pdf()
- .title("Future of Work Report")
- .description("Analysis of changing work patterns and remote collaboration")
- .url("https://www.mckinsey.com/featured-insights/future-of-work")
- .page_count(156));
-
-// === MARKDOWN DOCUMENTS ===
-print("Creating markdown documents...");
-
-let md1 = save_markdown(new_markdown()
- .title("OurWorld Mission Statement")
- .description("Our vision for a better world")
- .content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
-
-let md2 = save_markdown(new_markdown()
- .title("Getting Started Guide")
- .description("How to join the OurWorld movement")
- .content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
-
-let md3 = save_markdown(new_markdown()
- .title("Technology Roadmap 2024")
- .description("Our technical development plans")
- .content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
-
-let md4 = save_markdown(new_markdown()
- .title("Community Guidelines")
- .description("How we work together")
- .content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
-
-
-let investor = new_contact()
- .name("Example Investor")
- .save_contact();
-
-let investors = new_group()
- .name("Investors")
- .description("A group for example inverstors of ourworld");
-
-investors.add_contact(investor.id)
- .save_group();
-
-// === BOOKS ===
-print("Creating books...");
-
-let sustainability_book = save_book(new_book()
- .title("Sustainability Handbook")
- .description("Complete guide to sustainable living and practices")
- .add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
- .add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
- .add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
- .add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
- .add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
- .add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
- .add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
- .add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
-
-let tech_guide_book = save_book(new_book()
- .title("Green Technology Guide")
- .description("Understanding and implementing green technologies")
- .add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
- .add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
- .add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
- .add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
- .add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
- .add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
-
-let community_book = save_book(new_book()
- .title("Building Communities")
- .description("Guide to creating sustainable and inclusive communities")
- .add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
- .add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
- .add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
- .add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
-
-// === SLIDES ===
-print("Creating slides...");
-
-let climate_slides = save_slides(new_slides()
- .title("Climate Change Awareness")
- .description("Visual presentation on climate change impacts and solutions")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200").title("Global Temperature Rise"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200").title("Melting Ice Caps"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200").title("Extreme Weather Events"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200").title("Renewable Energy Solutions"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200").title("Sustainable Transportation")));
-
-let innovation_slides = save_slides(new_slides()
- .title("Innovation Showcase")
- .description("Cutting-edge technologies for a sustainable future")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200").title("AI and Machine Learning"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200").title("Blockchain Technology"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200").title("IoT and Smart Cities"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200").title("Quantum Computing"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200").title("Biotechnology Advances")));
-
-let nature_slides = save_slides(new_slides()
- .title("Biodiversity Gallery")
- .description("Celebrating Earth's incredible biodiversity")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200").title("Tropical Rainforest"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200").title("Coral Reef Ecosystem"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200").title("Arctic Wildlife"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200").title("Mountain Ecosystems")));
-
-// === COLLECTIONS ===
-print("Creating collections...");
-
-let nature_collection = save_collection(new_collection()
- .title("Nature & Environment")
- .description("Beautiful images and resources about our natural world")
- .add_image(nature1.id)
- .add_image(nature2.id)
- .add_image(nature3.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id)
- .add_book(sustainability_book.id)
- .add_slides(nature_slides.id));
-
-let technology_collection = save_collection(new_collection()
- .title("Sustainable Technology")
- .description("Innovations driving positive change")
- .add_image(tech1.id)
- .add_image(tech2.id)
- .add_pdf(pdf3.id)
- .add_pdf(pdf4.id)
- .add_markdown(md3.id)
- .add_book(tech_guide_book.id)
- .add_slides(innovation_slides.id));
-
-let space_collection = save_collection(new_collection()
- .title("Space & Cosmos")
- .description("Exploring the universe and our place in it")
- .add_image(space1.id)
- .add_image(space2.id)
- .add_pdf(pdf2.id)
- .add_markdown(md2.id));
-
-let community_collection = save_collection(new_collection()
- .title("Community & Collaboration")
- .description("Building better communities together")
- .add_image(city1.id)
- .add_pdf(pdf5.id)
- .add_markdown(md4.id)
- .add_book(community_book.id));
-
-let climate_collection = save_collection(new_collection()
- .title("Climate Action")
- .description("Understanding and addressing climate change")
- .add_slides(climate_slides.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id));
-
-print("✅ OurWorld library created successfully!");
-print("📚 Collections: 5");
-print("🖼️ Images: 8");
-print("📄 PDFs: 5");
-print("📝 Markdown docs: 4");
-print("📖 Books: 3");
-print("🎞️ Slide shows: 3");
\ No newline at end of file
diff --git a/examples/ourworld/scripts/freezone.rhai b/examples/ourworld/scripts/freezone.rhai
deleted file mode 100644
index 1caef11..0000000
--- a/examples/ourworld/scripts/freezone.rhai
+++ /dev/null
@@ -1,249 +0,0 @@
-// OurWorld Circle and Library Data
-
-new_circle()
- .title("Zanzibar Digital Freezone")
- .description("Creating a better world.")
- .ws_url("ws://localhost:8096/ws")
- .logo("🌍")
- .save_circle();
-
-let circle = get_circle();
-
-print("--- Creating OurWorld Library ---");
-
-// === IMAGES ===
-print("Creating images...");
-
-let nature1 = save_image(new_image()
- .title("Mountain Sunrise")
- .description("Breathtaking sunrise over mountain peaks")
- .url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
- .width(800).height(600));
-
-let nature2 = save_image(new_image()
- .title("Ocean Waves")
- .description("Powerful ocean waves crashing on rocks")
- .url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
- .width(800).height(600));
-
-let nature3 = save_image(new_image()
- .title("Forest Path")
- .description("Peaceful path through ancient forest")
- .url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
- .width(800).height(600));
-
-let tech1 = save_image(new_image()
- .title("Solar Panels")
- .description("Modern solar panel installation")
- .url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
- .width(800).height(600));
-
-let tech2 = save_image(new_image()
- .title("Wind Turbines")
- .description("Wind turbines generating clean energy")
- .url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
- .width(800).height(600));
-
-let space1 = save_image(new_image()
- .title("Earth from Space")
- .description("Our beautiful planet from orbit")
- .url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
- .width(800).height(600));
-
-let space2 = save_image(new_image()
- .title("Galaxy Spiral")
- .description("Stunning spiral galaxy in deep space")
- .url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
- .width(800).height(600));
-
-let city1 = save_image(new_image()
- .title("Smart City")
- .description("Futuristic smart city at night")
- .url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
- .width(800).height(600));
-
-// === PDFs ===
-print("Creating PDFs...");
-
-let pdf1 = save_pdf(new_pdf()
- .title("Climate Action Report 2024")
- .description("Comprehensive analysis of global climate initiatives")
- .url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
- .page_count(42));
-
-let pdf2 = save_pdf(new_pdf()
- .title("Sustainable Development Goals")
- .description("UN SDG implementation guide")
- .url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
- .page_count(35));
-
-let pdf3 = save_pdf(new_pdf()
- .title("Renewable Energy Handbook")
- .description("Technical guide to renewable energy systems")
- .url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
- .page_count(280));
-
-let pdf4 = save_pdf(new_pdf()
- .title("Blockchain for Good")
- .description("How blockchain technology can solve global challenges")
- .url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
- .page_count(24));
-
-let pdf5 = save_pdf(new_pdf()
- .title("Future of Work Report")
- .description("Analysis of changing work patterns and remote collaboration")
- .url("https://www.mckinsey.com/featured-insights/future-of-work")
- .page_count(156));
-
-// === MARKDOWN DOCUMENTS ===
-print("Creating markdown documents...");
-
-let md1 = save_markdown(new_markdown()
- .title("OurWorld Mission Statement")
- .description("Our vision for a better world")
- .content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
-
-let md2 = save_markdown(new_markdown()
- .title("Getting Started Guide")
- .description("How to join the OurWorld movement")
- .content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
-
-let md3 = save_markdown(new_markdown()
- .title("Technology Roadmap 2024")
- .description("Our technical development plans")
- .content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
-
-let md4 = save_markdown(new_markdown()
- .title("Community Guidelines")
- .description("How we work together")
- .content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
-
-
-let investor = new_contact()
- .name("Example Investor")
- .save_contact();
-
-let investors = new_group()
- .name("Investors")
- .description("A group for example inverstors of ourworld");
-
-investors.add_contact(investor.id)
- .save_group();
-
-// === BOOKS ===
-print("Creating books...");
-
-let sustainability_book = save_book(new_book()
- .title("Sustainability Handbook")
- .description("Complete guide to sustainable living and practices")
- .add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
- .add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
- .add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
- .add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
- .add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
- .add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
- .add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
- .add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
-
-let tech_guide_book = save_book(new_book()
- .title("Green Technology Guide")
- .description("Understanding and implementing green technologies")
- .add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
- .add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
- .add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
- .add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
- .add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
- .add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
-
-let community_book = save_book(new_book()
- .title("Building Communities")
- .description("Guide to creating sustainable and inclusive communities")
- .add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
- .add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
- .add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
- .add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
-
-// === SLIDES ===
-print("Creating slides...");
-
-let climate_slides = save_slides(new_slides()
- .title("Climate Change Awareness")
- .description("Visual presentation on climate change impacts and solutions")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200").title("Global Temperature Rise"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200").title("Melting Ice Caps"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200").title("Extreme Weather Events"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200").title("Renewable Energy Solutions"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200").title("Sustainable Transportation")));
-
-let innovation_slides = save_slides(new_slides()
- .title("Innovation Showcase")
- .description("Cutting-edge technologies for a sustainable future")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200").title("AI and Machine Learning"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200").title("Blockchain Technology"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200").title("IoT and Smart Cities"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200").title("Quantum Computing"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200").title("Biotechnology Advances")));
-
-let nature_slides = save_slides(new_slides()
- .title("Biodiversity Gallery")
- .description("Celebrating Earth's incredible biodiversity")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200").title("Tropical Rainforest"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200").title("Coral Reef Ecosystem"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200").title("Arctic Wildlife"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200").title("Mountain Ecosystems")));
-
-// === COLLECTIONS ===
-print("Creating collections...");
-
-let nature_collection = save_collection(new_collection()
- .title("Nature & Environment")
- .description("Beautiful images and resources about our natural world")
- .add_image(nature1.id)
- .add_image(nature2.id)
- .add_image(nature3.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id)
- .add_book(sustainability_book.id)
- .add_slides(nature_slides.id));
-
-let technology_collection = save_collection(new_collection()
- .title("Sustainable Technology")
- .description("Innovations driving positive change")
- .add_image(tech1.id)
- .add_image(tech2.id)
- .add_pdf(pdf3.id)
- .add_pdf(pdf4.id)
- .add_markdown(md3.id)
- .add_book(tech_guide_book.id)
- .add_slides(innovation_slides.id));
-
-let space_collection = save_collection(new_collection()
- .title("Space & Cosmos")
- .description("Exploring the universe and our place in it")
- .add_image(space1.id)
- .add_image(space2.id)
- .add_pdf(pdf2.id)
- .add_markdown(md2.id));
-
-let community_collection = save_collection(new_collection()
- .title("Community & Collaboration")
- .description("Building better communities together")
- .add_image(city1.id)
- .add_pdf(pdf5.id)
- .add_markdown(md4.id)
- .add_book(community_book.id));
-
-let climate_collection = save_collection(new_collection()
- .title("Climate Action")
- .description("Understanding and addressing climate change")
- .add_slides(climate_slides.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id));
-
-print("✅ OurWorld library created successfully!");
-print("📚 Collections: 5");
-print("🖼️ Images: 8");
-print("📄 PDFs: 5");
-print("📝 Markdown docs: 4");
-print("📖 Books: 3");
-print("🎞️ Slide shows: 3");
\ No newline at end of file
diff --git a/examples/ourworld/scripts/geomind.rhai b/examples/ourworld/scripts/geomind.rhai
deleted file mode 100644
index 78d88e0..0000000
--- a/examples/ourworld/scripts/geomind.rhai
+++ /dev/null
@@ -1,249 +0,0 @@
-// OurWorld Circle and Library Data
-
-new_circle()
- .title("Geomind")
- .description("Creating a better world.")
- .ws_url("ws://localhost:8095/ws")
- .logo("🌍")
- .save_circle();
-
-let circle = get_circle();
-
-print("--- Creating OurWorld Library ---");
-
-// === IMAGES ===
-print("Creating images...");
-
-let nature1 = save_image(new_image()
- .title("Mountain Sunrise")
- .description("Breathtaking sunrise over mountain peaks")
- .url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
- .width(800).height(600));
-
-let nature2 = save_image(new_image()
- .title("Ocean Waves")
- .description("Powerful ocean waves crashing on rocks")
- .url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
- .width(800).height(600));
-
-let nature3 = save_image(new_image()
- .title("Forest Path")
- .description("Peaceful path through ancient forest")
- .url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
- .width(800).height(600));
-
-let tech1 = save_image(new_image()
- .title("Solar Panels")
- .description("Modern solar panel installation")
- .url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
- .width(800).height(600));
-
-let tech2 = save_image(new_image()
- .title("Wind Turbines")
- .description("Wind turbines generating clean energy")
- .url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
- .width(800).height(600));
-
-let space1 = save_image(new_image()
- .title("Earth from Space")
- .description("Our beautiful planet from orbit")
- .url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
- .width(800).height(600));
-
-let space2 = save_image(new_image()
- .title("Galaxy Spiral")
- .description("Stunning spiral galaxy in deep space")
- .url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
- .width(800).height(600));
-
-let city1 = save_image(new_image()
- .title("Smart City")
- .description("Futuristic smart city at night")
- .url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
- .width(800).height(600));
-
-// === PDFs ===
-print("Creating PDFs...");
-
-let pdf1 = save_pdf(new_pdf()
- .title("Climate Action Report 2024")
- .description("Comprehensive analysis of global climate initiatives")
- .url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
- .page_count(42));
-
-let pdf2 = save_pdf(new_pdf()
- .title("Sustainable Development Goals")
- .description("UN SDG implementation guide")
- .url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
- .page_count(35));
-
-let pdf3 = save_pdf(new_pdf()
- .title("Renewable Energy Handbook")
- .description("Technical guide to renewable energy systems")
- .url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
- .page_count(280));
-
-let pdf4 = save_pdf(new_pdf()
- .title("Blockchain for Good")
- .description("How blockchain technology can solve global challenges")
- .url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
- .page_count(24));
-
-let pdf5 = save_pdf(new_pdf()
- .title("Future of Work Report")
- .description("Analysis of changing work patterns and remote collaboration")
- .url("https://www.mckinsey.com/featured-insights/future-of-work")
- .page_count(156));
-
-// === MARKDOWN DOCUMENTS ===
-print("Creating markdown documents...");
-
-let md1 = save_markdown(new_markdown()
- .title("OurWorld Mission Statement")
- .description("Our vision for a better world")
- .content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
-
-let md2 = save_markdown(new_markdown()
- .title("Getting Started Guide")
- .description("How to join the OurWorld movement")
- .content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
-
-let md3 = save_markdown(new_markdown()
- .title("Technology Roadmap 2024")
- .description("Our technical development plans")
- .content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
-
-let md4 = save_markdown(new_markdown()
- .title("Community Guidelines")
- .description("How we work together")
- .content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
-
-
-let investor = new_contact()
- .name("Example Investor")
- .save_contact();
-
-let investors = new_group()
- .name("Investors")
- .description("A group for example inverstors of ourworld");
-
-investors.add_contact(investor.id)
- .save_group();
-
-// === BOOKS ===
-print("Creating books...");
-
-let sustainability_book = save_book(new_book()
- .title("Sustainability Handbook")
- .description("Complete guide to sustainable living and practices")
- .add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
- .add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
- .add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
- .add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
- .add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
- .add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
- .add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
- .add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
-
-let tech_guide_book = save_book(new_book()
- .title("Green Technology Guide")
- .description("Understanding and implementing green technologies")
- .add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
- .add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
- .add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
- .add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
- .add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
- .add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
-
-let community_book = save_book(new_book()
- .title("Building Communities")
- .description("Guide to creating sustainable and inclusive communities")
- .add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
- .add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
- .add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
- .add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
-
-// === SLIDES ===
-print("Creating slides...");
-
-let climate_slides = save_slides(new_slides()
- .title("Climate Change Awareness")
- .description("Visual presentation on climate change impacts and solutions")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200").title("Global Temperature Rise"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200").title("Melting Ice Caps"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200").title("Extreme Weather Events"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200").title("Renewable Energy Solutions"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200").title("Sustainable Transportation")));
-
-let innovation_slides = save_slides(new_slides()
- .title("Innovation Showcase")
- .description("Cutting-edge technologies for a sustainable future")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200").title("AI and Machine Learning"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200").title("Blockchain Technology"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200").title("IoT and Smart Cities"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200").title("Quantum Computing"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200").title("Biotechnology Advances")));
-
-let nature_slides = save_slides(new_slides()
- .title("Biodiversity Gallery")
- .description("Celebrating Earth's incredible biodiversity")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200").title("Tropical Rainforest"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200").title("Coral Reef Ecosystem"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200").title("Arctic Wildlife"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200").title("Mountain Ecosystems")));
-
-// === COLLECTIONS ===
-print("Creating collections...");
-
-let nature_collection = save_collection(new_collection()
- .title("Nature & Environment")
- .description("Beautiful images and resources about our natural world")
- .add_image(nature1.id)
- .add_image(nature2.id)
- .add_image(nature3.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id)
- .add_book(sustainability_book.id)
- .add_slides(nature_slides.id));
-
-let technology_collection = save_collection(new_collection()
- .title("Sustainable Technology")
- .description("Innovations driving positive change")
- .add_image(tech1.id)
- .add_image(tech2.id)
- .add_pdf(pdf3.id)
- .add_pdf(pdf4.id)
- .add_markdown(md3.id)
- .add_book(tech_guide_book.id)
- .add_slides(innovation_slides.id));
-
-let space_collection = save_collection(new_collection()
- .title("Space & Cosmos")
- .description("Exploring the universe and our place in it")
- .add_image(space1.id)
- .add_image(space2.id)
- .add_pdf(pdf2.id)
- .add_markdown(md2.id));
-
-let community_collection = save_collection(new_collection()
- .title("Community & Collaboration")
- .description("Building better communities together")
- .add_image(city1.id)
- .add_pdf(pdf5.id)
- .add_markdown(md4.id)
- .add_book(community_book.id));
-
-let climate_collection = save_collection(new_collection()
- .title("Climate Action")
- .description("Understanding and addressing climate change")
- .add_slides(climate_slides.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id));
-
-print("✅ OurWorld library created successfully!");
-print("📚 Collections: 5");
-print("🖼️ Images: 8");
-print("📄 PDFs: 5");
-print("📝 Markdown docs: 4");
-print("📖 Books: 3");
-print("🎞️ Slide shows: 3");
\ No newline at end of file
diff --git a/examples/ourworld/scripts/kristof.rhai b/examples/ourworld/scripts/kristof.rhai
deleted file mode 100644
index 6e1cffe..0000000
--- a/examples/ourworld/scripts/kristof.rhai
+++ /dev/null
@@ -1,15 +0,0 @@
-// OurWorld Circle and Library Data
-
-new_circle()
- .title("Kristof de Spiegeleer")
- .description("Creating a better world.")
- .ws_url("ws://localhost:9101/ws")
- .add_circle("ws://localhost:9000/ws")
- .add_circle("ws://localhost:9001/ws")
- .add_circle("ws://localhost:9002/ws")
- .add_circle("ws://localhost:9003/ws")
- .add_circle("ws://localhost:9004/ws")
- .add_circle("ws://localhost:9005/ws")
- .add_circle("ws://localhost:8096/ws")
- .logo("🌍")
- .save_circle();
diff --git a/examples/ourworld/scripts/mbweni.rhai b/examples/ourworld/scripts/mbweni.rhai
deleted file mode 100644
index cdf76e2..0000000
--- a/examples/ourworld/scripts/mbweni.rhai
+++ /dev/null
@@ -1,249 +0,0 @@
-// OurWorld Circle and Library Data
-
-new_circle()
- .title("Mbweni Ruins & Gardens")
- .description("Mbweni ruins and Gardens")
- .ws_url("ws://localhost:8094/ws")
- .logo("🌍")
- .save_circle();
-
-let circle = get_circle();
-
-print("--- Creating OurWorld Library ---");
-
-// === IMAGES ===
-print("Creating images...");
-
-let nature1 = save_image(new_image()
- .title("Mountain Sunrise")
- .description("Breathtaking sunrise over mountain peaks")
- .url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
- .width(800).height(600));
-
-let nature2 = save_image(new_image()
- .title("Ocean Waves")
- .description("Powerful ocean waves crashing on rocks")
- .url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
- .width(800).height(600));
-
-let nature3 = save_image(new_image()
- .title("Forest Path")
- .description("Peaceful path through ancient forest")
- .url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
- .width(800).height(600));
-
-let tech1 = save_image(new_image()
- .title("Solar Panels")
- .description("Modern solar panel installation")
- .url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
- .width(800).height(600));
-
-let tech2 = save_image(new_image()
- .title("Wind Turbines")
- .description("Wind turbines generating clean energy")
- .url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
- .width(800).height(600));
-
-let space1 = save_image(new_image()
- .title("Earth from Space")
- .description("Our beautiful planet from orbit")
- .url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
- .width(800).height(600));
-
-let space2 = save_image(new_image()
- .title("Galaxy Spiral")
- .description("Stunning spiral galaxy in deep space")
- .url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
- .width(800).height(600));
-
-let city1 = save_image(new_image()
- .title("Smart City")
- .description("Futuristic smart city at night")
- .url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
- .width(800).height(600));
-
-// === PDFs ===
-print("Creating PDFs...");
-
-let pdf1 = save_pdf(new_pdf()
- .title("Climate Action Report 2024")
- .description("Comprehensive analysis of global climate initiatives")
- .url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
- .page_count(42));
-
-let pdf2 = save_pdf(new_pdf()
- .title("Sustainable Development Goals")
- .description("UN SDG implementation guide")
- .url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
- .page_count(35));
-
-let pdf3 = save_pdf(new_pdf()
- .title("Renewable Energy Handbook")
- .description("Technical guide to renewable energy systems")
- .url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
- .page_count(280));
-
-let pdf4 = save_pdf(new_pdf()
- .title("Blockchain for Good")
- .description("How blockchain technology can solve global challenges")
- .url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
- .page_count(24));
-
-let pdf5 = save_pdf(new_pdf()
- .title("Future of Work Report")
- .description("Analysis of changing work patterns and remote collaboration")
- .url("https://www.mckinsey.com/featured-insights/future-of-work")
- .page_count(156));
-
-// === MARKDOWN DOCUMENTS ===
-print("Creating markdown documents...");
-
-let md1 = save_markdown(new_markdown()
- .title("OurWorld Mission Statement")
- .description("Our vision for a better world")
- .content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
-
-let md2 = save_markdown(new_markdown()
- .title("Getting Started Guide")
- .description("How to join the OurWorld movement")
- .content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
-
-let md3 = save_markdown(new_markdown()
- .title("Technology Roadmap 2024")
- .description("Our technical development plans")
- .content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
-
-let md4 = save_markdown(new_markdown()
- .title("Community Guidelines")
- .description("How we work together")
- .content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
-
-
-let investor = new_contact()
- .name("Example Investor")
- .save_contact();
-
-let investors = new_group()
- .name("Investors")
- .description("A group for example inverstors of ourworld");
-
-investors.add_contact(investor.id)
- .save_group();
-
-// === BOOKS ===
-print("Creating books...");
-
-let sustainability_book = save_book(new_book()
- .title("Sustainability Handbook")
- .description("Complete guide to sustainable living and practices")
- .add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
- .add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
- .add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
- .add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
- .add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
- .add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
- .add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
- .add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
-
-let tech_guide_book = save_book(new_book()
- .title("Green Technology Guide")
- .description("Understanding and implementing green technologies")
- .add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
- .add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
- .add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
- .add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
- .add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
- .add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
-
-let community_book = save_book(new_book()
- .title("Building Communities")
- .description("Guide to creating sustainable and inclusive communities")
- .add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
- .add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
- .add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
- .add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
-
-// === SLIDES ===
-print("Creating slides...");
-
-let climate_slides = save_slides(new_slides()
- .title("Climate Change Awareness")
- .description("Visual presentation on climate change impacts and solutions")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200").title("Global Temperature Rise"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200").title("Melting Ice Caps"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200").title("Extreme Weather Events"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200").title("Renewable Energy Solutions"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200").title("Sustainable Transportation")));
-
-let innovation_slides = save_slides(new_slides()
- .title("Innovation Showcase")
- .description("Cutting-edge technologies for a sustainable future")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200").title("AI and Machine Learning"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200").title("Blockchain Technology"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200").title("IoT and Smart Cities"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200").title("Quantum Computing"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200").title("Biotechnology Advances")));
-
-let nature_slides = save_slides(new_slides()
- .title("Biodiversity Gallery")
- .description("Celebrating Earth's incredible biodiversity")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200").title("Tropical Rainforest"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200").title("Coral Reef Ecosystem"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200").title("Arctic Wildlife"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200").title("Mountain Ecosystems")));
-
-// === COLLECTIONS ===
-print("Creating collections...");
-
-let nature_collection = save_collection(new_collection()
- .title("Nature & Environment")
- .description("Beautiful images and resources about our natural world")
- .add_image(nature1.id)
- .add_image(nature2.id)
- .add_image(nature3.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id)
- .add_book(sustainability_book.id)
- .add_slides(nature_slides.id));
-
-let technology_collection = save_collection(new_collection()
- .title("Sustainable Technology")
- .description("Innovations driving positive change")
- .add_image(tech1.id)
- .add_image(tech2.id)
- .add_pdf(pdf3.id)
- .add_pdf(pdf4.id)
- .add_markdown(md3.id)
- .add_book(tech_guide_book.id)
- .add_slides(innovation_slides.id));
-
-let space_collection = save_collection(new_collection()
- .title("Space & Cosmos")
- .description("Exploring the universe and our place in it")
- .add_image(space1.id)
- .add_image(space2.id)
- .add_pdf(pdf2.id)
- .add_markdown(md2.id));
-
-let community_collection = save_collection(new_collection()
- .title("Community & Collaboration")
- .description("Building better communities together")
- .add_image(city1.id)
- .add_pdf(pdf5.id)
- .add_markdown(md4.id)
- .add_book(community_book.id));
-
-let climate_collection = save_collection(new_collection()
- .title("Climate Action")
- .description("Understanding and addressing climate change")
- .add_slides(climate_slides.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id));
-
-print("✅ OurWorld library created successfully!");
-print("📚 Collections: 5");
-print("🖼️ Images: 8");
-print("📄 PDFs: 5");
-print("📝 Markdown docs: 4");
-print("📖 Books: 3");
-print("🎞️ Slide shows: 3");
\ No newline at end of file
diff --git a/examples/ourworld/scripts/ourworld.rhai b/examples/ourworld/scripts/ourworld.rhai
deleted file mode 100644
index 4b9ad79..0000000
--- a/examples/ourworld/scripts/ourworld.rhai
+++ /dev/null
@@ -1,257 +0,0 @@
-// OurWorld Circle and Library Data
-
-new_circle()
- .title("Ourworld")
- .description("Creating a better world.")
- .ws_url("ws://localhost:9000/ws")
- .add_circle("ws://localhost:9001/ws")
- .add_circle("ws://localhost:9002/ws")
- .add_circle("ws://localhost:9003/ws")
- .add_circle("ws://localhost:9004/ws")
- .add_circle("ws://localhost:9005/ws")
- .add_circle("ws://localhost:8096/ws")
- .add_member("023b0a9d409506f41f5782353857dea6abc16ae4643661cd94d8155fdb498642e3")
- .add_member("030b62236efa67855b3379a9d4add1facbe8a545bafa86e1d6fbac06caae5b5b12")
- .logo("🌍")
- .save_circle();
-
-let circle = get_circle();
-
-print("--- Creating OurWorld Library ---");
-
-// === IMAGES ===
-print("Creating images...");
-
-let nature1 = save_image(new_image()
- .title("Mountain Sunrise")
- .description("Breathtaking sunrise over mountain peaks")
- .url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
- .width(800).height(600));
-
-let nature2 = save_image(new_image()
- .title("Ocean Waves")
- .description("Powerful ocean waves crashing on rocks")
- .url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
- .width(800).height(600));
-
-let nature3 = save_image(new_image()
- .title("Forest Path")
- .description("Peaceful path through ancient forest")
- .url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
- .width(800).height(600));
-
-let tech1 = save_image(new_image()
- .title("Solar Panels")
- .description("Modern solar panel installation")
- .url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
- .width(800).height(600));
-
-let tech2 = save_image(new_image()
- .title("Wind Turbines")
- .description("Wind turbines generating clean energy")
- .url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
- .width(800).height(600));
-
-let space1 = save_image(new_image()
- .title("Earth from Space")
- .description("Our beautiful planet from orbit")
- .url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
- .width(800).height(600));
-
-let space2 = save_image(new_image()
- .title("Galaxy Spiral")
- .description("Stunning spiral galaxy in deep space")
- .url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
- .width(800).height(600));
-
-let city1 = save_image(new_image()
- .title("Smart City")
- .description("Futuristic smart city at night")
- .url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
- .width(800).height(600));
-
-// === PDFs ===
-print("Creating PDFs...");
-
-let pdf1 = save_pdf(new_pdf()
- .title("Climate Action Report 2024")
- .description("Comprehensive analysis of global climate initiatives")
- .url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
- .page_count(42));
-
-let pdf2 = save_pdf(new_pdf()
- .title("Sustainable Development Goals")
- .description("UN SDG implementation guide")
- .url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
- .page_count(35));
-
-let pdf3 = save_pdf(new_pdf()
- .title("Renewable Energy Handbook")
- .description("Technical guide to renewable energy systems")
- .url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
- .page_count(280));
-
-let pdf4 = save_pdf(new_pdf()
- .title("Blockchain for Good")
- .description("How blockchain technology can solve global challenges")
- .url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
- .page_count(24));
-
-let pdf5 = save_pdf(new_pdf()
- .title("Future of Work Report")
- .description("Analysis of changing work patterns and remote collaboration")
- .url("https://www.mckinsey.com/featured-insights/future-of-work")
- .page_count(156));
-
-// === MARKDOWN DOCUMENTS ===
-print("Creating markdown documents...");
-
-let md1 = save_markdown(new_markdown()
- .title("OurWorld Mission Statement")
- .description("Our vision for a better world")
- .content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
-
-let md2 = save_markdown(new_markdown()
- .title("Getting Started Guide")
- .description("How to join the OurWorld movement")
- .content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
-
-let md3 = save_markdown(new_markdown()
- .title("Technology Roadmap 2024")
- .description("Our technical development plans")
- .content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
-
-let md4 = save_markdown(new_markdown()
- .title("Community Guidelines")
- .description("How we work together")
- .content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
-
-
-let investor = new_contact()
- .name("Example Investor")
- .save_contact();
-
-let investors = new_group()
- .name("Investors")
- .description("A group for example inverstors of ourworld");
-
-investors.add_contact(investor.id)
- .save_group();
-
-// === BOOKS ===
-print("Creating books...");
-
-let sustainability_book = save_book(new_book()
- .title("Sustainability Handbook")
- .description("Complete guide to sustainable living and practices")
- .add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
- .add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
- .add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
- .add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
- .add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
- .add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
- .add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
- .add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
-
-let tech_guide_book = save_book(new_book()
- .title("Green Technology Guide")
- .description("Understanding and implementing green technologies")
- .add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
- .add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
- .add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
- .add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
- .add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
- .add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
-
-let community_book = save_book(new_book()
- .title("Building Communities")
- .description("Guide to creating sustainable and inclusive communities")
- .add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
- .add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
- .add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
- .add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
-
-// === SLIDES ===
-print("Creating slides...");
-
-let climate_slides = save_slides(new_slides()
- .title("Climate Change Awareness")
- .description("Visual presentation on climate change impacts and solutions")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200").title("Global Temperature Rise"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200").title("Melting Ice Caps"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200").title("Extreme Weather Events"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200").title("Renewable Energy Solutions"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200").title("Sustainable Transportation")));
-
-let innovation_slides = save_slides(new_slides()
- .title("Innovation Showcase")
- .description("Cutting-edge technologies for a sustainable future")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200").title("AI and Machine Learning"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200").title("Blockchain Technology"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200").title("IoT and Smart Cities"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200").title("Quantum Computing"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200").title("Biotechnology Advances")));
-
-let nature_slides = save_slides(new_slides()
- .title("Biodiversity Gallery")
- .description("Celebrating Earth's incredible biodiversity")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200").title("Tropical Rainforest"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200").title("Coral Reef Ecosystem"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200").title("Arctic Wildlife"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200").title("Mountain Ecosystems")));
-
-// === COLLECTIONS ===
-print("Creating collections...");
-
-let nature_collection = save_collection(new_collection()
- .title("Nature & Environment")
- .description("Beautiful images and resources about our natural world")
- .add_image(nature1.id)
- .add_image(nature2.id)
- .add_image(nature3.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id)
- .add_book(sustainability_book.id)
- .add_slides(nature_slides.id));
-
-let technology_collection = save_collection(new_collection()
- .title("Sustainable Technology")
- .description("Innovations driving positive change")
- .add_image(tech1.id)
- .add_image(tech2.id)
- .add_pdf(pdf3.id)
- .add_pdf(pdf4.id)
- .add_markdown(md3.id)
- .add_book(tech_guide_book.id)
- .add_slides(innovation_slides.id));
-
-let space_collection = save_collection(new_collection()
- .title("Space & Cosmos")
- .description("Exploring the universe and our place in it")
- .add_image(space1.id)
- .add_image(space2.id)
- .add_pdf(pdf2.id)
- .add_markdown(md2.id));
-
-let community_collection = save_collection(new_collection()
- .title("Community & Collaboration")
- .description("Building better communities together")
- .add_image(city1.id)
- .add_pdf(pdf5.id)
- .add_markdown(md4.id)
- .add_book(community_book.id));
-
-let climate_collection = save_collection(new_collection()
- .title("Climate Action")
- .description("Understanding and addressing climate change")
- .add_slides(climate_slides.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id));
-
-print("✅ OurWorld library created successfully!");
-print("📚 Collections: 5");
-print("🖼️ Images: 8");
-print("📄 PDFs: 5");
-print("📝 Markdown docs: 4");
-print("📖 Books: 3");
-print("🎞️ Slide shows: 3");
\ No newline at end of file
diff --git a/examples/ourworld/scripts/sikana.rhai b/examples/ourworld/scripts/sikana.rhai
deleted file mode 100644
index 9a17032..0000000
--- a/examples/ourworld/scripts/sikana.rhai
+++ /dev/null
@@ -1,249 +0,0 @@
-// OurWorld Circle and Library Data
-
-new_circle()
- .title("Sikana")
- .description("Creating a better world.")
- .ws_url("ws://localhost:9002/ws")
- .logo("🌍")
- .save_circle();
-
-let circle = get_circle();
-
-print("--- Creating OurWorld Library ---");
-
-// === IMAGES ===
-print("Creating images...");
-
-let nature1 = save_image(new_image()
- .title("Mountain Sunrise")
- .description("Breathtaking sunrise over mountain peaks")
- .url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
- .width(800).height(600));
-
-let nature2 = save_image(new_image()
- .title("Ocean Waves")
- .description("Powerful ocean waves crashing on rocks")
- .url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
- .width(800).height(600));
-
-let nature3 = save_image(new_image()
- .title("Forest Path")
- .description("Peaceful path through ancient forest")
- .url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
- .width(800).height(600));
-
-let tech1 = save_image(new_image()
- .title("Solar Panels")
- .description("Modern solar panel installation")
- .url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
- .width(800).height(600));
-
-let tech2 = save_image(new_image()
- .title("Wind Turbines")
- .description("Wind turbines generating clean energy")
- .url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
- .width(800).height(600));
-
-let space1 = save_image(new_image()
- .title("Earth from Space")
- .description("Our beautiful planet from orbit")
- .url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
- .width(800).height(600));
-
-let space2 = save_image(new_image()
- .title("Galaxy Spiral")
- .description("Stunning spiral galaxy in deep space")
- .url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
- .width(800).height(600));
-
-let city1 = save_image(new_image()
- .title("Smart City")
- .description("Futuristic smart city at night")
- .url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
- .width(800).height(600));
-
-// === PDFs ===
-print("Creating PDFs...");
-
-let pdf1 = save_pdf(new_pdf()
- .title("Climate Action Report 2024")
- .description("Comprehensive analysis of global climate initiatives")
- .url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
- .page_count(42));
-
-let pdf2 = save_pdf(new_pdf()
- .title("Sustainable Development Goals")
- .description("UN SDG implementation guide")
- .url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
- .page_count(35));
-
-let pdf3 = save_pdf(new_pdf()
- .title("Renewable Energy Handbook")
- .description("Technical guide to renewable energy systems")
- .url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
- .page_count(280));
-
-let pdf4 = save_pdf(new_pdf()
- .title("Blockchain for Good")
- .description("How blockchain technology can solve global challenges")
- .url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
- .page_count(24));
-
-let pdf5 = save_pdf(new_pdf()
- .title("Future of Work Report")
- .description("Analysis of changing work patterns and remote collaboration")
- .url("https://www.mckinsey.com/featured-insights/future-of-work")
- .page_count(156));
-
-// === MARKDOWN DOCUMENTS ===
-print("Creating markdown documents...");
-
-let md1 = save_markdown(new_markdown()
- .title("OurWorld Mission Statement")
- .description("Our vision for a better world")
- .content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
-
-let md2 = save_markdown(new_markdown()
- .title("Getting Started Guide")
- .description("How to join the OurWorld movement")
- .content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
-
-let md3 = save_markdown(new_markdown()
- .title("Technology Roadmap 2024")
- .description("Our technical development plans")
- .content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
-
-let md4 = save_markdown(new_markdown()
- .title("Community Guidelines")
- .description("How we work together")
- .content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
-
-
-let investor = new_contact()
- .name("Example Investor")
- .save_contact();
-
-let investors = new_group()
- .name("Investors")
- .description("A group for example inverstors of ourworld");
-
-investors.add_contact(investor.id)
- .save_group();
-
-// === BOOKS ===
-print("Creating books...");
-
-let sustainability_book = save_book(new_book()
- .title("Sustainability Handbook")
- .description("Complete guide to sustainable living and practices")
- .add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
- .add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
- .add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
- .add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
- .add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
- .add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
- .add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
- .add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
-
-let tech_guide_book = save_book(new_book()
- .title("Green Technology Guide")
- .description("Understanding and implementing green technologies")
- .add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
- .add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
- .add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
- .add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
- .add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
- .add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
-
-let community_book = save_book(new_book()
- .title("Building Communities")
- .description("Guide to creating sustainable and inclusive communities")
- .add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
- .add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
- .add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
- .add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
-
-// === SLIDES ===
-print("Creating slides...");
-
-let climate_slides = save_slides(new_slides()
- .title("Climate Change Awareness")
- .description("Visual presentation on climate change impacts and solutions")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200").title("Global Temperature Rise"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200").title("Melting Ice Caps"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200").title("Extreme Weather Events"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200").title("Renewable Energy Solutions"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200").title("Sustainable Transportation")));
-
-let innovation_slides = save_slides(new_slides()
- .title("Innovation Showcase")
- .description("Cutting-edge technologies for a sustainable future")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200").title("AI and Machine Learning"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200").title("Blockchain Technology"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200").title("IoT and Smart Cities"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200").title("Quantum Computing"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200").title("Biotechnology Advances")));
-
-let nature_slides = save_slides(new_slides()
- .title("Biodiversity Gallery")
- .description("Celebrating Earth's incredible biodiversity")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200").title("Tropical Rainforest"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200").title("Coral Reef Ecosystem"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200").title("Arctic Wildlife"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200").title("Mountain Ecosystems")));
-
-// === COLLECTIONS ===
-print("Creating collections...");
-
-let nature_collection = save_collection(new_collection()
- .title("Nature & Environment")
- .description("Beautiful images and resources about our natural world")
- .add_image(nature1.id)
- .add_image(nature2.id)
- .add_image(nature3.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id)
- .add_book(sustainability_book.id)
- .add_slides(nature_slides.id));
-
-let technology_collection = save_collection(new_collection()
- .title("Sustainable Technology")
- .description("Innovations driving positive change")
- .add_image(tech1.id)
- .add_image(tech2.id)
- .add_pdf(pdf3.id)
- .add_pdf(pdf4.id)
- .add_markdown(md3.id)
- .add_book(tech_guide_book.id)
- .add_slides(innovation_slides.id));
-
-let space_collection = save_collection(new_collection()
- .title("Space & Cosmos")
- .description("Exploring the universe and our place in it")
- .add_image(space1.id)
- .add_image(space2.id)
- .add_pdf(pdf2.id)
- .add_markdown(md2.id));
-
-let community_collection = save_collection(new_collection()
- .title("Community & Collaboration")
- .description("Building better communities together")
- .add_image(city1.id)
- .add_pdf(pdf5.id)
- .add_markdown(md4.id)
- .add_book(community_book.id));
-
-let climate_collection = save_collection(new_collection()
- .title("Climate Action")
- .description("Understanding and addressing climate change")
- .add_slides(climate_slides.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id));
-
-print("✅ OurWorld library created successfully!");
-print("📚 Collections: 5");
-print("🖼️ Images: 8");
-print("📄 PDFs: 5");
-print("📝 Markdown docs: 4");
-print("📖 Books: 3");
-print("🎞️ Slide shows: 3");
\ No newline at end of file
diff --git a/examples/ourworld/scripts/threefold.rhai b/examples/ourworld/scripts/threefold.rhai
deleted file mode 100644
index f2c2357..0000000
--- a/examples/ourworld/scripts/threefold.rhai
+++ /dev/null
@@ -1,249 +0,0 @@
-// OurWorld Circle and Library Data
-
-new_circle()
- .title("Threefold DMCC")
- .description("Creating a better world.")
- .ws_url("ws://localhost:8093/ws")
- .logo("🌍")
- .save_circle();
-
-let circle = get_circle();
-
-print("--- Creating OurWorld Library ---");
-
-// === IMAGES ===
-print("Creating images...");
-
-let nature1 = save_image(new_image()
- .title("Mountain Sunrise")
- .description("Breathtaking sunrise over mountain peaks")
- .url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800")
- .width(800).height(600));
-
-let nature2 = save_image(new_image()
- .title("Ocean Waves")
- .description("Powerful ocean waves crashing on rocks")
- .url("https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800")
- .width(800).height(600));
-
-let nature3 = save_image(new_image()
- .title("Forest Path")
- .description("Peaceful path through ancient forest")
- .url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800")
- .width(800).height(600));
-
-let tech1 = save_image(new_image()
- .title("Solar Panels")
- .description("Modern solar panel installation")
- .url("https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800")
- .width(800).height(600));
-
-let tech2 = save_image(new_image()
- .title("Wind Turbines")
- .description("Wind turbines generating clean energy")
- .url("https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800")
- .width(800).height(600));
-
-let space1 = save_image(new_image()
- .title("Earth from Space")
- .description("Our beautiful planet from orbit")
- .url("https://images.unsplash.com/photo-1446776877081-d282a0f896e2?w=800")
- .width(800).height(600));
-
-let space2 = save_image(new_image()
- .title("Galaxy Spiral")
- .description("Stunning spiral galaxy in deep space")
- .url("https://images.unsplash.com/photo-1502134249126-9f3755a50d78?w=800")
- .width(800).height(600));
-
-let city1 = save_image(new_image()
- .title("Smart City")
- .description("Futuristic smart city at night")
- .url("https://images.unsplash.com/photo-1480714378408-67cf0d13bc1f?w=800")
- .width(800).height(600));
-
-// === PDFs ===
-print("Creating PDFs...");
-
-let pdf1 = save_pdf(new_pdf()
- .title("Climate Action Report 2024")
- .description("Comprehensive analysis of global climate initiatives")
- .url("https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_summary-for-policymakers.pdf")
- .page_count(42));
-
-let pdf2 = save_pdf(new_pdf()
- .title("Sustainable Development Goals")
- .description("UN SDG implementation guide")
- .url("https://sdgs.un.org/sites/default/files/publications/21252030%20Agenda%20for%20Sustainable%20Development%20web.pdf")
- .page_count(35));
-
-let pdf3 = save_pdf(new_pdf()
- .title("Renewable Energy Handbook")
- .description("Technical guide to renewable energy systems")
- .url("https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2019/Oct/IRENA_Renewable-Energy-Statistics-2019.pdf")
- .page_count(280));
-
-let pdf4 = save_pdf(new_pdf()
- .title("Blockchain for Good")
- .description("How blockchain technology can solve global challenges")
- .url("https://www.weforum.org/whitepapers/blockchain-beyond-the-hype")
- .page_count(24));
-
-let pdf5 = save_pdf(new_pdf()
- .title("Future of Work Report")
- .description("Analysis of changing work patterns and remote collaboration")
- .url("https://www.mckinsey.com/featured-insights/future-of-work")
- .page_count(156));
-
-// === MARKDOWN DOCUMENTS ===
-print("Creating markdown documents...");
-
-let md1 = save_markdown(new_markdown()
- .title("OurWorld Mission Statement")
- .description("Our vision for a better world")
- .content("# OurWorld Mission\n\n## Vision\nTo create a more sustainable, equitable, and connected world through technology and collaboration.\n\n## Values\n- **Sustainability**: Every decision considers environmental impact\n- **Inclusivity**: Technology that serves everyone\n- **Transparency**: Open source and open governance\n- **Innovation**: Pushing boundaries for positive change\n\n## Goals\n1. Reduce global carbon footprint by 50% by 2030\n2. Provide internet access to 1 billion underserved people\n3. Create 10 million green jobs worldwide\n4. Establish 1000 sustainable communities"));
-
-let md2 = save_markdown(new_markdown()
- .title("Getting Started Guide")
- .description("How to join the OurWorld movement")
- .content("# Getting Started with OurWorld\n\n## Welcome!\nThank you for joining our mission to create a better world.\n\n## First Steps\n1. **Explore**: Browse our projects and initiatives\n2. **Connect**: Join our community forums\n3. **Contribute**: Find ways to get involved\n4. **Learn**: Access our educational resources\n\n## Ways to Contribute\n- **Developers**: Contribute to open source projects\n- **Activists**: Organize local initiatives\n- **Educators**: Share knowledge and skills\n- **Investors**: Support sustainable ventures\n\n## Resources\n- [Community Forum](https://forum.ourworld.tf)\n- [Developer Portal](https://dev.ourworld.tf)\n- [Learning Hub](https://learn.ourworld.tf)"));
-
-let md3 = save_markdown(new_markdown()
- .title("Technology Roadmap 2024")
- .description("Our technical development plans")
- .content("# Technology Roadmap 2024\n\n## Q1 Objectives\n- Launch decentralized identity system\n- Deploy carbon tracking blockchain\n- Release mobile app v2.0\n\n## Q2 Objectives\n- Implement AI-powered resource optimization\n- Launch peer-to-peer energy trading platform\n- Deploy IoT sensor network\n\n## Q3 Objectives\n- Release virtual collaboration spaces\n- Launch digital twin cities pilot\n- Implement quantum-safe encryption\n\n## Q4 Objectives\n- Deploy autonomous governance systems\n- Launch global impact measurement platform\n- Release AR/VR sustainability training"));
-
-let md4 = save_markdown(new_markdown()
- .title("Community Guidelines")
- .description("How we work together")
- .content("# Community Guidelines\n\n## Our Principles\n- **Respect**: Treat everyone with dignity\n- **Collaboration**: Work together towards common goals\n- **Constructive**: Focus on solutions, not problems\n- **Inclusive**: Welcome diverse perspectives\n\n## Communication Standards\n- Use clear, respectful language\n- Listen actively to others\n- Provide constructive feedback\n- Share knowledge freely\n\n## Conflict Resolution\n1. Address issues directly and respectfully\n2. Seek to understand different viewpoints\n3. Involve mediators when needed\n4. Focus on solutions that benefit everyone"));
-
-
-let investor = new_contact()
- .name("Example Investor")
- .save_contact();
-
-let investors = new_group()
- .name("Investors")
- .description("A group for example inverstors of ourworld");
-
-investors.add_contact(investor.id)
- .save_group();
-
-// === BOOKS ===
-print("Creating books...");
-
-let sustainability_book = save_book(new_book()
- .title("Sustainability Handbook")
- .description("Complete guide to sustainable living and practices")
- .add_page("# Introduction to Sustainability\n\nSustainability is about meeting our present needs without compromising the ability of future generations to meet their own needs.\n\n## Key Principles\n- Environmental stewardship\n- Social equity\n- Economic viability\n\n## Why It Matters\nOur planet faces unprecedented challenges from climate change, resource depletion, and environmental degradation.")
- .add_page("# Energy Efficiency\n\n## Home Energy Savings\n- LED lighting reduces energy consumption by 75%\n- Smart thermostats can save 10-15% on heating/cooling\n- Energy-efficient appliances make a significant difference\n\n## Renewable Energy\n- Solar panels: Clean electricity from sunlight\n- Wind power: Harnessing natural wind currents\n- Hydroelectric: Using water flow for energy\n\n## Transportation\n- Electric vehicles reduce emissions\n- Public transit decreases individual carbon footprint\n- Cycling and walking for short distances")
- .add_page("# Waste Reduction\n\n## The 5 R's\n1. **Refuse**: Say no to unnecessary items\n2. **Reduce**: Use less of what you need\n3. **Reuse**: Find new purposes for items\n4. **Recycle**: Process materials into new products\n5. **Rot**: Compost organic waste\n\n## Practical Tips\n- Use reusable bags and containers\n- Buy products with minimal packaging\n- Repair instead of replacing\n- Donate items you no longer need")
- .add_page("# Sustainable Food\n\n## Local and Seasonal\n- Support local farmers and reduce transport emissions\n- Eat seasonal produce for better nutrition and taste\n- Visit farmers markets and join CSAs\n\n## Plant-Based Options\n- Reduce meat consumption for environmental benefits\n- Explore diverse plant proteins\n- Grow your own herbs and vegetables\n\n## Food Waste Prevention\n- Plan meals and make shopping lists\n- Store food properly to extend freshness\n- Use leftovers creatively")
- .add_toc_entry(new_toc_entry().title("Introduction to Sustainability").page(0))
- .add_toc_entry(new_toc_entry().title("Energy Efficiency").page(1))
- .add_toc_entry(new_toc_entry().title("Waste Reduction").page(2))
- .add_toc_entry(new_toc_entry().title("Sustainable Food").page(3)));
-
-let tech_guide_book = save_book(new_book()
- .title("Green Technology Guide")
- .description("Understanding and implementing green technologies")
- .add_page("# Green Technology Overview\n\nGreen technology, also known as clean technology, refers to the use of science and technology to create products and services that are environmentally friendly.\n\n## Categories\n- Renewable energy systems\n- Energy efficiency technologies\n- Pollution prevention and cleanup\n- Sustainable materials and manufacturing\n\n## Benefits\n- Reduced environmental impact\n- Lower operating costs\n- Improved public health\n- Economic opportunities")
- .add_page("# Solar Technology\n\n## How Solar Works\nSolar panels convert sunlight directly into electricity using photovoltaic cells.\n\n## Types of Solar Systems\n- **Grid-tied**: Connected to the electrical grid\n- **Off-grid**: Standalone systems with battery storage\n- **Hybrid**: Combination of grid-tied and battery backup\n\n## Installation Considerations\n- Roof orientation and shading\n- Local climate and sun exposure\n- Energy consumption patterns\n- Available incentives and rebates")
- .add_page("# Smart Home Technology\n\n## Automation Benefits\n- Optimized energy usage\n- Enhanced comfort and convenience\n- Remote monitoring and control\n- Predictive maintenance\n\n## Key Technologies\n- Smart thermostats\n- Automated lighting systems\n- Energy monitoring devices\n- Smart appliances\n- Home energy management systems")
- .add_toc_entry(new_toc_entry().title("Green Technology Overview").page(0))
- .add_toc_entry(new_toc_entry().title("Solar Technology").page(1))
- .add_toc_entry(new_toc_entry().title("Smart Home Technology").page(2)));
-
-let community_book = save_book(new_book()
- .title("Building Communities")
- .description("Guide to creating sustainable and inclusive communities")
- .add_page("# Community Building Fundamentals\n\n## What Makes a Strong Community?\n- Shared values and vision\n- Open communication channels\n- Mutual support and cooperation\n- Inclusive decision-making processes\n\n## Benefits of Strong Communities\n- Enhanced quality of life\n- Economic resilience\n- Social cohesion\n- Environmental stewardship")
- .add_page("# Governance and Leadership\n\n## Collaborative Leadership\n- Distributed decision-making\n- Transparent processes\n- Accountability mechanisms\n- Conflict resolution systems\n\n## Community Engagement\n- Regular town halls and meetings\n- Digital participation platforms\n- Volunteer coordination\n- Feedback and improvement cycles")
- .add_toc_entry(new_toc_entry().title("Community Building Fundamentals").page(0))
- .add_toc_entry(new_toc_entry().title("Governance and Leadership").page(1)));
-
-// === SLIDES ===
-print("Creating slides...");
-
-let climate_slides = save_slides(new_slides()
- .title("Climate Change Awareness")
- .description("Visual presentation on climate change impacts and solutions")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1569163139394-de4e4f43e4e3?w=1200").title("Global Temperature Rise"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=1200").title("Melting Ice Caps"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1200").title("Extreme Weather Events"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=1200").title("Renewable Energy Solutions"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1497436072909-f5e4be1dffea?w=1200").title("Sustainable Transportation")));
-
-let innovation_slides = save_slides(new_slides()
- .title("Innovation Showcase")
- .description("Cutting-edge technologies for a sustainable future")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=1200").title("AI and Machine Learning"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1639322537228-f710d846310a?w=1200").title("Blockchain Technology"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=1200").title("IoT and Smart Cities"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=1200").title("Quantum Computing"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1581092162384-8987c1d64718?w=1200").title("Biotechnology Advances")));
-
-let nature_slides = save_slides(new_slides()
- .title("Biodiversity Gallery")
- .description("Celebrating Earth's incredible biodiversity")
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1564349683136-77e08dba1ef7?w=1200").title("Tropical Rainforest"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=1200").title("Coral Reef Ecosystem"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=1200").title("Arctic Wildlife"))
- .add_slide(new_slide().url("https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200").title("Mountain Ecosystems")));
-
-// === COLLECTIONS ===
-print("Creating collections...");
-
-let nature_collection = save_collection(new_collection()
- .title("Nature & Environment")
- .description("Beautiful images and resources about our natural world")
- .add_image(nature1.id)
- .add_image(nature2.id)
- .add_image(nature3.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id)
- .add_book(sustainability_book.id)
- .add_slides(nature_slides.id));
-
-let technology_collection = save_collection(new_collection()
- .title("Sustainable Technology")
- .description("Innovations driving positive change")
- .add_image(tech1.id)
- .add_image(tech2.id)
- .add_pdf(pdf3.id)
- .add_pdf(pdf4.id)
- .add_markdown(md3.id)
- .add_book(tech_guide_book.id)
- .add_slides(innovation_slides.id));
-
-let space_collection = save_collection(new_collection()
- .title("Space & Cosmos")
- .description("Exploring the universe and our place in it")
- .add_image(space1.id)
- .add_image(space2.id)
- .add_pdf(pdf2.id)
- .add_markdown(md2.id));
-
-let community_collection = save_collection(new_collection()
- .title("Community & Collaboration")
- .description("Building better communities together")
- .add_image(city1.id)
- .add_pdf(pdf5.id)
- .add_markdown(md4.id)
- .add_book(community_book.id));
-
-let climate_collection = save_collection(new_collection()
- .title("Climate Action")
- .description("Understanding and addressing climate change")
- .add_slides(climate_slides.id)
- .add_pdf(pdf1.id)
- .add_markdown(md1.id));
-
-print("✅ OurWorld library created successfully!");
-print("📚 Collections: 5");
-print("🖼️ Images: 8");
-print("📄 PDFs: 5");
-print("📝 Markdown docs: 4");
-print("📖 Books: 3");
-print("🎞️ Slide shows: 3");
\ No newline at end of file
diff --git a/examples/ourworld/scripts/timur.rhai b/examples/ourworld/scripts/timur.rhai
deleted file mode 100644
index da98615..0000000
--- a/examples/ourworld/scripts/timur.rhai
+++ /dev/null
@@ -1,15 +0,0 @@
-// OurWorld Circle and Library Data
-
-new_circle()
- .title("Timur Gordon")
- .description("Creating a better world.")
- .ws_url("ws://localhost:9100/ws")
- .add_circle("ws://localhost:9000/ws")
- .add_circle("ws://localhost:9001/ws")
- .add_circle("ws://localhost:9002/ws")
- .add_circle("ws://localhost:9003/ws")
- .add_circle("ws://localhost:9004/ws")
- .add_circle("ws://localhost:9005/ws")
- .add_circle("ws://localhost:8096/ws")
- .logo("🌍")
- .save_circle();
\ No newline at end of file
diff --git a/examples/server_e2e_rhai_flow.rs b/examples/server_e2e_rhai_flow.rs
index 6f2b5ba..76490a7 100644
--- a/examples/server_e2e_rhai_flow.rs
+++ b/examples/server_e2e_rhai_flow.rs
@@ -16,7 +16,7 @@ use circle_client_ws::CircleWsClientBuilder;
const TEST_CIRCLE_NAME: &str = "e2e_test_circle";
const TEST_SERVER_PORT: u16 = 9876; // Choose a unique port for the test
const RHAI_WORKER_BIN_NAME: &str = "worker";
-const CIRCLE_SERVER_WS_BIN_NAME: &str = "server_ws";
+const CIRCLE_SERVER_WS_BIN_NAME: &str = "server";
// RAII guard for cleaning up child processes
struct ChildProcessGuard {
diff --git a/examples/server_timeout_demonstration.rs b/examples/server_timeout_demonstration.rs
index 8b50900..5e7a991 100644
--- a/examples/server_timeout_demonstration.rs
+++ b/examples/server_timeout_demonstration.rs
@@ -14,7 +14,7 @@ use tokio::time::{sleep, Duration};
const EXAMPLE_SERVER_PORT: u16 = 8089; // Using a specific port for this example
const WS_URL: &str = "ws://127.0.0.1:8089/ws";
const CIRCLE_NAME_FOR_EXAMPLE: &str = "timeout_example_circle";
-const CIRCLE_SERVER_WS_BIN_NAME: &str = "server_ws";
+const CIRCLE_SERVER_WS_BIN_NAME: &str = "server";
const SCRIPT_TIMEOUT_SECONDS: u64 = 30; // This is the server-side timeout we expect to hit
// RAII guard for cleaning up child processes
diff --git a/examples/wss_auth_example.rs b/examples/wss_auth_example.rs
new file mode 100644
index 0000000..0535bfd
--- /dev/null
+++ b/examples/wss_auth_example.rs
@@ -0,0 +1,83 @@
+//! WSS + Authentication Example
+//!
+//! This example demonstrates a secure WebSocket server with:
+//! - TLS/WSS encryption
+//! - secp256k1 authentication
+//! - Message handling with authentication verification
+//!
+//! Usage: cargo run --manifest-path src/server/Cargo.toml --example wss_auth_example
+
+use circle_ws_lib::{ServerConfig, spawn_circle_server};
+use log::{info, warn};
+use std::time::Duration;
+use tokio::time::sleep;
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ // Initialize logging
+ env_logger::init();
+
+ info!("🔐 Starting WSS + Authentication Server Example");
+ info!("🛡️ This example demonstrates secure WebSocket with authentication");
+
+ // Create server configuration with TLS and authentication enabled
+ let mut config = ServerConfig::new(
+ "127.0.0.1".to_string(),
+ 8080, // Regular WebSocket port
+ "redis://127.0.0.1:6379".to_string(),
+ );
+
+ // Configure TLS settings
+ config.enable_tls = true;
+ config.tls_port = Some(8443);
+ config.cert_path = Some("cert.pem".to_string());
+ config.key_path = Some("key.pem".to_string());
+ config.enable_auth = true; // Enable secp256k1 authentication
+
+ info!("📋 Server Configuration:");
+ info!(" Host: {}", config.host);
+ info!(" Regular WS Port: {}", config.port);
+ info!(" WSS Port: {}", config.tls_port.unwrap_or(0));
+ info!(" TLS Enabled: {}", config.enable_tls);
+ info!(" Auth Enabled: {}", config.enable_auth);
+ info!(" Certificate: {:?}", config.cert_path);
+ info!(" Private Key: {:?}", config.key_path);
+
+ // Start the server
+ let (join_handle, _server_handle) = spawn_circle_server(config)
+ .map_err(|e| -> Box {
+ warn!("❌ Failed to start WSS + Auth server: {}", e);
+ Box::new(e)
+ })?;
+
+ info!("✅ WSS + Auth Server started successfully!");
+ info!("🔒 Secure WebSocket URL: wss://127.0.0.1:8443/ws");
+ info!("🔓 Regular WebSocket URL: ws://127.0.0.1:8080/ws");
+ info!("🛡️ Authentication: secp256k1 signatures required");
+ info!("");
+ info!("🧪 To test authenticated connections:");
+ info!(" 1. Generate a secp256k1 key pair");
+ info!(" 2. Sign your messages with the private key");
+ info!(" 3. Include the signature in your WebSocket messages");
+ info!(" 4. The server will verify signatures before processing");
+ info!("");
+ info!("📝 Message format for authenticated requests:");
+ info!(" {{");
+ info!(" \"type\": \"your_message_type\",");
+ info!(" \"data\": {{...}},");
+ info!(" \"signature\": \"hex_encoded_signature\",");
+ info!(" \"public_key\": \"hex_encoded_public_key\"");
+ info!(" }}");
+ info!("");
+
+ // Keep server running for demonstration
+ info!("⏰ Server will run for 60 seconds, then shutdown...");
+ sleep(Duration::from_secs(60)).await;
+
+ info!("🛑 Shutting down WSS + Auth server example");
+
+ // Wait for the server to finish
+ let _ = join_handle.await;
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/examples/wss_basic_example.rs b/examples/wss_basic_example.rs
new file mode 100644
index 0000000..636e068
--- /dev/null
+++ b/examples/wss_basic_example.rs
@@ -0,0 +1,69 @@
+//! Basic WSS (WebSocket Secure) server example
+//!
+//! This example demonstrates how to start a WSS server with TLS enabled.
+//! It uses the existing certificate and key files for testing.
+//!
+//! To run this example:
+//! ```bash
+//! cargo run --example wss_basic_example
+//! ```
+
+use circle_ws_lib::{spawn_circle_server, ServerConfig};
+use log::info;
+use std::time::Duration;
+use tokio::time::sleep;
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ // Initialize logging
+ env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
+
+ info!("🚀 Starting Basic WSS Server Example");
+
+ // Create server configuration with TLS enabled
+ let config = ServerConfig::new(
+ "127.0.0.1".to_string(),
+ 8443, // Use port 8443 for WSS
+ "redis://127.0.0.1/".to_string(),
+ )
+ .with_tls("cert.pem".to_string(), "key.pem".to_string())
+ .with_tls_port(8443);
+
+ info!("📋 Server Configuration:");
+ info!(" Host: {}", config.host);
+ info!(" WSS Port: {}", config.get_tls_port());
+ info!(" TLS Enabled: {}", config.enable_tls);
+ info!(" Auth Enabled: {}", config.enable_auth);
+ info!(" Certificate: {:?}", config.cert_path);
+ info!(" Private Key: {:?}", config.key_path);
+
+ // Start the WSS server
+ let (server_task, server_handle) = spawn_circle_server(config)?;
+
+ info!("✅ WSS Server started successfully!");
+ info!("🔒 You can now connect to: wss://127.0.0.1:8443/ws");
+ info!("📝 Note: This uses a self-signed certificate for testing");
+ info!("");
+ info!("🧪 To test the connection, you can use:");
+ info!(" - A WebSocket client that supports WSS");
+ info!(" - The client_ws library with TLS support");
+ info!(" - Browser developer tools (may show certificate warnings)");
+ info!("");
+ info!("⏰ Server will run for 30 seconds, then shutdown...");
+
+ // Let the server run for 30 seconds
+ sleep(Duration::from_secs(30)).await;
+
+ info!("🛑 Shutting down WSS server...");
+ server_handle.stop(true).await;
+
+ // Wait for the server task to complete
+ match server_task.await {
+ Ok(Ok(())) => info!("✅ WSS server shut down successfully"),
+ Ok(Err(e)) => info!("⚠️ WSS server shut down with error: {}", e),
+ Err(e) => info!("❌ Failed to wait for server shutdown: {}", e),
+ }
+
+ info!("🏁 Basic WSS example completed");
+ Ok(())
+}
\ No newline at end of file
diff --git a/examples/wss_demo/.gitignore b/examples/wss_demo/.gitignore
new file mode 100644
index 0000000..8085f81
--- /dev/null
+++ b/examples/wss_demo/.gitignore
@@ -0,0 +1,7 @@
+# TLS Certificates (uncomment if you want to exclude them)
+# cert.pem
+# key.pem
+
+# Temporary files
+*.tmp
+*.log
\ No newline at end of file
diff --git a/examples/wss_demo/Cargo.lock b/examples/wss_demo/Cargo.lock
new file mode 100644
index 0000000..75406b3
--- /dev/null
+++ b/examples/wss_demo/Cargo.lock
@@ -0,0 +1,2958 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "actix"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b"
+dependencies = [
+ "actix-macros",
+ "actix-rt",
+ "actix_derive",
+ "bitflags",
+ "bytes",
+ "crossbeam-channel",
+ "futures-core",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+ "log",
+ "once_cell",
+ "parking_lot",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "actix-codec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "actix-http"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44dfe5c9e0004c623edc65391dfd51daa201e7e30ebd9c9bedf873048ec32bc2"
+dependencies = [
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "actix-tls",
+ "actix-utils",
+ "base64",
+ "bitflags",
+ "brotli",
+ "bytes",
+ "bytestring",
+ "derive_more",
+ "encoding_rs",
+ "flate2",
+ "foldhash",
+ "futures-core",
+ "h2",
+ "http 0.2.12",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "language-tags",
+ "local-channel",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rand 0.9.1",
+ "sha1",
+ "smallvec",
+ "tokio",
+ "tokio-util",
+ "tracing",
+ "zstd",
+]
+
+[[package]]
+name = "actix-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "actix-router"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
+dependencies = [
+ "bytestring",
+ "cfg-if",
+ "http 0.2.12",
+ "regex",
+ "regex-lite",
+ "serde",
+ "tracing",
+]
+
+[[package]]
+name = "actix-rt"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208"
+dependencies = [
+ "futures-core",
+ "tokio",
+]
+
+[[package]]
+name = "actix-server"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502"
+dependencies = [
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "futures-core",
+ "futures-util",
+ "mio",
+ "socket2",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "actix-service"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "actix-tls"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389"
+dependencies = [
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "futures-core",
+ "impl-more",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "actix-utils"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
+dependencies = [
+ "local-waker",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "actix-web"
+version = "4.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea"
+dependencies = [
+ "actix-codec",
+ "actix-http",
+ "actix-macros",
+ "actix-router",
+ "actix-rt",
+ "actix-server",
+ "actix-service",
+ "actix-tls",
+ "actix-utils",
+ "actix-web-codegen",
+ "bytes",
+ "bytestring",
+ "cfg-if",
+ "cookie",
+ "derive_more",
+ "encoding_rs",
+ "foldhash",
+ "futures-core",
+ "futures-util",
+ "impl-more",
+ "itoa",
+ "language-tags",
+ "log",
+ "mime",
+ "once_cell",
+ "pin-project-lite",
+ "regex",
+ "regex-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "smallvec",
+ "socket2",
+ "time",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "actix-web-actors"
+version = "4.3.1+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980"
+dependencies = [
+ "actix",
+ "actix-codec",
+ "actix-http",
+ "actix-web",
+ "bytes",
+ "bytestring",
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "actix-web-codegen"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8"
+dependencies = [
+ "actix-router",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "actix_derive"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079"
+dependencies = [
+ "bindgen",
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bindgen"
+version = "0.69.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+ "which",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "bytestring"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f"
+dependencies = [
+ "bytes",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
+dependencies = [
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "circle_client_ws"
+version = "0.1.0"
+dependencies = [
+ "futures-channel",
+ "futures-util",
+ "gloo-console",
+ "gloo-net",
+ "hex",
+ "http 0.2.12",
+ "js-sys",
+ "log",
+ "native-tls",
+ "rand 0.8.5",
+ "secp256k1",
+ "serde",
+ "serde_json",
+ "sha3",
+ "thiserror",
+ "tokio",
+ "tokio-tungstenite",
+ "url",
+ "uuid",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "circle_ws_lib"
+version = "0.1.0"
+dependencies = [
+ "actix",
+ "actix-web",
+ "actix-web-actors",
+ "chrono",
+ "clap",
+ "env_logger",
+ "hex",
+ "log",
+ "once_cell",
+ "rand 0.8.5",
+ "redis",
+ "rhai_client",
+ "rustls",
+ "rustls-pemfile",
+ "secp256k1",
+ "serde",
+ "serde_json",
+ "sha3",
+ "thiserror",
+ "tokio",
+ "uuid",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
+
+[[package]]
+name = "cmake"
+version = "0.1.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "cookie"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+
+[[package]]
+name = "deranged"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "unicode-xid",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "flate2"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[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.1+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 = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "glob"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+
+[[package]]
+name = "gloo-console"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261"
+dependencies = [
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-net"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-sink",
+ "gloo-utils",
+ "http 0.2.12",
+ "js-sys",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "home"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "humantime"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "impl-more"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
+
+[[package]]
+name = "indexmap"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "jobserver"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
+dependencies = [
+ "getrandom 0.3.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "keccak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
+dependencies = [
+ "cpufeatures",
+]
+
+[[package]]
+name = "language-tags"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "libloading"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.53.2",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "local-channel"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "local-waker",
+]
+
+[[package]]
+name = "local-waker"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "log",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[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.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
+[[package]]
+name = "redis"
+version = "0.25.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "combine",
+ "futures-util",
+ "itoa",
+ "percent-encoding",
+ "pin-project-lite",
+ "ryu",
+ "sha1_smol",
+ "socket2",
+ "tokio",
+ "tokio-util",
+ "url",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-lite"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rhai_client"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "log",
+ "redis",
+ "serde",
+ "serde_json",
+ "tokio",
+ "uuid",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.9.4",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
+dependencies = [
+ "aws-lc-rs",
+ "log",
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
+dependencies = [
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "secp256k1"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
+dependencies = [
+ "rand 0.8.5",
+ "secp256k1-sys",
+]
+
+[[package]]
+name = "secp256k1-sys"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha1_smol"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
+
+[[package]]
+name = "sha3"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
+dependencies = [
+ "digest",
+ "keccak",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.3",
+ "once_cell",
+ "rustix 1.0.7",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+
+[[package]]
+name = "time-macros"
+version = "0.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tokio"
+version = "1.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
+dependencies = [
+ "futures-util",
+ "log",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tungstenite",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tungstenite"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "data-encoding",
+ "http 1.3.1",
+ "httparse",
+ "log",
+ "native-tls",
+ "rand 0.8.5",
+ "sha1",
+ "thiserror",
+ "utf-8",
+]
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
+dependencies = [
+ "getrandom 0.3.3",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[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.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[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 = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix 0.38.44",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[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 = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "wss_demo"
+version = "0.1.0"
+dependencies = [
+ "actix",
+ "actix-web",
+ "actix-web-actors",
+ "circle_client_ws",
+ "circle_ws_lib",
+ "clap",
+ "env_logger",
+ "log",
+ "serde",
+ "serde_json",
+ "tokio",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.15+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/examples/wss_demo/Cargo.toml b/examples/wss_demo/Cargo.toml
new file mode 100644
index 0000000..0a27c43
--- /dev/null
+++ b/examples/wss_demo/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "wss_demo"
+version = "0.1.0"
+edition = "2021"
+
+# Empty workspace table to exclude from parent workspace
+[workspace]
+
+[[bin]]
+name = "wss_client"
+path = "wss_client.rs"
+
+[[bin]]
+name = "wss_server"
+path = "wss_server.rs"
+
+[dependencies]
+# WebSocket client library
+circle_client_ws = { path = "../../src/client_ws", features = ["crypto"] }
+
+# Server library for the WSS server demo
+circle_ws_lib = { path = "../../src/server", features = ["auth"] }
+
+# Common dependencies
+tokio = { version = "1.45", features = ["full"] }
+env_logger = "0.10"
+log = "0.4"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+
+# Server-specific dependencies
+actix = "0.13"
+actix-web = "4.11"
+actix-web-actors = "4.3"
+clap = { version = "4.5", features = ["derive"] }
\ No newline at end of file
diff --git a/examples/wss_demo/README.md b/examples/wss_demo/README.md
new file mode 100644
index 0000000..34d9e8b
--- /dev/null
+++ b/examples/wss_demo/README.md
@@ -0,0 +1,204 @@
+# WSS (WebSocket Secure) Demo
+
+This directory contains a complete demonstration of WSS (WebSocket Secure) functionality with TLS encryption and authentication.
+
+## Contents
+
+- `cert.pem` - Self-signed TLS certificate for testing
+- `key.pem` - Private key for the TLS certificate
+- `wss_server.rs` - WSS server example with authentication
+- `wss_client.rs` - WSS client example for testing
+
+## Quick Start
+
+### 1. Start the WSS Server
+
+```bash
+# From the project root - specify the correct package and feature
+cargo run --manifest-path src/server/Cargo.toml --example wss_server --features circle_ws_lib/auth
+
+# OR run from the wss_demo directory
+cd examples/wss_demo
+cargo run --bin wss_server
+```
+
+The server will start on:
+- **WSS (Secure)**: `wss://127.0.0.1:8443/ws`
+- **WS (Regular)**: `ws://127.0.0.1:8080/ws`
+
+### 2. Test with the WSS Client
+
+Run the WSS client that uses the circle_client_ws library:
+
+```bash
+# Navigate to the wss_demo directory
+cd examples/wss_demo
+
+# Run with logging (recommended)
+RUST_LOG=info cargo run --bin wss_client
+
+# Or without logging
+cargo run --bin wss_client
+```
+
+This will run the `wss_client.rs` file which demonstrates:
+- Automatic credential generation using secp256k1
+- WSS connection with TLS encryption
+- Full authentication flow
+- Script execution over secure WebSocket
+
+**Note**: The WSS client must be run from the `examples/wss_demo` directory as it's a standalone project with its own dependencies.
+
+## Features Demonstrated
+
+### 🔒 **TLS/WSS Encryption**
+- Self-signed certificate for development/testing
+- Secure WebSocket connections over TLS
+- Certificate validation and error handling
+
+### 🛡️ **Authentication**
+- secp256k1 signature-based authentication
+- Nonce generation and verification
+- Authenticated vs unauthenticated request handling
+
+### 📝 **JSON-RPC Protocol**
+- Standard JSON-RPC 2.0 over WebSocket
+- Method calls: `fetch_nonce`, `authenticate`, `play`
+- Error handling and response validation
+
+## Certificate Information
+
+The included certificate is a **self-signed certificate** for development and testing purposes only.
+
+**Certificate Details:**
+- **Subject**: `/C=US/ST=Demo/L=Demo/O=WSS Demo/CN=localhost`
+- **Validity**: 365 days from generation
+- **Key Size**: RSA 4096-bit
+- **Usage**: Development/Testing only
+
+⚠️ **Security Notice**: Do not use this certificate in production. Generate proper certificates from a trusted Certificate Authority for production use.
+
+## Testing with Browser
+
+You can test the WSS connection using browser developer tools:
+
+```javascript
+// Open browser console and run:
+const ws = new WebSocket('wss://127.0.0.1:8443/ws');
+
+ws.onopen = () => {
+ console.log('WSS Connected!');
+ // Send a test message
+ ws.send(JSON.stringify({
+ jsonrpc: "2.0",
+ method: "fetch_nonce",
+ params: { pubkey: "test_key" },
+ id: 1
+ }));
+};
+
+ws.onmessage = (event) => {
+ console.log('Response:', JSON.parse(event.data));
+};
+```
+
+**Note**: Your browser may show a security warning due to the self-signed certificate. This is expected for development.
+
+## Testing with websocat
+
+You can also test with websocat (WebSocket command-line client):
+
+```bash
+# Install websocat if not already installed
+cargo install websocat
+
+# Connect to WSS server (ignoring certificate validation for self-signed cert)
+websocat --insecure wss://127.0.0.1:8443/ws
+
+# Or with more explicit options
+websocat -k wss://127.0.0.1:8443/ws
+
+# Send a test message (after connecting)
+{"jsonrpc":"2.0","method":"fetch_nonce","params":{"pubkey":"test_key"},"id":1}
+```
+
+**Note**: The `--insecure` or `-k` flag is needed because we're using a self-signed certificate.
+
+## Authentication Flow
+
+### 1. Fetch Nonce
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "fetch_nonce",
+ "params": { "pubkey": "your_public_key_hex" },
+ "id": 1
+}
+```
+
+### 2. Authenticate
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "authenticate",
+ "params": {
+ "pubkey": "your_public_key_hex",
+ "signature": "signed_nonce_hex"
+ },
+ "id": 2
+}
+```
+
+### 3. Execute Commands
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "play",
+ "params": { "script": "40 + 2" },
+ "id": 3
+}
+```
+
+## Troubleshooting
+
+### Certificate Issues
+If you encounter certificate-related errors:
+
+1. **Regenerate certificates**:
+ ```bash
+ cd examples/wss_demo
+ rm cert.pem key.pem
+ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/C=US/ST=Demo/L=Demo/O=WSS Demo/CN=localhost"
+ ```
+
+2. **Check file permissions**:
+ ```bash
+ ls -la cert.pem key.pem
+ ```
+
+### Connection Issues
+- Ensure no other services are using ports 8080 or 8443
+- Check firewall settings
+- Verify the server started successfully (look for "✅ WSS Server started successfully!" message)
+
+### Authentication Issues
+- Ensure you're using valid secp256k1 signatures
+- Check that the nonce hasn't expired (default: 5 minutes)
+- Verify the public key format is correct hex encoding
+
+## Production Deployment
+
+For production use:
+
+1. **Use proper certificates** from a trusted CA
+2. **Configure proper hostnames** (not localhost)
+3. **Set up proper firewall rules**
+4. **Use environment variables** for sensitive configuration
+5. **Enable proper logging and monitoring**
+6. **Consider load balancing** for high availability
+
+## Related Documentation
+
+- [WSS Implementation Plan](../../WSS_IMPLEMENTATION_PLAN.md)
+- [Server WS README](../../src/server/README.md)
+- [Client WS README](../../src/client_ws/README.md)
\ No newline at end of file
diff --git a/examples/wss_demo/wss_client.rs b/examples/wss_demo/wss_client.rs
new file mode 100644
index 0000000..15e18e2
--- /dev/null
+++ b/examples/wss_demo/wss_client.rs
@@ -0,0 +1,94 @@
+//! WSS Client Demo using Circle WebSocket Client
+//!
+//! This example demonstrates connecting to a WSS (WebSocket Secure) server
+//! using the circle_client_ws library with proper authentication.
+
+use circle_client_ws::{CircleWsClientBuilder, auth};
+use log::{info, error};
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ // Initialize logging - use RUST_LOG=info for detailed output
+ env_logger::init();
+
+ println!("🧪 Starting WSS Client Demo");
+
+ // Generate authentication credentials
+ let private_key = auth::generate_private_key()?;
+ let public_key = auth::derive_public_key(&private_key)?;
+
+ info!("🔑 Generated credentials - Public key: {}...", &public_key[..20]);
+
+ // WSS server URL (secure WebSocket)
+ let wss_url = "wss://127.0.0.1:8443/ws";
+
+ info!("🔗 Connecting to WSS server at {}", wss_url);
+ println!("🔌 Connecting to WSS server...");
+
+ // Create WSS client with authentication
+ let mut client = CircleWsClientBuilder::new(wss_url.to_string())
+ .with_keypair(private_key)
+ .build();
+
+ // Connect to the server
+ match client.connect().await {
+ Ok(()) => {
+ println!("✅ Successfully connected to WSS server!");
+ info!("📡 Response status: 101 Switching Protocols");
+
+ println!("\n🧪 Testing WSS connection with JSON-RPC messages...");
+
+ // Test 1: Authentication
+ println!("📤 Test 1: Performing authentication");
+ match client.authenticate().await {
+ Ok(true) => {
+ println!("✅ Authentication successful!");
+ info!("🔐 Client authenticated with server");
+
+ // Test 2: Execute script (authenticated request)
+ println!("\n📤 Test 2: Executing script (authenticated request)");
+ let test_script = "40 + 2".to_string();
+
+ match client.play(test_script).await {
+ Ok(result) => {
+ println!("✅ Script execution successful!");
+ println!("📊 Result: {}", result.output);
+ info!("Script output: {}", result.output);
+ }
+ Err(e) => {
+ error!("❌ Script execution failed: {}", e);
+ println!("❌ Script execution failed: {}", e);
+ }
+ }
+ }
+ Ok(false) => {
+ println!("❌ Authentication failed - server rejected credentials");
+ error!("Authentication failed");
+ }
+ Err(e) => {
+ error!("❌ Authentication error: {}", e);
+ println!("❌ Authentication error: {}", e);
+ }
+ }
+
+ // Disconnect
+ client.disconnect().await;
+ println!("\n🔌 Disconnected from WSS server");
+
+ println!("\n🎉 Summary:");
+ println!(" ✅ WSS connection established");
+ println!(" ✅ TLS encryption working");
+ println!(" ✅ JSON-RPC protocol working");
+ println!(" ✅ Authentication system working");
+ println!(" ✅ Script execution working");
+ }
+ Err(e) => {
+ error!("❌ Failed to connect to WSS server: {}", e);
+ println!("❌ Connection failed: {}", e);
+ println!("\n💡 Make sure the WSS server is running:");
+ println!(" cargo run --manifest-path src/server/Cargo.toml --example wss_server --features auth");
+ }
+ }
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/examples/wss_demo/wss_server.rs b/examples/wss_demo/wss_server.rs
new file mode 100644
index 0000000..eeba196
--- /dev/null
+++ b/examples/wss_demo/wss_server.rs
@@ -0,0 +1,91 @@
+//! WSS Server Demo
+//!
+//! This example demonstrates a complete WSS (WebSocket Secure) server with:
+//! - TLS encryption using self-signed certificates
+//! - secp256k1 authentication
+//! - JSON-RPC protocol support
+//! - Comprehensive logging and error handling
+//!
+//! Usage: cargo run --example wss_server --features auth
+
+use circle_ws_lib::{ServerConfig, spawn_circle_server};
+use log::{info, warn, error};
+use std::time::Duration;
+use tokio::time::sleep;
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ // Initialize logging
+ env_logger::init();
+
+ info!("🚀 Starting WSS Server Demo");
+ info!("🔐 This demo includes TLS encryption and secp256k1 authentication");
+ info!("");
+
+ // Create server configuration with TLS and authentication enabled
+ let config = ServerConfig::new(
+ "127.0.0.1".to_string(),
+ 8080, // Regular WebSocket port
+ "redis://127.0.0.1:6379".to_string(),
+ )
+ .with_tls(
+ "../../src/server/examples/wss_demo/cert.pem".to_string(),
+ "../../src/server/examples/wss_demo/key.pem".to_string()
+ )
+ .with_tls_port(8443) // Secure WebSocket port
+ .with_auth(); // Enable secp256k1 authentication
+
+ info!("📋 Server Configuration:");
+ info!(" Host: {}", config.host);
+ info!(" Regular WS Port: {}", config.port);
+ info!(" WSS Port: {}", config.get_tls_port());
+ info!(" TLS Enabled: {}", config.enable_tls);
+ info!(" Auth Enabled: {}", config.enable_auth);
+ info!(" Certificate: {:?}", config.cert_path);
+ info!(" Private Key: {:?}", config.key_path);
+ info!("");
+
+ // Start the server
+ let (join_handle, _server_handle) = spawn_circle_server(config)
+ .map_err(|e| -> Box {
+ error!("❌ Failed to start WSS server: {}", e);
+ Box::new(e)
+ })?;
+
+ info!("✅ WSS Server started successfully!");
+ info!("");
+ info!("🔗 Connection URLs:");
+ info!(" 🔒 Secure WebSocket: wss://127.0.0.1:8443/ws");
+ info!(" 🔓 Regular WebSocket: ws://127.0.0.1:8080/ws");
+ info!("");
+ info!("🛡️ Authentication: secp256k1 signatures required for 'play' commands");
+ info!("🔓 Public methods: 'fetch_nonce' (no auth required)");
+ info!("");
+ info!("📝 Example JSON-RPC requests:");
+ info!(" 1. Fetch nonce (no auth):");
+ info!(" {{\"jsonrpc\":\"2.0\",\"method\":\"fetch_nonce\",\"params\":{{\"pubkey\":\"your_pubkey\"}},\"id\":1}}");
+ info!("");
+ info!(" 2. Authenticate:");
+ info!(" {{\"jsonrpc\":\"2.0\",\"method\":\"authenticate\",\"params\":{{\"pubkey\":\"your_pubkey\",\"signature\":\"signed_nonce\"}},\"id\":2}}");
+ info!("");
+ info!(" 3. Execute script (requires auth):");
+ info!(" {{\"jsonrpc\":\"2.0\",\"method\":\"play\",\"params\":{{\"script\":\"40 + 2\"}},\"id\":3}}");
+ info!("");
+ info!("🧪 Test with the WSS client:");
+ info!(" cargo run --example wss_client");
+ info!("");
+ info!("🌐 Test with browser (open console):");
+ info!(" const ws = new WebSocket('wss://127.0.0.1:8443/ws');");
+ info!(" ws.onopen = () => ws.send(JSON.stringify({{jsonrpc:'2.0',method:'fetch_nonce',params:{{pubkey:'test'}},id:1}}));");
+ info!(" ws.onmessage = (e) => console.log(JSON.parse(e.data));");
+ info!("");
+ info!("⚠️ Note: Browser may show certificate warning (self-signed cert)");
+ info!("");
+ info!("🔄 Server running... Press Ctrl+C to stop");
+
+ // Keep server running until interrupted
+ let _ = join_handle.await;
+
+ info!("🛑 WSS Server stopped");
+ Ok(())
+}
\ No newline at end of file
diff --git a/examples/wss_test_client.rs b/examples/wss_test_client.rs
new file mode 100644
index 0000000..5e9baf0
--- /dev/null
+++ b/examples/wss_test_client.rs
@@ -0,0 +1,136 @@
+//! WSS Test Client
+//!
+//! This example demonstrates connecting to a WSS server for testing purposes.
+//! It's a simple client that connects to the WSS server and sends a test message.
+//!
+//! Usage: cargo run --manifest-path src/server/Cargo.toml --example wss_test_client
+
+use tokio_tungstenite::{connect_async_tls_with_config, Connector};
+use tokio_tungstenite::tungstenite::protocol::Message;
+use futures_util::{SinkExt, StreamExt};
+use log::{info, warn, error};
+use std::time::Duration;
+use tokio::time::timeout;
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ // Initialize logging
+ env_logger::init();
+
+ info!("🧪 Starting WSS Test Client");
+ info!("🔗 Attempting to connect to wss://127.0.0.1:8443/ws");
+
+ // Create a TLS connector that accepts self-signed certificates for testing
+ let connector = Connector::NativeTls(
+ native_tls::TlsConnector::builder()
+ .danger_accept_invalid_certs(true)
+ .danger_accept_invalid_hostnames(true)
+ .build()
+ .map_err(|e| format!("TLS connector error: {}", e))?
+ );
+
+ let config = Some(tokio_tungstenite::tungstenite::protocol::WebSocketConfig {
+ max_send_queue: None,
+ max_message_size: Some(64 << 20), // 64 MB
+ max_frame_size: Some(16 << 20), // 16 MB
+ accept_unmasked_frames: false,
+ });
+
+ // Connect to the WSS server
+ info!("🔌 Connecting to WSS server...");
+
+ let connect_result = timeout(
+ Duration::from_secs(10),
+ connect_async_tls_with_config(
+ "wss://127.0.0.1:8443/ws",
+ config,
+ false,
+ Some(connector)
+ )
+ ).await;
+
+ let (ws_stream, response) = match connect_result {
+ Ok(Ok((stream, response))) => {
+ info!("✅ Successfully connected to WSS server!");
+ info!("📡 Response status: {}", response.status());
+ (stream, response)
+ }
+ Ok(Err(e)) => {
+ error!("❌ Failed to connect to WSS server: {}", e);
+ return Err(format!("Connection failed: {}", e).into());
+ }
+ Err(_) => {
+ error!("❌ Connection timeout after 10 seconds");
+ return Err("Connection timeout".into());
+ }
+ };
+
+ let (mut ws_sender, mut ws_receiver) = ws_stream.split();
+
+ // Send a test JSON-RPC message
+ let test_message = serde_json::json!({
+ "jsonrpc": "2.0",
+ "method": "fetch_nonce",
+ "params": {
+ "pubkey": "test_public_key_123"
+ },
+ "id": 1
+ });
+
+ info!("📤 Sending test message: {}", test_message);
+
+ ws_sender.send(Message::Text(test_message.to_string())).await
+ .map_err(|e| format!("Send error: {}", e))?;
+
+ // Wait for response
+ info!("⏳ Waiting for server response...");
+
+ let response_result = timeout(Duration::from_secs(5), ws_receiver.next()).await;
+
+ match response_result {
+ Ok(Some(Ok(Message::Text(response)))) => {
+ info!("📥 Received response: {}", response);
+
+ // Parse the JSON response
+ match serde_json::from_str::(&response) {
+ Ok(json_response) => {
+ if json_response.get("result").is_some() {
+ info!("✅ Server responded with valid JSON-RPC result");
+ info!("🎉 WSS connection and communication test PASSED!");
+ } else if json_response.get("error").is_some() {
+ warn!("⚠️ Server responded with error (expected for unauthenticated request)");
+ info!("✅ WSS connection test PASSED (server is responding correctly)");
+ } else {
+ warn!("⚠️ Unexpected response format");
+ }
+ }
+ Err(e) => {
+ warn!("⚠️ Failed to parse JSON response: {}", e);
+ }
+ }
+ }
+ Ok(Some(Ok(message))) => {
+ info!("📥 Received non-text message: {:?}", message);
+ }
+ Ok(Some(Err(e))) => {
+ error!("❌ WebSocket error: {}", e);
+ return Err(format!("WebSocket error: {}", e).into());
+ }
+ Ok(None) => {
+ warn!("⚠️ Connection closed by server");
+ }
+ Err(_) => {
+ error!("❌ Response timeout after 5 seconds");
+ return Err("Response timeout".into());
+ }
+ }
+
+ // Close the connection
+ info!("🔌 Closing WSS connection...");
+ ws_sender.close().await
+ .map_err(|e| format!("Close error: {}", e))?;
+
+ info!("✅ WSS Test Client completed successfully!");
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/index.html b/index.html
deleted file mode 100644
index 3a08ce9..0000000
--- a/index.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
- Circles
-
-
-
-
-
-
diff --git a/src/launcher/.gitignore b/research/launcher/.gitignore
similarity index 100%
rename from src/launcher/.gitignore
rename to research/launcher/.gitignore
diff --git a/src/launcher/ARCHITECTURE.md b/research/launcher/ARCHITECTURE.md
similarity index 95%
rename from src/launcher/ARCHITECTURE.md
rename to research/launcher/ARCHITECTURE.md
index 52312f6..712c7db 100644
--- a/src/launcher/ARCHITECTURE.md
+++ b/research/launcher/ARCHITECTURE.md
@@ -6,7 +6,7 @@ This document provides a detailed look into the internal architecture of the `la
The `launcher` is a `tokio`-based asynchronous application. Its primary responsibility is to parse a configuration file and then spawn and manage the lifecycle of two key components for each configured Circle:
-1. **A `server_ws` instance**: A WebSocket server running in its own `tokio` task.
+1. **A `server` instance**: A WebSocket server running in its own `tokio` task.
2. **A Rhai worker**: A script execution engine running in a separate `tokio` task.
The launcher maintains a central registry of all running circles, allowing it to monitor their status and coordinate a graceful shutdown.
diff --git a/src/launcher/Cargo.lock b/research/launcher/Cargo.lock
similarity index 100%
rename from src/launcher/Cargo.lock
rename to research/launcher/Cargo.lock
diff --git a/src/launcher/Cargo.toml b/research/launcher/Cargo.toml
similarity index 75%
rename from src/launcher/Cargo.toml
rename to research/launcher/Cargo.toml
index 2578da0..f50892e 100644
--- a/src/launcher/Cargo.toml
+++ b/research/launcher/Cargo.toml
@@ -1,22 +1,22 @@
[package]
-name = "launcher"
+name = "circles-launcher"
version = "0.1.0"
edition = "2021"
[lib]
-name = "launcher"
+name = "circles_launcher"
path = "src/lib.rs"
[[bin]]
name = "launcher"
path = "src/cmd/main.rs"
+
[dependencies]
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
-env_logger = { workspace = true }
comfy-table = "7.0"
actix-web = { workspace = true }
secp256k1 = { version = "0.28.0", features = ["rand-std"] }
@@ -26,11 +26,11 @@ rhai = "1.18.0"
# Path dependencies to other local crates
heromodels = { path = "../../../db/heromodels" }
-engine = { path = "../../../rhailib/src/engine" }
+rhailib_engine = { path = "../../../rhailib/src/engine" }
rhailib_worker = { path = "../../../rhailib/src/worker" }
rhai_client = { path = "../../../rhailib/src/client" }
ourdb = { path = "../../../db/ourdb" } # Added for IdSequence
-circle_ws_lib = { path = "../server_ws" }
+sal-service-manager = { path = "../../../sal/service_manager" }
tokio-tungstenite = "0.23"
url = "2.5.2"
@@ -43,3 +43,12 @@ futures-util = "0.3"
redis = { version = "0.25.4", features = ["tokio-comp"] }
rand = "0.8"
url = "2.5.2"
+hex = "0.4"
+
+[[example]]
+name = "circle_launcher_example"
+path = "examples/circle_launcher_example.rs"
+
+[[example]]
+name = "cleanup_example"
+path = "examples/cleanup_example.rs"
diff --git a/research/launcher/README.md b/research/launcher/README.md
new file mode 100644
index 0000000..e9bf6b9
--- /dev/null
+++ b/research/launcher/README.md
@@ -0,0 +1,254 @@
+# Circle Launcher
+
+Crate for launching and managing [circle workers](../worker) and the [circles ws server](../server).
+
+## Features
+
+- **Single-server multi-circle architecture**: One WebSocket server handles all circles via path-based routing
+- **Dual operation modes**: Direct spawning or service manager integration
+- **Initialization scripts**: Send Rhai scripts to workers on startup
+- **Service management**: Automatic restart and background operation support
+- **Cross-platform**: macOS (launchctl) and Linux (systemd) support
+- **Service cleanup**: Automatic cleanup of background services on exit
+
+## Installation
+
+Build the launcher:
+
+```bash
+cargo build --release --bin launcher
+```
+
+## Usage
+
+### Basic Syntax
+
+```bash
+launcher [OPTIONS] -c ...
+```
+
+### Circle Configuration Format
+
+Circles are specified using the `-c/--circle` option with the format:
+
+```
+public_key[:init_script.rhai]
+```
+
+- `public_key`: secp256k1 public key in hex format (required)
+- `init_script.rhai`: Optional initialization script to send to the worker
+
+### Examples
+
+#### Development Mode (Direct Spawning)
+
+```bash
+# Single circle without initialization script
+launcher -c 02a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4
+
+# Multiple circles with initialization scripts
+launcher -c 02a1b2c3d4e5f6a7...d4:setup.rhai -c 03b2c3d4e5f6a7...e5:config.rhai
+
+# Mixed configuration (some with scripts, some without)
+launcher -c 02a1b2c3d4e5f6a7...d4:init.rhai -c 03b2c3d4e5f6a7...e5
+```
+
+#### Production Mode (Service Manager)
+
+```bash
+# Using service manager with worker binary
+launcher --use-service-manager --worker-binary ./target/release/worker \
+ -c 02a1b2c3d4e5f6a7...d4:prod_init.rhai \
+ -c 03b2c3d4e5f6a7...e5
+
+# Service manager without initialization scripts
+launcher --use-service-manager --worker-binary /usr/local/bin/worker \
+ -c 02a1b2c3d4e5f6a7...d4 \
+ -c 03b2c3d4e5f6a7...e5
+```
+
+## Command Line Options
+
+| Option | Short | Description | Default |
+|--------|-------|-------------|---------|
+| `--circle` | `-c` | Circle configuration: `public_key[:init_script.rhai]` | Required |
+| `--port` | `-p` | WebSocket server port | 8080 |
+| `--redis-url` | | Redis connection URL | `redis://127.0.0.1:6379` |
+| `--enable-auth` | | Enable WebSocket authentication | false |
+| `--use-service-manager` | | Use service manager instead of direct spawning | false |
+| `--worker-binary` | | Path to worker binary (required with service manager) | None |
+| `--debug` | `-d` | Enable debug logging | false |
+| `--verbose` | `-v` | Increase verbosity (can be used multiple times) | 0 |
+
+## Operation Modes
+
+### Direct Spawn Mode (Default)
+
+In direct spawn mode, workers run as Tokio tasks within the launcher process:
+
+- **Pros**: Simple setup, immediate shutdown, ideal for development
+- **Cons**: Workers stop when launcher exits, no automatic restart
+
+```bash
+launcher -c 02a1b2c3d4e5f6a7...d4:init.rhai
+```
+
+### Service Manager Mode
+
+In service manager mode, workers are managed by the system service manager:
+
+- **Pros**: Background operation, automatic restart, production-ready
+- **Cons**: Requires worker binary, platform-specific setup
+
+```bash
+launcher --use-service-manager --worker-binary ./target/release/worker \
+ -c 02a1b2c3d4e5f6a7...d4:init.rhai
+```
+
+#### Service Manager Support
+
+- **macOS**: Uses `launchctl` with launch agents
+- **Linux**: Uses `systemd` (implementation in progress)
+
+Services are named: `tf.ourworld.circles.circle-worker-{public_key}`
+
+## Architecture
+
+### Single-Server Multi-Circle
+
+The launcher creates one WebSocket server that handles multiple circles through path-based routing:
+
+- **Server URL**: `ws://127.0.0.1:8080`
+- **Circle URLs**: `ws://127.0.0.1:8080/{circle_public_key}`
+- **Worker Queues**: `rhai_tasks:{circle_public_key}`
+
+### Initialization Scripts
+
+When a circle configuration includes an initialization script:
+
+1. Worker starts and connects to Redis
+2. Launcher waits 2 seconds for worker startup
+3. Launcher sends script content via RhaiClient to worker's queue
+4. Worker executes the initialization script
+
+## Configuration
+
+### Environment Variables
+
+- `RUST_LOG`: Controls logging level (auto-configured based on verbosity)
+- `PRESERVE_TASKS`: Preserve Redis tasks on worker shutdown
+
+### Data Directory
+
+The launcher creates a `./launch_data` directory for:
+- Worker databases: `circle_db_{public_key}.db`
+- Service configuration files (service manager mode)
+
+## Error Handling
+
+Common error scenarios and solutions:
+
+| Error | Cause | Solution |
+|-------|-------|----------|
+| "Invalid public key" | Malformed secp256k1 key | Verify key format (64 hex chars) |
+| "Worker binary path required" | Missing `--worker-binary` in service mode | Provide path to worker executable |
+| "Failed to read init script" | Script file not found | Check script file path and permissions |
+| "Service already exists" | Service name conflict | Stop existing service or use different key |
+
+## Development
+
+### Building
+
+```bash
+# Debug build
+cargo build --bin launcher
+
+# Release build
+cargo build --release --bin launcher
+```
+
+### Testing
+
+```bash
+# Run with debug logging
+RUST_LOG=debug cargo run --bin launcher -- -c 02a1b2c3d4e5f6a7...d4
+
+# Test service manager mode
+cargo run --bin launcher -- --use-service-manager \
+ --worker-binary ./target/debug/worker \
+ -c 02a1b2c3d4e5f6a7...d4:test.rhai
+```
+
+## Troubleshooting
+
+### Worker Connection Issues
+
+1. Verify Redis is running on the specified URL
+2. Check worker binary exists and is executable
+3. Ensure public keys are valid secp256k1 format
+
+### Service Manager Issues
+
+1. Check service manager logs: `launchctl log show --predicate 'subsystem == "tf.ourworld.circles"'`
+2. Verify worker binary permissions and dependencies
+3. Ensure working directory is accessible
+
+### Script Execution Issues
+
+1. Verify script file exists and is readable
+2. Check Redis connectivity for script transmission
+3. Monitor worker logs for script execution errors
+
+## Security Considerations
+
+- **Public Key Validation**: All keys are validated as proper secp256k1 public keys
+- **Script Execution**: Initialization scripts run with worker privileges
+- **Service Isolation**: Each worker runs as a separate service in service manager mode
+- **Redis Security**: Ensure Redis instance is properly secured in production
+
+## Performance
+
+- **Concurrent Workers**: No hard limit on number of circles
+- **Resource Usage**: Each worker consumes memory for database and Rhai engine
+- **Network**: Single WebSocket server reduces port usage
+- **Startup Time**: ~2 second delay for initialization script transmission
+
+## Service Cleanup
+
+The launcher provides automatic cleanup functionality to stop and remove all circle-related services:
+
+### Automatic Cleanup
+
+Examples automatically clean up services on exit:
+
+```bash
+# Run example - services are cleaned up automatically on exit or Ctrl+C
+cargo run --example circle_launcher_example
+```
+
+### Manual Cleanup
+
+Clean up all circle services manually:
+
+```bash
+# Using the cleanup example
+cargo run --example cleanup_example
+
+# Or using the library function
+use circles_launcher::cleanup_launcher;
+cleanup_launcher().await?;
+```
+
+### What Gets Cleaned Up
+
+The cleanup function removes:
+- All worker services (`circle-worker-{public_key}`)
+- WebSocket server service (`circle-ws-server`)
+- Associated service configuration files (plist files on macOS)
+
+### Signal Handling
+
+Examples include signal handling for graceful cleanup:
+- **Ctrl+C**: Triggers cleanup before exit
+- **Normal exit**: Always runs cleanup before termination
+- **Error exit**: Cleanup still runs to prevent orphaned services
\ No newline at end of file
diff --git a/src/launcher/circles.json b/research/launcher/circles.json
similarity index 100%
rename from src/launcher/circles.json
rename to research/launcher/circles.json
diff --git a/research/launcher/examples/README.md b/research/launcher/examples/README.md
new file mode 100644
index 0000000..8f29c39
--- /dev/null
+++ b/research/launcher/examples/README.md
@@ -0,0 +1,144 @@
+# Launcher Examples
+
+This directory contains examples demonstrating how to use the circles launcher.
+
+## Prerequisites
+
+Before running the examples, make sure you have:
+
+1. Built the worker binary:
+ ```bash
+ cd ../worker && cargo build --release
+ ```
+
+2. Built the WebSocket server binary:
+ ```bash
+ cd ../server && cargo build --release
+ ```
+
+3. Redis server running on `redis://127.0.0.1:6379`
+
+## Examples
+
+### 1. Circle Launcher Example (`circle_launcher_example.rs`)
+
+Demonstrates the builder pattern API for launching circles programmatically:
+
+```bash
+cd src/launcher
+cargo run --example circle_launcher_example
+```
+
+This example shows:
+- Creating circles with generated public keys
+- Using the builder pattern API
+- Launching single and multiple circles
+- Adding initialization scripts
+- Proper cleanup between examples
+
+### 2. Cleanup Example (`cleanup_example.rs`)
+
+Shows how to clean up all launcher services:
+
+```bash
+cd src/launcher
+cargo run --example cleanup_example
+```
+
+### 3. End-to-End Confirmation (`confirm_launch.rs`)
+
+Tests the complete launcher workflow including service communication:
+
+```bash
+cd src/launcher
+cargo run --example confirm_launch
+```
+
+This example:
+- Launches the launcher binary with command line arguments
+- Tests worker communication via Redis
+- Verifies environment variables are set correctly
+- Performs end-to-end validation
+
+### 4. OurWorld Example (`ourworld/main.rs`)
+
+Real-world example using actual circle configurations:
+
+```bash
+cd src/launcher
+cargo run --example ourworld
+```
+
+## Command Line Usage
+
+You can also use the launcher binary directly:
+
+```bash
+# Single circle
+cargo run --bin launcher -- \
+ --circle 02a1b2c3d4e5f6789abcdef... \
+ --worker-binary ../target/release/worker \
+ --port 8080
+
+# Multiple circles with initialization scripts
+cargo run --bin launcher -- \
+ --circle 02a1b2c3d4e5f6789abcdef...:test_script.rhai \
+ --circle 03b2c3d4e5f6789abcdef012... \
+ --worker-binary ../target/release/worker \
+ --port 8080
+
+# With custom Redis URL
+cargo run --bin launcher -- \
+ --circle 02a1b2c3d4e5f6789abcdef... \
+ --worker-binary ../target/release/worker \
+ --redis-url redis://localhost:6379 \
+ --port 8080
+```
+
+## Circle Configuration Format
+
+Circles can be specified in two formats:
+
+1. **Public key only**: `02a1b2c3d4e5f6789abcdef...`
+2. **Public key with init script**: `02a1b2c3d4e5f6789abcdef...:init_script.rhai`
+
+The public key must be a valid secp256k1 public key in hex format.
+
+## Service Management
+
+The launcher uses the system service manager (launchctl on macOS) to manage:
+
+- **WebSocket server**: `circle-ws-server`
+- **Worker processes**: `circle-worker-`
+
+Services are automatically started and can be managed independently after launch.
+
+## Cleanup
+
+To clean up all launcher services:
+
+```bash
+cargo run --example cleanup_example
+```
+
+Or use the library function:
+
+```rust
+use circles_launcher::cleanup_launcher;
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ cleanup_launcher().await?;
+ Ok(())
+}
+```
+
+## Troubleshooting
+
+1. **Port already in use**: The launcher checks if services are already running and reuses them when possible.
+
+2. **Worker binary not found**: Make sure to build the worker binary first and specify the correct path.
+
+3. **Redis connection failed**: Ensure Redis is running and accessible at the specified URL.
+
+4. **Service manager errors**: Check system logs for service-specific errors. On macOS, use `launchctl list | grep circle` to see service status.
\ No newline at end of file
diff --git a/research/launcher/examples/circle_launcher_example.rs b/research/launcher/examples/circle_launcher_example.rs
new file mode 100644
index 0000000..2b7b041
--- /dev/null
+++ b/research/launcher/examples/circle_launcher_example.rs
@@ -0,0 +1,146 @@
+use circles_launcher::{Circle, Launcher};
+use rand::rngs::OsRng;
+use sal_service_manager::create_service_manager;
+use secp256k1::{PublicKey, Secp256k1, SecretKey};
+
+const WORKER_BINARY: &str = "../target/release/worker";
+const SERVER_BINARY: &str = "../target/release/server";
+const REDIS_URL: &str = "redis://127.0.0.1/";
+const PORT: u16 = 8080;
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ run_examples().await
+}
+
+async fn run_examples() -> Result<(), Box> {
+ // Generate valid secp256k1 keypairs for testing
+ let secp = Secp256k1::new();
+ let mut rng = OsRng;
+
+ let secret_key1 = SecretKey::new(&mut rng);
+ let public_key1 = PublicKey::from_secret_key(&secp, &secret_key1);
+ let pk1_hex = hex::encode(public_key1.serialize());
+
+ let secret_key2 = SecretKey::new(&mut rng);
+ let public_key2 = PublicKey::from_secret_key(&secp, &secret_key2);
+ let pk2_hex = hex::encode(public_key2.serialize());
+
+ let secret_key3 = SecretKey::new(&mut rng);
+ let public_key3 = PublicKey::from_secret_key(&secp, &secret_key3);
+ let pk3_hex = hex::encode(public_key3.serialize());
+
+ println!("Generated test public keys:");
+ println!(" PK1: {}", pk1_hex);
+ println!(" PK2: {}", pk2_hex);
+ println!(" PK3: {}", pk3_hex);
+
+ // Example 1: Simple launcher with single circle
+ println!("\n=== Example 1: Simple Launcher ===");
+ let launcher1 = Launcher {
+ service_manager: tokio::task::spawn_blocking(|| create_service_manager(None))
+ .await??,
+ circles: vec![Circle {
+ public_key: pk1_hex.clone(),
+ init_script: None,
+ }],
+ worker_binary: WORKER_BINARY.to_string(),
+ server_binary: SERVER_BINARY.to_string(),
+ redis_url: REDIS_URL.to_string(),
+ port: PORT,
+ };
+ match launcher1.launch().await {
+ Ok(_) => println!("Circle launched successfully!"),
+ Err(e) => println!("Failed to launch: {}", e),
+ }
+ launcher1.clean().await?;
+
+ // Example 2: Launcher with multiple circles
+ println!("\n=== Example 2: Multiple Circles ===");
+ let launcher2 = Launcher {
+ service_manager: tokio::task::spawn_blocking(|| create_service_manager(None))
+ .await??,
+ circles: vec![
+ Circle {
+ public_key: pk1_hex.clone(),
+ init_script: None,
+ },
+ Circle {
+ public_key: pk2_hex.clone(),
+ init_script: None,
+ },
+ ],
+ worker_binary: WORKER_BINARY.to_string(),
+ server_binary: SERVER_BINARY.to_string(),
+ redis_url: REDIS_URL.to_string(),
+ port: PORT,
+ };
+ match launcher2.launch().await {
+ Ok(_) => println!("Multiple circles launched successfully!"),
+ Err(e) => println!("Failed to launch multiple circles: {}", e),
+ }
+ launcher2.clean().await?;
+
+ // Example 3: Multiple circles with initialization scripts
+ println!("\n=== Example 3: Multiple Circles with Init Scripts ===");
+ let launcher3 = Launcher {
+ service_manager: tokio::task::spawn_blocking(|| create_service_manager(None))
+ .await??,
+ circles: vec![
+ Circle {
+ public_key: pk1_hex.clone(),
+ init_script: Some("test_script.rhai".to_string()),
+ },
+ Circle {
+ public_key: pk2_hex.clone(),
+ init_script: Some("test_script.rhai".to_string()),
+ },
+ Circle {
+ public_key: pk3_hex.clone(),
+ init_script: Some("test_script.rhai".to_string()),
+ },
+ ],
+ worker_binary: WORKER_BINARY.to_string(),
+ server_binary: SERVER_BINARY.to_string(),
+ redis_url: REDIS_URL.to_string(),
+ port: PORT,
+ };
+ match launcher3.launch().await {
+ Ok(_) => println!("Multiple circles with init scripts launched successfully!"),
+ Err(e) => println!("Failed to launch multiple circles with init scripts: {}", e),
+ }
+ launcher3.clean().await?;
+
+ // Example 4: Mixed configuration (some with scripts, some without)
+ println!("\n=== Example 4: Mixed Configuration ===");
+ let launcher4 = Launcher {
+ service_manager: tokio::task::spawn_blocking(|| create_service_manager(None))
+ .await??,
+ circles: vec![
+ Circle {
+ public_key: pk1_hex.clone(),
+ init_script: Some("test_script.rhai".to_string()),
+ },
+ Circle {
+ public_key: pk2_hex.clone(),
+ init_script: None,
+ },
+ Circle {
+ public_key: pk3_hex.clone(),
+ init_script: Some("test_script.rhai".to_string()),
+ },
+ ],
+ worker_binary: WORKER_BINARY.to_string(),
+ server_binary: SERVER_BINARY.to_string(),
+ redis_url: REDIS_URL.to_string(),
+ port: PORT,
+ };
+ match launcher4.launch().await {
+ Ok(_) => println!("Mixed configuration launched successfully!"),
+ Err(e) => println!("Failed to launch mixed configuration: {}", e),
+ }
+ launcher4.clean().await?;
+
+ println!("\nAll examples completed.");
+ Ok(())
+}
\ No newline at end of file
diff --git a/research/launcher/examples/cleanup_example.rs b/research/launcher/examples/cleanup_example.rs
new file mode 100644
index 0000000..0f064d7
--- /dev/null
+++ b/research/launcher/examples/cleanup_example.rs
@@ -0,0 +1,13 @@
+use circles_launcher::cleanup_launcher;
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ println!("Cleaning up all launcher services...");
+
+ match cleanup_launcher().await {
+ Ok(_) => println!("Cleanup completed successfully!"),
+ Err(e) => println!("Cleanup failed: {}", e),
+ }
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/research/launcher/examples/confirm_launch.rs b/research/launcher/examples/confirm_launch.rs
new file mode 100644
index 0000000..78540dc
--- /dev/null
+++ b/research/launcher/examples/confirm_launch.rs
@@ -0,0 +1,111 @@
+use secp256k1::{Secp256k1, SecretKey, PublicKey};
+use rand::rngs::OsRng;
+use std::process::{Child, Command, Stdio};
+use std::time::Duration;
+
+const REDIS_URL: &str = "redis://127.0.0.1:6379";
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ println!("--- Starting End-to-End Circle Launch Confirmation ---");
+
+ // Generate a test public key
+ let secp = Secp256k1::new();
+ let mut rng = OsRng;
+ let secret_key = SecretKey::new(&mut rng);
+ let public_key = PublicKey::from_secret_key(&secp, &secret_key);
+ let test_public_key = hex::encode(public_key.serialize());
+
+ println!("Using test public key: {}", test_public_key);
+
+ // Start the launcher with the test public key
+ let mut launcher_process: Child = Command::new("cargo")
+ .arg("run")
+ .arg("--bin")
+ .arg("launcher")
+ .arg("--")
+ .arg("--circle")
+ .arg(format!("{}:test_script.rhai", test_public_key))
+ .arg("--worker-binary")
+ .arg("../target/release/worker")
+ .arg("--port")
+ .arg("8080")
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()?;
+
+ println!(
+ "Launcher process started with PID: {}",
+ launcher_process.id()
+ );
+
+ // Wait a moment for the launcher to start services
+ tokio::time::sleep(Duration::from_secs(5)).await;
+
+ let client = rhai_client::RhaiClientBuilder::new()
+ .redis_url(REDIS_URL)
+ .caller_id("test_launcher")
+ .build()?;
+
+ // Test 1: Verify that CIRCLE_PUBLIC_KEY is set correctly.
+ println!("--- Test 1: Verifying CIRCLE_PUBLIC_KEY ---");
+ let script_circle_pk = r#"CIRCLE_PUBLIC_KEY"#;
+ println!("Submitting script to verify CIRCLE_PUBLIC_KEY...");
+ let task_details_circle_pk = client
+ .new_play_request()
+ .recipient_id(&format!("rhai_tasks:{}", test_public_key))
+ .script(script_circle_pk)
+ .request_id("task_id_circle_pk")
+ .timeout(Duration::from_secs(10))
+ .await_response()
+ .await?;
+ println!("Received task details: {:?}", task_details_circle_pk);
+ assert_eq!(task_details_circle_pk.status, "completed");
+ assert_eq!(task_details_circle_pk.output, Some(test_public_key.to_string()));
+ println!("✅ SUCCESS: Worker correctly reported its CIRCLE_PUBLIC_KEY.");
+
+ // Test 2: Verify that CALLER_PUBLIC_KEY is set correctly when the launcher calls.
+ println!("\n--- Test 2: Verifying CALLER_PUBLIC_KEY for init scripts ---");
+ let script_caller_pk = r#"CALLER_PUBLIC_KEY"#;
+ println!("Submitting script to verify CALLER_PUBLIC_KEY...");
+ let task_details_caller_pk = client
+ .new_play_request()
+ .recipient_id(&format!("rhai_tasks:{}", test_public_key))
+ .script(script_caller_pk)
+ .request_id("task_id_caller_pk")
+ .timeout(Duration::from_secs(10))
+ .await_response()
+ .await?;
+ println!("Received task details: {:?}", task_details_caller_pk);
+ assert_eq!(task_details_caller_pk.status, "completed");
+ // The caller should be "launcher" as set in the RhaiClient
+ println!("✅ SUCCESS: Worker correctly reported CALLER_PUBLIC_KEY for init script.");
+
+ // Test 3: Simple script execution
+ println!("\n--- Test 3: Simple Script Execution ---");
+ let simple_script = r#"print("Hello from worker!"); "test_result""#;
+ println!("Submitting simple script...");
+ let task_details_simple = client
+ .new_play_request()
+ .recipient_id(&format!("rhai_tasks:{}", test_public_key))
+ .script(simple_script)
+ .request_id("task_id_simple")
+ .timeout(Duration::from_secs(10))
+ .await_response()
+ .await?;
+ println!("Received task details: {:?}", task_details_simple);
+ assert_eq!(task_details_simple.status, "completed");
+ assert_eq!(task_details_simple.output, Some("test_result".to_string()));
+ println!("✅ SUCCESS: Worker executed simple script correctly.");
+
+ // Gracefully shut down the launcher
+ println!("Shutting down launcher process...");
+ launcher_process.kill()?;
+ tokio::task::spawn_blocking(move || {
+ let _ = launcher_process.wait();
+ })
+ .await?;
+ println!("--- End-to-End Test Finished Successfully ---");
+
+ Ok(())
+}
diff --git a/src/launcher/examples/ourworld/circles.json b/research/launcher/examples/ourworld/circles.json
similarity index 100%
rename from src/launcher/examples/ourworld/circles.json
rename to research/launcher/examples/ourworld/circles.json
diff --git a/src/launcher/examples/ourworld/main.rs b/research/launcher/examples/ourworld/main.rs
similarity index 64%
rename from src/launcher/examples/ourworld/main.rs
rename to research/launcher/examples/ourworld/main.rs
index f5e2f92..c75867c 100644
--- a/src/launcher/examples/ourworld/main.rs
+++ b/research/launcher/examples/ourworld/main.rs
@@ -16,12 +16,19 @@
//! 3. Create a `ourworld_output.json` file in the same directory with the details.
//! 4. The launcher will run until you stop it with Ctrl+C.
-use launcher::{run_launcher, Args, CircleConfig};
+use circles_launcher::{run_launcher, Args};
use log::{error, info};
+use serde::{Deserialize, Serialize};
use std::error::Error as StdError;
use std::fs;
use std::path::PathBuf;
+#[derive(Serialize, Deserialize, Debug)]
+struct OurWorldCircleConfig {
+ pub public_key: String,
+ pub init_script: Option,
+}
+
#[tokio::main]
async fn main() -> Result<(), Box> {
println!("--- Launching OurWorld Example Programmatically ---");
@@ -33,15 +40,6 @@ async fn main() -> Result<(), Box> {
println!("Using config file: {:?}", config_path);
println!("Output will be written to: {:?}", output_path);
- // Manually construct the arguments instead of parsing from command line.
- // This is useful when embedding the launcher logic in another application.
- let args = Args {
- config_path: config_path.clone(),
- output: Some(output_path),
- debug: true, // Enable debug logging for the example
- verbose: 2, // Set verbosity to max
- };
-
if !config_path.exists() {
let msg = format!("Configuration file not found at {:?}", config_path);
error!("{}", msg);
@@ -50,11 +48,11 @@ async fn main() -> Result<(), Box> {
let config_content = fs::read_to_string(&config_path)?;
- let circle_configs: Vec = match serde_json::from_str(&config_content) {
+ let ourworld_configs: Vec = match serde_json::from_str(&config_content) {
Ok(configs) => configs,
Err(e) => {
error!(
- "Failed to parse {}: {}. Ensure it's a valid JSON array of CircleConfig.",
+ "Failed to parse {}: {}. Ensure it's a valid JSON array of circle configs.",
config_path.display(),
e
);
@@ -62,7 +60,7 @@ async fn main() -> Result<(), Box> {
}
};
- if circle_configs.is_empty() {
+ if ourworld_configs.is_empty() {
info!(
"No circle configurations found in {}. Exiting.",
config_path.display()
@@ -70,11 +68,33 @@ async fn main() -> Result<(), Box> {
return Ok(());
}
- println!("Starting launcher... Press Ctrl+C to exit.");
+ // Convert OurWorld configs to circle strings for the new API
+ let mut circle_strings = Vec::new();
+ for config in &ourworld_configs {
+ let circle_str = if let Some(script) = &config.init_script {
+ format!("{}:{}", config.public_key, script)
+ } else {
+ config.public_key.clone()
+ };
+ circle_strings.push(circle_str);
+ }
+
+ // Manually construct the arguments for the new API
+ let args = Args {
+ port: 443, // Default port
+ circles: circle_strings,
+ redis_url: "redis://127.0.0.1:6379".to_string(),
+ enable_auth: false,
+ worker_binary: Some("../target/release/worker".to_string()),
+ debug: true, // Enable debug logging for the example
+ verbose: 2, // Set verbosity to max
+ };
+
+ println!("Starting launcher with {} circles... Press Ctrl+C to exit.", ourworld_configs.len());
// The run_launcher function will setup logging, spawn circles, print the table,
// and wait for a shutdown signal (Ctrl+C).
- run_launcher(args, circle_configs).await?;
+ run_launcher(args).await?;
println!("--- OurWorld Example Finished ---");
Ok(())
diff --git a/src/launcher/examples/ourworld/scripts/dunia_cybercity.rhai b/research/launcher/examples/ourworld/scripts/dunia_cybercity.rhai
similarity index 100%
rename from src/launcher/examples/ourworld/scripts/dunia_cybercity.rhai
rename to research/launcher/examples/ourworld/scripts/dunia_cybercity.rhai
diff --git a/src/launcher/examples/ourworld/scripts/freezone.rhai b/research/launcher/examples/ourworld/scripts/freezone.rhai
similarity index 100%
rename from src/launcher/examples/ourworld/scripts/freezone.rhai
rename to research/launcher/examples/ourworld/scripts/freezone.rhai
diff --git a/src/launcher/examples/ourworld/scripts/geomind.rhai b/research/launcher/examples/ourworld/scripts/geomind.rhai
similarity index 100%
rename from src/launcher/examples/ourworld/scripts/geomind.rhai
rename to research/launcher/examples/ourworld/scripts/geomind.rhai
diff --git a/src/launcher/examples/ourworld/scripts/mbweni.rhai b/research/launcher/examples/ourworld/scripts/mbweni.rhai
similarity index 100%
rename from src/launcher/examples/ourworld/scripts/mbweni.rhai
rename to research/launcher/examples/ourworld/scripts/mbweni.rhai
diff --git a/src/launcher/examples/ourworld/scripts/ourworld.rhai b/research/launcher/examples/ourworld/scripts/ourworld.rhai
similarity index 100%
rename from src/launcher/examples/ourworld/scripts/ourworld.rhai
rename to research/launcher/examples/ourworld/scripts/ourworld.rhai
diff --git a/src/launcher/examples/ourworld/scripts/sikana.rhai b/research/launcher/examples/ourworld/scripts/sikana.rhai
similarity index 100%
rename from src/launcher/examples/ourworld/scripts/sikana.rhai
rename to research/launcher/examples/ourworld/scripts/sikana.rhai
diff --git a/src/launcher/examples/ourworld/scripts/threefold.rhai b/research/launcher/examples/ourworld/scripts/threefold.rhai
similarity index 100%
rename from src/launcher/examples/ourworld/scripts/threefold.rhai
rename to research/launcher/examples/ourworld/scripts/threefold.rhai
diff --git a/research/launcher/examples/test_circles.json b/research/launcher/examples/test_circles.json
new file mode 100644
index 0000000..746f352
--- /dev/null
+++ b/research/launcher/examples/test_circles.json
@@ -0,0 +1,5 @@
+{
+ "note": "This file is no longer used by the launcher binary.",
+ "new_usage": "Use command line arguments instead:",
+ "example": "cargo run --bin launcher -- --circle 02a1b2c3d4e5f6...:test_script.rhai --worker-binary ../target/release/worker --port 8080"
+}
\ No newline at end of file
diff --git a/research/launcher/examples/test_script.rhai b/research/launcher/examples/test_script.rhai
new file mode 100644
index 0000000..a61ef58
--- /dev/null
+++ b/research/launcher/examples/test_script.rhai
@@ -0,0 +1,13 @@
+// Simple test script for circle initialization
+print("Initialization script running for circle: " + CIRCLE_PUBLIC_KEY);
+print("Called by: " + CALLER_PUBLIC_KEY);
+
+// Set some test variables
+let test_value = 42;
+let test_message = "Hello from " + CIRCLE_PUBLIC_KEY;
+
+print("Test value: " + test_value);
+print("Test message: " + test_message);
+
+// Return a success message
+"Initialization completed successfully"
\ No newline at end of file
diff --git a/research/launcher/src/builder.rs b/research/launcher/src/builder.rs
new file mode 100644
index 0000000..1097428
--- /dev/null
+++ b/research/launcher/src/builder.rs
@@ -0,0 +1,153 @@
+use sal_service_manager::create_service_manager;
+use crate::Circle;
+use crate::Launcher;
+use std::sync::{Arc, Mutex};
+
+const DEFAULT_REDIS_URL: &str = "redis://127.0.0.1:6379";
+const DEFAULT_PORT: u16 = 8443;
+
+pub struct LauncherBuilder {
+ circles: Vec, // circle pk's and their init scripts
+ worker_binary: String, // path to worker binary
+ server_binary: String, // path to server binary
+ redis_url: String, // redis url
+ port: u16, // port to bind to
+}
+
+/// Creates a new launcher builder
+pub fn new_launcher() -> LauncherBuilder {
+ LauncherBuilder::new()
+}
+
+impl LauncherBuilder {
+ /// Creates a new launcher builder
+ pub fn new() -> Self {
+ let server_binary = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ .parent() // Go up one level from lib.rs
+ .unwrap()
+ .parent() // Go up one level from src
+ .unwrap()
+ .join("target/release/circles_server")
+ .to_string_lossy()
+ .to_string();
+
+ let worker_binary = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ .parent() // Go up one level from lib.rs
+ .unwrap()
+ .parent() // Go up one level from src
+ .unwrap()
+ .join("target/release/worker")
+ .to_string_lossy()
+ .to_string();
+
+ Self {
+ circles: vec![],
+ worker_binary: worker_binary,
+ server_binary: server_binary,
+ redis_url: DEFAULT_REDIS_URL.to_string(),
+ port: DEFAULT_PORT,
+ }
+ }
+
+ /// Adds a circle by public key
+ pub fn add_circle(mut self, public_key: impl ToString) -> Self {
+ self.circles.push(CircleBuilder::new().public_key(public_key.to_string()));
+ self
+ }
+
+ /// Sets initialization script for the last added circle
+ pub fn add_init_script(mut self, script_path: impl ToString) -> Self {
+ if let Some(last_circle) = self.circles.last_mut() {
+ last_circle.init_script = Some(script_path.to_string());
+ }
+ self
+ }
+
+ /// Sets the worker binary path
+ pub fn worker_binary(mut self, path: impl ToString) -> Self {
+ // TODO: Validate path
+ self.worker_binary = path.to_string();
+ self
+ }
+
+ /// Sets the server binary path
+ pub fn server_binary(mut self, path: impl ToString) -> Self {
+ // TODO: Validate path
+ self.server_binary = path.to_string();
+ self
+ }
+
+ /// Sets the Redis URL
+ pub fn redis_url(mut self, url: impl ToString) -> Self {
+ // TODO: Validate URL
+ self.redis_url = url.to_string();
+ self
+ }
+
+ /// Sets the port
+ pub fn port(mut self, port: u16) -> Self {
+ // TODO: Validate port
+ self.port = port;
+ self
+ }
+
+ pub async fn build(self) -> Result> {
+ if self.circles.is_empty() {
+ return Err("No circles configured. Use add_circle() to add circles.".into());
+ }
+
+ let service_manager = tokio::task::spawn_blocking(|| create_service_manager(None))
+ .await??;
+
+ Ok(Launcher {
+ service_manager: Arc::new(Mutex::new(service_manager)),
+ circles: self.circles.iter().map(|circle| circle.build()).collect(),
+ worker_binary: self.worker_binary,
+ server_binary: self.server_binary,
+ redis_url: self.redis_url,
+ port: self.port,
+ })
+ }
+}
+
+/// Check if a port is in use by any process.
+/// Note: This only indicates that *something* is using the port,
+/// not necessarily our WebSocket server. Should only be used as a fallback
+/// when service manager status is unavailable.
+async fn is_port_in_use(port: u16) -> bool {
+ use std::net::{TcpListener, SocketAddr};
+
+ let addr = SocketAddr::from(([127, 0, 0, 1], port));
+ TcpListener::bind(addr).is_err()
+}
+
+pub struct CircleBuilder {
+ public_key: String,
+ init_script: Option,
+}
+
+impl CircleBuilder {
+ pub fn new() -> Self {
+ Self {
+ public_key: String::new(),
+ init_script: None,
+ }
+ }
+
+ pub fn public_key(mut self, public_key: String) -> Self {
+ self.public_key = public_key;
+ self
+ }
+
+ pub fn init_script(mut self, init_script: String) -> Self {
+ self.init_script = Some(init_script);
+ self
+ }
+
+ pub fn build(&self) -> Circle {
+ Circle {
+ public_key: self.public_key.clone(),
+ init_script: self.init_script.clone(),
+ }
+ }
+}
\ No newline at end of file
diff --git a/research/launcher/src/cmd/main.rs b/research/launcher/src/cmd/main.rs
new file mode 100644
index 0000000..0420c1a
--- /dev/null
+++ b/research/launcher/src/cmd/main.rs
@@ -0,0 +1,98 @@
+use clap::Parser;
+use circles_launcher::{Circle, Launcher};
+use sal_service_manager::create_service_manager;
+use std::error::Error as StdError;
+use std::path::PathBuf;
+use std::str::FromStr;
+use std::sync::{Arc, Mutex};
+
+const DEFAULT_REDIS_URL: &str = "redis://127.0.0.1/";
+
+// Newtype wrapper to satisfy the orphan rule
+#[derive(Clone, Debug)]
+pub struct CircleArg(Circle);
+
+impl FromStr for CircleArg {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result {
+ let parts: Vec<&str> = s.split(':').collect();
+ let circle = match parts.len() {
+ 1 => {
+ // Validate public key
+ secp256k1::PublicKey::from_str(parts[0])
+ .map_err(|e| format!("Invalid public key '{}': {}", parts[0], e))?;
+ Circle {
+ public_key: parts[0].to_string(),
+ init_script: None,
+ }
+ }
+ 2 => {
+ // Validate public key
+ secp256k1::PublicKey::from_str(parts[0])
+ .map_err(|e| format!("Invalid public key '{}': {}", parts[0], e))?;
+ Circle {
+ public_key: parts[0].to_string(),
+ init_script: Some(parts[1].to_string()),
+ }
+ }
+ _ => return Err(format!("Invalid circle format '{}'. Expected 'public_key' or 'public_key:init_script.rhai'", s)),
+ };
+ Ok(CircleArg(circle))
+ }
+}
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+pub struct Args {
+ /// Port for the WebSocket server
+ #[arg(short, long, default_value = "443")]
+ pub port: u16,
+
+ /// Circle configurations: public_key[:init_script.rhai] (can be specified multiple times)
+ #[arg(short = 'c', long = "circle", required = true)]
+ pub circles: Vec,
+
+ /// Redis URL
+ #[arg(long, default_value = DEFAULT_REDIS_URL)]
+ pub redis_url: String,
+
+ /// Worker binary path
+ #[arg(long, default_value = "./target/release/worker")]
+ pub worker_binary: PathBuf,
+
+ /// Server binary path
+ #[arg(long, default_value = "./target/release/server")]
+ pub server_binary: PathBuf,
+
+ /// Enable debug mode
+ #[arg(short, long)]
+ pub debug: bool,
+
+ /// Verbosity level
+ #[arg(short, long, action = clap::ArgAction::Count)]
+ pub verbose: u8,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ let args = Args::parse();
+
+ // To build the launcher, its fields must be public, or a public constructor
+ // must be provided from the `circles_launcher` library.
+ let service_manager = tokio::task::spawn_blocking(|| create_service_manager(None))
+ .await??;
+
+ let launcher = Launcher {
+ service_manager: Arc::new(Mutex::new(service_manager)),
+ circles: args.circles.into_iter().map(|c| c.0).collect(),
+ worker_binary: args.worker_binary.to_string_lossy().into_owned(),
+ server_binary: args.server_binary.to_string_lossy().into_owned(),
+ redis_url: args.redis_url,
+ port: args.port,
+ };
+
+ launcher.launch().await?;
+
+ Ok(())
+}
diff --git a/research/launcher/src/lib.rs b/research/launcher/src/lib.rs
new file mode 100644
index 0000000..8aef87a
--- /dev/null
+++ b/research/launcher/src/lib.rs
@@ -0,0 +1,236 @@
+use log::{info, debug};
+use rhai_client::RhaiClientBuilder;
+use sal_service_manager::{ServiceConfig as ServiceManagerConfig, ServiceStatus};
+use std::sync::{Arc, Mutex};
+
+mod builder;
+
+pub use builder::*;
+
+const SERVER_SERVICE_NAME: &str = "circle-ws-server";
+
+pub struct Launcher {
+ pub service_manager: Arc>>,
+ pub circles: Vec,
+ pub worker_binary: String,
+ pub server_binary: String,
+ pub redis_url: String,
+ pub port: u16,
+}
+
+#[derive(Debug, Clone)]
+pub struct Circle {
+ pub public_key: String,
+ pub init_script: Option,
+}
+
+impl Circle {
+ pub fn service_name(&self) -> String {
+ format!("circle-worker-{}", self.public_key)
+ }
+}
+
+impl Launcher {
+ /// Launches all configured circles
+ pub async fn launch(&self) -> Result<(), Box> {
+ self.launch_server().await?;
+ for circle in &self.circles {
+ println!("Launching circle {}", circle.public_key);
+ self.launch_circle(circle).await?;
+ }
+ Ok(())
+ }
+
+ // Launches the circles WebSocket server
+ async fn launch_server(&self) -> Result<(), Box> {
+ // Check if service exists
+ let exists = tokio::task::spawn_blocking({
+ let service_manager = Arc::clone(&self.service_manager);
+ move || service_manager.lock().unwrap().exists(SERVER_SERVICE_NAME)
+ }).await??;
+
+ if !exists {
+ self.create_circle_server_service().await?;
+ }
+
+ // Check if the WebSocket server service is already running via service manager
+ let status = tokio::task::spawn_blocking({
+ let service_manager = Arc::clone(&self.service_manager);
+ move || service_manager.lock().unwrap().status(SERVER_SERVICE_NAME)
+ }).await??;
+
+ match status {
+ ServiceStatus::Running => {
+ println!("✓ WebSocket server service '{}' is already running", SERVER_SERVICE_NAME);
+ return Ok(());
+ }
+ ServiceStatus::Failed => {
+ println!("WebSocket server service '{}' exists but failed, removing it", SERVER_SERVICE_NAME);
+ if let Err(e) = tokio::task::spawn_blocking({
+ let service_manager = Arc::clone(&self.service_manager);
+ move || service_manager.lock().unwrap().remove(SERVER_SERVICE_NAME)
+ }).await? {
+ println!("Warning: Failed to remove failed service '{}': {}", SERVER_SERVICE_NAME, e);
+ return Err(e.into());
+ }
+ }
+ ServiceStatus::Unknown => {
+ println!("WebSocket server service '{}' exists but is in an unknown state, removing it", SERVER_SERVICE_NAME);
+ if let Err(e) = tokio::task::spawn_blocking({
+ let service_manager = Arc::clone(&self.service_manager);
+ move || service_manager.lock().unwrap().remove(SERVER_SERVICE_NAME)
+ }).await? {
+ println!("Warning: Failed to remove failed service '{}': {}", SERVER_SERVICE_NAME, e);
+ return Err(e.into());
+ }
+ }
+ ServiceStatus::Stopped => {
+ println!("WebSocket server service '{}' exists but is stopped, starting it", SERVER_SERVICE_NAME);
+ match tokio::task::spawn_blocking({
+ let service_manager = Arc::clone(&self.service_manager);
+ move || service_manager.lock().unwrap().start(SERVER_SERVICE_NAME)
+ }).await? {
+ Ok(_) => {
+ println!("✓ WebSocket server service '{}' started", SERVER_SERVICE_NAME);
+ return Ok(());
+ }
+ Err(e) => {
+ println!("Failed to start existing service, removing and recreating: {}", e);
+ if let Err(e) = tokio::task::spawn_blocking({
+ let service_manager = Arc::clone(&self.service_manager);
+ move || service_manager.lock().unwrap().remove(SERVER_SERVICE_NAME)
+ }).await? {
+ println!("Warning: Failed to remove problematic service '{}': {}", SERVER_SERVICE_NAME, e);
+ return Err(e.into());
+ }
+ }
+ }
+ }
+ }
+
+ // This part is reached if the service was Failed, Unknown or Stopped and then removed/failed to start.
+ // We need to create and start it.
+ println!("Creating and starting new WebSocket server service '{}'", SERVER_SERVICE_NAME);
+ self.create_circle_server_service().await?;
+ tokio::task::spawn_blocking({
+ let service_manager = Arc::clone(&self.service_manager);
+ move || service_manager.lock().unwrap().start(SERVER_SERVICE_NAME)
+ }).await??;
+ println!("✓ WebSocket server service '{}' started successfully", SERVER_SERVICE_NAME);
+
+ Ok(())
+ }
+
+ // Creates circles server service
+ async fn create_circle_server_service(&self) -> Result<(), Box> {
+ let config = ServiceManagerConfig {
+ name: SERVER_SERVICE_NAME.to_string(),
+ binary_path: self.server_binary.clone(),
+ args: vec![
+ "--port".to_string(),
+ self.port.to_string(),
+ "--redis-url".to_string(),
+ self.redis_url.clone(),
+ ],
+ working_directory: Some(std::env::current_dir()?.to_string_lossy().to_string()),
+ environment: std::env::vars().collect(),
+ auto_restart: true,
+ };
+
+ // Use spawn_blocking to avoid runtime conflicts
+ tokio::task::spawn_blocking({
+ let service_manager = Arc::clone(&self.service_manager);
+ move || service_manager.lock().unwrap().create(&config)
+ }).await??;
+
+ Ok(())
+ }
+
+ pub async fn launch_circle(&self, circle: &Circle) -> Result<(), Box> {
+ info!("Launching circle {}", circle.public_key);
+ let config = ServiceManagerConfig {
+ name: circle.service_name(),
+ binary_path: self.worker_binary.clone(),
+ args: vec![circle.public_key.clone()],
+ auto_restart: true,
+ environment: std::env::vars().collect(),
+ working_directory: Some(std::env::current_dir()?.to_string_lossy().to_string()),
+ };
+
+ // Use spawn_blocking for service manager operations
+ let service_name = circle.service_name();
+ tokio::task::spawn_blocking({
+ let service_manager = Arc::clone(&self.service_manager);
+ let config = config.clone();
+ move || service_manager.lock().unwrap().create(&config)
+ }).await?
+ .map_err(|e| format!("Failed to create service manager: {}", e))?;
+
+ tokio::task::spawn_blocking({
+ let service_manager = Arc::clone(&self.service_manager);
+ let service_name = service_name.clone();
+ move || service_manager.lock().unwrap().start(&service_name)
+ }).await?
+ .map_err(|e| format!("Failed to start service manager: {}", e))?;
+
+ if let Some(init_script) = &circle.init_script {
+ send_init_script_to_worker(&circle.public_key, &init_script, &self.redis_url)
+ .await?;
+ }
+
+ Ok(())
+ }
+
+ /// Cleanup all services created by the launcher
+ pub async fn clean(&self) -> Result<(), Box> {
+ tokio::task::spawn_blocking({
+ let service_manager = Arc::clone(&self.service_manager);
+ move || service_manager.lock().unwrap().remove(SERVER_SERVICE_NAME)
+ }).await??;
+
+ for circle in &self.circles {
+ self.clean_circle(&circle.public_key).await?;
+ }
+
+ println!("Cleanup completed.");
+ Ok(())
+ }
+
+ /// Cleanup all services created by the launcher
+ pub async fn clean_circle(&self, circle_public_key: &str) -> Result<(), Box> {
+ let circle_key = circle_public_key.to_string();
+ match tokio::task::spawn_blocking({
+ let service_manager = Arc::clone(&self.service_manager);
+ move || service_manager.lock().unwrap().remove(&circle_key)
+ }).await? {
+ Ok(_) => Ok(()),
+ Err(e) => Err(e.into()),
+ }
+ }
+
+}
+
+async fn send_init_script_to_worker(
+ public_key: &str,
+ init_script: &str,
+ redis_url: &str,
+) -> Result<(), Box> {
+ println!("Sending initialization script '{}' to worker for circle: {}", init_script, public_key);
+
+ // Create RhaiClient and send script
+ let client = RhaiClientBuilder::new()
+ .redis_url(redis_url)
+ .caller_id("launcher")
+ .build()?;
+
+ client.new_play_request()
+ .recipient_id(&format!("rhai_tasks:{}", public_key))
+ .script(init_script)
+ .submit()
+ .await?;
+
+ println!("Successfully sent initialization script to worker for circle: {}", public_key);
+
+ Ok(())
+}
+
diff --git a/research/launcher/tests/spawn_test.rs b/research/launcher/tests/spawn_test.rs
new file mode 100644
index 0000000..29a622a
--- /dev/null
+++ b/research/launcher/tests/spawn_test.rs
@@ -0,0 +1,173 @@
+use futures_util::{SinkExt, StreamExt};
+use circles_launcher::{new_launcher, setup_multi_circle_server, shutdown_circles, Args, CircleConfig};
+use secp256k1::Secp256k1;
+use tokio_tungstenite::connect_async;
+use url::Url;
+use std::str::FromStr;
+
+#[tokio::test]
+async fn test_launcher_builder_pattern() {
+ // Test the new builder pattern API
+ let secp = Secp256k1::new();
+ let (secret_key, public_key) = secp.generate_keypair(&mut secp256k1::rand::thread_rng());
+ let public_key_str = public_key.to_string();
+
+ // Use the builder pattern to create a launcher
+ let launcher = new_launcher()
+ .add_circle(&public_key_str)
+ .port(8088)
+ .redis_url("redis://127.0.0.1:6379")
+ .worker_binary("../target/debug/worker") // Use debug for tests
+ .enable_auth(false);
+
+ // Note: We can't easily test the full launch in unit tests since it requires
+ // actual binaries and Redis. This test verifies the builder pattern works.
+
+ // Verify the builder created the launcher correctly
+ // (This is more of a compilation test than a runtime test)
+ assert!(true, "Builder pattern works correctly");
+}
+
+#[tokio::test]
+async fn test_circle_config_parsing() {
+ // Test parsing circle configurations from strings
+ let public_key_only = "02a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890";
+ let config = CircleConfig::from_str(public_key_only).expect("Failed to parse public key only");
+ assert_eq!(config.public_key, public_key_only);
+ assert!(config.init_script.is_none());
+
+ // Test with init script
+ let with_script = "02a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890:init.rhai";
+ let config = CircleConfig::from_str(with_script).expect("Failed to parse with script");
+ assert_eq!(config.public_key, public_key_only);
+ assert_eq!(config.init_script, Some("init.rhai".to_string()));
+
+ // Test invalid format
+ let invalid = "invalid:too:many:colons";
+ let result = CircleConfig::from_str(invalid);
+ assert!(result.is_err(), "Should fail with invalid format");
+}
+
+#[tokio::test]
+async fn test_args_structure() {
+ // Test that Args structure works correctly with the new API
+ let secp = Secp256k1::new();
+ let (_, public_key) = secp.generate_keypair(&mut secp256k1::rand::thread_rng());
+ let public_key_str = public_key.to_string();
+
+ let args = Args {
+ port: 8089,
+ circles: vec![public_key_str.clone()],
+ redis_url: "redis://127.0.0.1:6379".to_string(),
+ enable_auth: false,
+ worker_binary: Some("../target/debug/worker".to_string()),
+ debug: true,
+ verbose: 1,
+ };
+
+ // Verify args structure
+ assert_eq!(args.port, 8089);
+ assert_eq!(args.circles.len(), 1);
+ assert_eq!(args.circles[0], public_key_str);
+ assert!(!args.enable_auth);
+ assert!(args.worker_binary.is_some());
+}
+
+#[tokio::test]
+async fn test_setup_multi_circle_server_validation() {
+ // Test validation in setup_multi_circle_server
+ let args = Args {
+ port: 8090,
+ circles: vec![], // Empty circles should cause error
+ redis_url: "redis://127.0.0.1:6379".to_string(),
+ enable_auth: false,
+ worker_binary: None, // Missing worker binary should cause error
+ debug: true,
+ verbose: 0,
+ };
+
+ // This should fail due to missing worker binary
+ let result = setup_multi_circle_server(&args).await;
+ assert!(result.is_err(), "Should fail with missing worker binary");
+
+ if let Err(e) = result {
+ let error_msg = e.to_string();
+ assert!(
+ error_msg.contains("Worker binary path is required"),
+ "Error should mention missing worker binary, got: {}",
+ error_msg
+ );
+ }
+}
+
+#[tokio::test]
+async fn test_circle_config_validation() {
+ // Test that invalid public keys are rejected
+ let invalid_key = "not_a_valid_public_key";
+ let result = CircleConfig::from_str(invalid_key);
+ assert!(result.is_err(), "Should reject invalid public key");
+
+ // Test valid public key format
+ let valid_key = "02a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890";
+ let result = CircleConfig::from_str(valid_key);
+ assert!(result.is_ok(), "Should accept valid public key");
+}
+
+#[tokio::test]
+async fn test_launcher_cleanup_functionality() {
+ // Test that cleanup functionality exists and can be called
+ // Note: This doesn't test actual cleanup since we don't have running services
+ use circles_launcher::cleanup_launcher;
+
+ // This should not panic and should handle the case where no services exist
+ let result = cleanup_launcher().await;
+ // It's OK if this fails due to no services - we're just testing the API exists
+ let _ = result;
+
+ assert!(true, "Cleanup function exists and can be called");
+}
+
+// Integration test that would require actual binaries and Redis
+// Commented out since it requires external dependencies
+/*
+#[tokio::test]
+#[ignore] // Use `cargo test -- --ignored` to run this test
+async fn test_full_launcher_integration() {
+ // This test requires:
+ // 1. Redis server running on localhost:6379
+ // 2. Worker binary built at ../target/debug/worker
+ // 3. WebSocket server binary built at ../target/debug/circles_server
+
+ let secp = Secp256k1::new();
+ let (_, public_key) = secp.generate_keypair(&mut secp256k1::rand::thread_rng());
+ let public_key_str = public_key.to_string();
+
+ let args = Args {
+ port: 8091,
+ circles: vec![public_key_str.clone()],
+ redis_url: "redis://127.0.0.1:6379".to_string(),
+ enable_auth: false,
+ worker_binary: Some("../target/debug/worker".to_string()),
+ debug: true,
+ verbose: 1,
+ };
+
+ // Setup the multi-circle server
+ let result = setup_multi_circle_server(&args).await;
+ assert!(result.is_ok(), "Failed to setup multi-circle server: {:?}", result.err());
+
+ let (running_circles, outputs) = result.unwrap();
+
+ // Verify outputs
+ assert_eq!(outputs.len(), 1);
+ assert_eq!(outputs[0].public_key, public_key_str);
+
+ // Test WebSocket connection
+ let ws_url = &outputs[0].ws_url;
+ let connection_result = connect_async(ws_url).await;
+ assert!(connection_result.is_ok(), "Failed to connect to WebSocket");
+
+ // Cleanup
+ shutdown_circles(running_circles).await;
+}
+*/
diff --git a/src/app/Cargo.toml b/src/app/Cargo.toml
index 22b8c0c..4aa1d12 100644
--- a/src/app/Cargo.toml
+++ b/src/app/Cargo.toml
@@ -28,15 +28,14 @@ gloo-utils = "0.2"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } # For StreamExt
futures-channel = "0.3" # For MPSC channels
rand = "0.8" # For random traffic simulation
-common_models = { path = "/Users/timurgordon/code/playground/yew/common_models" }
-engine = { path = "/Users/timurgordon/code/git.ourworld.tf/herocode/rhailib/src/engine" }
+rhailib_engine = { path = "../../../rhailib/src/engine" }
rhai = "1.17"
js-sys = "0.3"
getrandom = { version = "0.3", features = ["wasm_js"] }
urlencoding = "2.1"
# Authentication dependencies
-secp256k1 = { workspace = true, features = ["rand", "recovery", "hashes"] }
+secp256k1 = { version = "0.29", default-features = false, features = ["rand", "recovery", "hashes"] }
hex = "0.4"
sha3 = "0.10"
gloo-storage = "0.3"
diff --git a/src/app/src/views/intelligence_view.rs b/src/app/_archive/intelligence_view.rs
similarity index 99%
rename from src/app/src/views/intelligence_view.rs
rename to src/app/_archive/intelligence_view.rs
index 9bb8be2..1525827 100644
--- a/src/app/src/views/intelligence_view.rs
+++ b/src/app/_archive/intelligence_view.rs
@@ -6,7 +6,7 @@ use yew::prelude::*;
// Imports from common_models
use crate::ws_manager::CircleWsManager;
-use common_models::{AiConversation, AiMessageRole};
+use crate::common_models::{AiConversation, AiMessageRole};
use heromodels::models::circle::Circle;
#[derive(Properties, PartialEq, Clone)]
diff --git a/src/app/src/views/publishing_view.rs b/src/app/_archive/publishing_view.rs
similarity index 100%
rename from src/app/src/views/publishing_view.rs
rename to src/app/_archive/publishing_view.rs
diff --git a/content/intro.md b/src/app/content/intro.md
similarity index 100%
rename from content/intro.md
rename to src/app/content/intro.md
diff --git a/src/app/src/app.rs b/src/app/src/app.rs
index 4710032..58a4ea3 100644
--- a/src/app/src/app.rs
+++ b/src/app/src/app.rs
@@ -12,9 +12,7 @@ use crate::views::auth_view::AuthView;
use crate::views::circles_view::CirclesView;
use crate::views::customize_view::CustomizeView;
use crate::views::inspector_view::InspectorView;
-use crate::views::intelligence_view::IntelligenceView;
use crate::views::library_view::LibraryView;
-use crate::views::publishing_view::PublishingView;
use crate::ws_manager::fetch_data_from_ws_urls;
use heromodels::models::circle::{Circle, ThemeData};
@@ -29,8 +27,6 @@ pub enum AppView {
Login,
Circles,
Library,
- Intelligence,
- Publishing,
Customize,
Inspector, // Added Inspector
}
@@ -41,8 +37,6 @@ impl AppView {
AppView::Login => "/login".to_string(),
AppView::Circles => "/".to_string(),
AppView::Library => "/library".to_string(),
- AppView::Intelligence => "/intelligence".to_string(),
- AppView::Publishing => "/publishing".to_string(),
AppView::Customize => "/customize".to_string(),
AppView::Inspector => "/inspector".to_string(),
}
@@ -52,8 +46,6 @@ impl AppView {
let (base_view, _sub_route) = AppRouteParser::parse_app_route(path);
match base_view.as_str() {
"library" => AppView::Library,
- "intelligence" => AppView::Intelligence,
- "publishing" => AppView::Publishing,
"customize" => AppView::Customize,
"inspector" => AppView::Inspector,
"login" => AppView::Login,
@@ -67,8 +59,6 @@ impl AppView {
AppView::Login => "login",
AppView::Circles => "",
AppView::Library => "library",
- AppView::Intelligence => "intelligence",
- AppView::Publishing => "publishing",
AppView::Customize => "customize",
AppView::Inspector => "inspector",
};
@@ -389,18 +379,6 @@ impl Component for App {
/>
}
},
- AppView::Intelligence => html! {
-
- },
- AppView::Publishing => html! {
-
- },
AppView::Inspector => {
html! {
Html {
// Create circle data for the map animation
let circles_data = use_memo(props.circle_ws_addresses.clone(), |addresses| {
let mut circles = HashMap::new();
-
- for (index, ws_url) in addresses.iter().enumerate() {
- circles.insert(
- index as u32 + 1,
- CircleData {
- id: index as u32 + 1,
- name: format!("Circle {}", index + 1),
- description: format!("Circle at {}", ws_url),
- ws_url: ws_url.clone(),
- ws_urls: vec![],
- theme: HashMap::new(),
- tasks: None,
- epics: None,
- sprints: None,
- proposals: None,
- members: None,
- library: None,
- intelligence: None,
- timeline: None,
- calendar_events: None,
- treasury: None,
- publications: None,
- deployments: None,
- },
- );
- }
-
Rc::new(circles)
});
diff --git a/src/app/src/components/nav_island.rs b/src/app/src/components/nav_island.rs
index 90ace90..2032fa8 100644
--- a/src/app/src/components/nav_island.rs
+++ b/src/app/src/components/nav_island.rs
@@ -22,16 +22,16 @@ pub fn nav_island(props: &NavIslandProps) -> yew::Html {
),
(AppView::Library, None::<()>, "fas fa-book", "Library"),
(
- AppView::Intelligence,
+ AppView::Customize,
None::<()>,
"fas fa-brain",
- "Intelligence",
+ "Customize",
),
(
- AppView::Publishing,
+ AppView::Inspector,
None::<()>,
"fas fa-rocket",
- "Publishing",
+ "Inspector",
),
(
AppView::Inspector,
diff --git a/src/app/src/components/network_animation_view.rs b/src/app/src/components/network_animation_view.rs
index 2ee668a..7ce5c03 100644
--- a/src/app/src/components/network_animation_view.rs
+++ b/src/app/src/components/network_animation_view.rs
@@ -1,4 +1,4 @@
-use common_models::CircleData;
+use heromodels::models::circle::Circle;
use gloo_timers::callback::{Interval, Timeout};
use rand::seq::SliceRandom;
use rand::Rng;
@@ -33,7 +33,7 @@ enum TransmissionType {
#[derive(Properties, Clone, PartialEq)]
pub struct NetworkAnimationViewProps {
- pub all_circles: Rc>,
+ pub all_circles: Rc>,
}
pub enum Msg {
@@ -53,7 +53,7 @@ pub struct NetworkAnimationView {
impl NetworkAnimationView {
fn calculate_server_positions(
- all_circles: &Rc>,
+ all_circles: &Rc>,
) -> Rc> {
let mut nodes = HashMap::new();
@@ -76,7 +76,7 @@ impl NetworkAnimationView {
ServerNode {
x: *x,
y: *y,
- name: format!("{}", circle_data.name),
+ name: format!("{}", circle_data.title),
id: *id,
is_active: true,
},
diff --git a/src/app/src/rhai_executor.rs b/src/app/src/rhai_executor.rs
index f7c4120..3424d27 100644
--- a/src/app/src/rhai_executor.rs
+++ b/src/app/src/rhai_executor.rs
@@ -1,5 +1,5 @@
use circle_client_ws::CircleWsClientBuilder;
-use engine::{create_heromodels_engine, eval_script};
+use rhailib_engine::{create_heromodels_engine, eval_script};
use heromodels::db::hero::OurDB;
use rhai::Engine;
use std::sync::Arc;
@@ -18,7 +18,7 @@ impl RhaiExecutor {
let db = OurDB::new("app.db", true).expect("Failed to create database");
// Create the heromodels engine with all the registered functions
- let engine = create_heromodels_engine(Arc::new(db));
+ let engine = create_heromodels_engine();
Self { engine }
}
diff --git a/src/app/src/views/mod.rs b/src/app/src/views/mod.rs
index d69f2fb..7b758d1 100644
--- a/src/app/src/views/mod.rs
+++ b/src/app/src/views/mod.rs
@@ -2,6 +2,4 @@ pub mod auth_view;
pub mod circles_view;
pub mod customize_view;
pub mod inspector_view;
-pub mod intelligence_view;
pub mod library_view;
-pub mod publishing_view;
diff --git a/src/client_ws/Cargo.toml b/src/client_ws/Cargo.toml
index 3281527..46d0c66 100644
--- a/src/client_ws/Cargo.toml
+++ b/src/client_ws/Cargo.toml
@@ -3,6 +3,10 @@ name = "circle_client_ws"
version = "0.1.0"
edition = "2021"
+[[bin]]
+name = "circles_client"
+path = "cmd/main.rs"
+
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
@@ -23,7 +27,7 @@ secp256k1 = { workspace = true, optional = true }
sha3 = { workspace = true, optional = true }
# Optional server dependency for end-to-end examples
-circle_ws_lib = { path = "../server_ws", optional = true }
+circle_ws_lib = { path = "../server", optional = true }
# WASM-specific dependencies
[target.'cfg(target_arch = "wasm32")'.dependencies]
@@ -36,10 +40,12 @@ web-sys = { version = "0.3", features = ["Request", "RequestInit", "RequestMode"
# Native-specific dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-tokio-tungstenite = { version = "0.19.0", features = ["native-tls"] }
-native-tls = "0.2.11"
-tokio-native-tls = "0.3.0"
+tokio-tungstenite = { version = "0.23.1", features = ["native-tls"] }
tokio = { workspace = true, features = ["rt", "macros", "time"] }
+native-tls = "0.2"
+clap = { workspace = true }
+env_logger = { workspace = true }
+dotenv = "0.15"
[dev-dependencies]
tokio = { workspace = true }
diff --git a/src/client_ws/README.md b/src/client_ws/README.md
index 01b93af..d97cf54 100644
--- a/src/client_ws/README.md
+++ b/src/client_ws/README.md
@@ -1,89 +1,67 @@
-# `client_ws`: The Circles WebSocket Client
+# Circle WebSocket Client
-The `client_ws` crate provides a high-level, cross-platform WebSocket client for interacting with the `server_ws`. It is designed to work seamlessly in both native Rust applications and WebAssembly (WASM) environments, making it suitable for a wide range of use cases, from command-line tools to web-based frontends.
+A Rust library for connecting to Circle WebSocket servers with authentication support.
## Features
-- **Cross-Platform**: Works in both native and WASM environments using `tokio-tungstenite` and `gloo-net`, respectively.
-- **JSON-RPC 2.0**: Handles all the complexities of JSON-RPC 2.0 communication, including request serialization, response deserialization, and error handling.
-- **Asynchronous**: Built with `async/await` for non-blocking I/O.
-- **Script Execution**: Provides a simple `play` method to send Rhai scripts to the server for execution.
-- **Authentication**: Implements a `secp256k1` signature-based authentication flow, allowing the client to securely identify itself to the server.
-- **API**: The client's full API is formally defined in the root [openrpc.json](../../openrpc.json) file.
+- Cross-platform WebSocket client (native and WASM)
+- secp256k1 cryptographic authentication
+- JSON-RPC 2.0 protocol support
+- Async/await interface with Tokio
+- Built on tokio-tungstenite for reliable WebSocket connections
-## Core Components
+## Usage
-### `CircleWsClientBuilder`
+Add this to your `Cargo.toml`:
-A builder pattern is used to construct the client, providing a clear and flexible way to configure it.
+```toml
+[dependencies]
+circle_client_ws = { path = "../client_ws" }
+```
-- `new(ws_url: String)`: Creates a new builder.
-- `with_keypair(private_key: String)`: Optionally provides a private key for authentication.
-- `build()`: Constructs the `CircleWsClient`.
-
-### `CircleWsClient`
-
-The main client struct.
-
-- `connect()`: Establishes the WebSocket connection.
-- `authenticate()`: Performs the full, unified authentication flow over the WebSocket connection. Returns an error if no keypair was provided.
-- `play(script: String)`: Sends a Rhai script to the server for execution.
-- `disconnect()`: Closes the WebSocket connection.
-
-## Usage Example
-
-The following example demonstrates how to build a client with a keypair, connect, authenticate, and execute a script.
+### Basic Example
```rust
-use client_ws::CircleWsClientBuilder;
-use client_ws::auth; // For key generation
+use circle_client_ws::CircleWsClientBuilder;
-async fn run_client() {
- // In a real application, the private key would be loaded securely.
- let private_key = auth::generate_private_key().unwrap();
-
- let mut client = CircleWsClientBuilder::new("ws://127.0.0.1:9001/ws".to_string())
- .with_keypair(private_key)
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ // Create client with private key
+ let private_key = "your_private_key_hex";
+ let client = CircleWsClientBuilder::new()
+ .with_private_key(private_key)?
.build();
-
- if let Err(e) = client.connect().await {
- eprintln!("Failed to connect: {}", e);
- return;
- }
-
- // Authenticate with the server
- match client.authenticate().await {
- Ok(true) => println!("Successfully authenticated!"),
- Ok(false) => println!("Authentication failed."),
- Err(e) => eprintln!("Error during authentication: {}", e),
- }
-
- // Execute a script
- let script = "40 + 2".to_string();
- match client.play(script).await {
- Ok(result) => {
- println!("Script output: {}", result.output);
- }
- Err(e) => {
- eprintln!("Error during play: {}", e);
- }
- }
-
- client.disconnect().await;
+
+ // Connect and authenticate
+ client.connect("ws://localhost:8080").await?;
+
+ // Use the authenticated client...
+
+ Ok(())
}
```
-## Authentication
+### Authentication Flow
-The client includes a robust authentication mechanism to securely interact with protected server endpoints. For a detailed explanation of the authentication architecture and the cryptographic principles involved, see the [ARCHITECTURE.md](ARCHITECTURE.md) file.
+The client automatically handles the secp256k1 authentication flow:
+1. Connects to WebSocket server
+2. Receives authentication challenge
+3. Signs challenge with private key
+4. Sends signed response
+5. Receives authentication confirmation
-## Building
+## Binary Tool
-### Native
-```bash
-cargo build
-```
+A command-line binary is also available for interactive use and script execution. See [`cmd/README.md`](cmd/README.md) for details.
-### WASM
-```bash
-cargo build --target wasm32-unknown-unknown
+## Platform Support
+
+- **Native**: Full support on all Rust-supported platforms
+- **WASM**: Browser support with web-sys bindings
+
+## Dependencies
+
+- `tokio-tungstenite`: WebSocket implementation
+- `secp256k1`: Cryptographic operations
+- `serde`: JSON serialization
+- `uuid`: Request ID generation
diff --git a/src/client_ws/cmd/README.md b/src/client_ws/cmd/README.md
new file mode 100644
index 0000000..c91d92b
--- /dev/null
+++ b/src/client_ws/cmd/README.md
@@ -0,0 +1,89 @@
+# Circles WebSocket Client Binary
+
+A command-line WebSocket client for connecting to Circles servers with authentication support.
+
+## Binary: `circles_client`
+
+### Installation
+
+Build the binary:
+```bash
+cargo build --bin circles_client --release
+```
+
+### Configuration
+
+Create a `.env` file in the `cmd/` directory:
+```bash
+# cmd/.env
+PRIVATE_KEY=your_actual_private_key_hex_here
+```
+
+Or set the environment variable directly:
+```bash
+export PRIVATE_KEY=your_actual_private_key_hex_here
+```
+
+### Usage
+
+```bash
+# Basic usage - connects and enters interactive mode
+circles_client ws://localhost:8080
+
+# Execute a single Rhai script
+circles_client -s "print('Hello from Rhai!')" ws://localhost:8080
+
+# Execute a script from file
+circles_client -f script.rhai ws://localhost:8080
+
+# Increase verbosity (can be used multiple times)
+circles_client -v ws://localhost:8080
+circles_client -vv ws://localhost:8080
+```
+
+### Features
+
+- **Authentication**: Automatically loads private key and completes secp256k1 authentication flow
+- **Script Execution**: Supports both inline scripts (`-s`) and script files (`-f`)
+- **Interactive Mode**: When no script is provided, enters interactive REPL mode
+- **Verbosity Control**: Use `-v` flags to increase logging detail
+- **Cross-platform**: Works on all platforms supported by Rust and tokio-tungstenite
+
+### Interactive Mode
+
+When run without `-s` or `-f` flags, the client enters interactive mode where you can:
+- Enter Rhai scripts line by line
+- Type `exit` or `quit` to close the connection
+- Use Ctrl+C to terminate
+
+### Examples
+
+```bash
+# Connect to local development server
+circles_client ws://localhost:8080
+
+# Connect to secure WebSocket with verbose logging
+circles_client -v wss://circles.example.com/ws
+
+# Execute a simple calculation
+circles_client -s "let result = 2 + 2; print(result);" ws://localhost:8080
+
+# Load and execute a complex script
+circles_client -f examples/complex_script.rhai ws://localhost:8080
+```
+
+### Error Handling
+
+The client provides clear error messages for common issues:
+- Missing or invalid private key
+- Connection failures
+- Authentication errors
+- Script execution errors
+
+### Dependencies
+
+- `tokio-tungstenite`: WebSocket client implementation
+- `secp256k1`: Cryptographic authentication
+- `clap`: Command-line argument parsing
+- `env_logger`: Logging infrastructure
+- `dotenv`: Environment variable loading
\ No newline at end of file
diff --git a/src/client_ws/cmd/main.rs b/src/client_ws/cmd/main.rs
new file mode 100644
index 0000000..233768b
--- /dev/null
+++ b/src/client_ws/cmd/main.rs
@@ -0,0 +1,249 @@
+use circle_client_ws::CircleWsClientBuilder;
+use clap::{Arg, ArgAction, Command};
+use dotenv::dotenv;
+use env_logger;
+use log::{error, info};
+use std::env;
+use std::io::{self, Write};
+use std::path::Path;
+use tokio;
+
+#[derive(Debug)]
+struct Args {
+ ws_url: String,
+ script: Option,
+ script_path: Option,
+ verbose: u8,
+ no_timestamp: bool,
+}
+
+fn parse_args() -> Args {
+ let matches = Command::new("circles_client")
+ .version("0.1.0")
+ .about("WebSocket client for Circles server")
+ .arg(
+ Arg::new("url")
+ .help("WebSocket server URL")
+ .required(true)
+ .index(1),
+ )
+ .arg(
+ Arg::new("script")
+ .short('s')
+ .long("script")
+ .value_name("SCRIPT")
+ .help("Rhai script to execute")
+ .conflicts_with("script_path"),
+ )
+ .arg(
+ Arg::new("script_path")
+ .short('f')
+ .long("file")
+ .value_name("FILE")
+ .help("Path to Rhai script file")
+ .conflicts_with("script"),
+ )
+ .arg(
+ Arg::new("verbose")
+ .short('v')
+ .long("verbose")
+ .help("Increase verbosity (can be used multiple times)")
+ .action(ArgAction::Count),
+ )
+ .arg(
+ Arg::new("no_timestamp")
+ .long("no-timestamp")
+ .help("Remove timestamps from log output")
+ .action(ArgAction::SetTrue),
+ )
+ .get_matches();
+
+ Args {
+ ws_url: matches.get_one::("url").unwrap().clone(),
+ script: matches.get_one::("script").cloned(),
+ script_path: matches.get_one::("script_path").cloned(),
+ verbose: matches.get_count("verbose"),
+ no_timestamp: matches.get_flag("no_timestamp"),
+ }
+}
+
+fn setup_logging(verbose: u8, no_timestamp: bool) {
+ let log_level = match verbose {
+ 0 => "warn,circle_client_ws=info",
+ 1 => "info,circle_client_ws=debug",
+ 2 => "debug",
+ _ => "trace",
+ };
+
+ std::env::set_var("RUST_LOG", log_level);
+
+ // Configure env_logger with or without timestamps
+ if no_timestamp {
+ env_logger::Builder::from_default_env()
+ .format_timestamp(None)
+ .init();
+ } else {
+ env_logger::init();
+ }
+}
+
+fn load_private_key() -> Result> {
+ // Try to load from .env file first
+ if let Ok(_) = dotenv() {
+ if let Ok(key) = env::var("PRIVATE_KEY") {
+ return Ok(key);
+ }
+ }
+
+ // Try to load from cmd/.env file
+ let cmd_env_path = Path::new("cmd/.env");
+ if cmd_env_path.exists() {
+ dotenv::from_path(cmd_env_path)?;
+ if let Ok(key) = env::var("PRIVATE_KEY") {
+ return Ok(key);
+ }
+ }
+
+ Err("PRIVATE_KEY not found in environment or .env files".into())
+}
+
+async fn run_interactive_mode(client: circle_client_ws::CircleWsClient) -> Result<(), Box> {
+ info!("Entering interactive mode. Type 'exit' or 'quit' to leave.");
+ println!("🔄 Interactive mode - Enter Rhai scripts (type 'exit' or 'quit' to leave):");
+
+ loop {
+ print!("rhai> ");
+ io::stdout().flush()?;
+
+ let mut input = String::new();
+ io::stdin().read_line(&mut input)?;
+
+ let script = input.trim();
+
+ if script.is_empty() {
+ continue;
+ }
+
+ if script == "exit" || script == "quit" {
+ println!("👋 Goodbye!");
+ break;
+ }
+
+ match client.play(script.to_string()).await {
+ Ok(result) => {
+ println!("📤 Result: {}", result.output);
+ }
+ Err(e) => {
+ error!("❌ Script execution failed: {}", e);
+ println!("❌ Error: {}", e);
+ }
+ }
+ }
+
+ Ok(())
+}
+
+async fn execute_script(client: circle_client_ws::CircleWsClient, script: String) -> Result<(), Box> {
+ info!("Executing script: {}", script);
+
+ match client.play(script).await {
+ Ok(result) => {
+ println!("{}", result.output);
+ Ok(())
+ }
+ Err(e) => {
+ error!("Script execution failed: {}", e);
+ Err(e.into())
+ }
+ }
+}
+
+async fn load_script_from_file(path: &str) -> Result> {
+ let script = tokio::fs::read_to_string(path).await?;
+ Ok(script)
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ let args = parse_args();
+ setup_logging(args.verbose, args.no_timestamp);
+
+ info!("🚀 Starting Circles WebSocket client");
+ info!("📡 Connecting to: {}", args.ws_url);
+
+ // Load private key from environment
+ let private_key = match load_private_key() {
+ Ok(key) => {
+ info!("🔑 Private key loaded from environment");
+ key
+ }
+ Err(e) => {
+ error!("❌ Failed to load private key: {}", e);
+ eprintln!("Error: {}", e);
+ eprintln!("Please set PRIVATE_KEY in your environment or create a cmd/.env file with:");
+ eprintln!("PRIVATE_KEY=your_private_key_here");
+ std::process::exit(1);
+ }
+ };
+
+ // Build client with private key
+ let mut client = CircleWsClientBuilder::new(args.ws_url.clone())
+ .with_keypair(private_key)
+ .build();
+
+ // Connect to WebSocket server
+ info!("🔌 Connecting to WebSocket server...");
+ if let Err(e) = client.connect().await {
+ error!("❌ Failed to connect: {}", e);
+ eprintln!("Connection failed: {}", e);
+ std::process::exit(1);
+ }
+ info!("✅ Connected successfully");
+
+ // Authenticate with server
+ info!("🔐 Authenticating with server...");
+ match client.authenticate().await {
+ Ok(true) => {
+ info!("✅ Authentication successful");
+ println!("🔐 Authentication successful");
+ }
+ Ok(false) => {
+ error!("❌ Authentication failed");
+ eprintln!("Authentication failed");
+ std::process::exit(1);
+ }
+ Err(e) => {
+ error!("❌ Authentication error: {}", e);
+ eprintln!("Authentication error: {}", e);
+ std::process::exit(1);
+ }
+ }
+
+ // Determine execution mode
+ let result = if let Some(script) = args.script {
+ // Execute provided script and exit
+ execute_script(client, script).await
+ } else if let Some(script_path) = args.script_path {
+ // Load script from file and execute
+ match load_script_from_file(&script_path).await {
+ Ok(script) => execute_script(client, script).await,
+ Err(e) => {
+ error!("❌ Failed to load script from file '{}': {}", script_path, e);
+ eprintln!("Failed to load script file: {}", e);
+ std::process::exit(1);
+ }
+ }
+ } else {
+ // Enter interactive mode
+ run_interactive_mode(client).await
+ };
+
+ // Handle any errors from execution
+ if let Err(e) = result {
+ error!("❌ Execution failed: {}", e);
+ std::process::exit(1);
+ }
+
+ info!("🏁 Client finished successfully");
+ Ok(())
+}
\ No newline at end of file
diff --git a/src/client_ws/src/lib.rs b/src/client_ws/src/lib.rs
index a5f9b48..4121990 100644
--- a/src/client_ws/src/lib.rs
+++ b/src/client_ws/src/lib.rs
@@ -22,15 +22,13 @@ use {
#[cfg(not(target_arch = "wasm32"))]
use {
- native_tls::TlsConnector,
- tokio::net::TcpStream,
tokio::spawn as spawn_local,
tokio_tungstenite::{
+ connect_async, connect_async_tls_with_config,
tungstenite::{
- client::IntoClientRequest, handshake::client::Response,
protocol::Message as TungsteniteWsMessage,
},
- MaybeTlsStream, WebSocketStream,
+ Connector,
},
};
@@ -284,9 +282,10 @@ impl CircleWsClient {
let (internal_tx, internal_rx) = mpsc::channel::(32);
self.internal_tx = Some(internal_tx);
- // Determine the final URL to connect to - always use the base ws_url now
- let connection_url = self.ws_url.replace("ws://", "ws://");
- info!("Connecting to WebSocket: {}", connection_url);
+ // Use the URL as provided - support both ws:// and wss://
+ let connection_url = self.ws_url.clone();
+ let is_secure = connection_url.starts_with("wss://");
+ info!("🔗 Connecting to WebSocket: {} ({})", connection_url, if is_secure { "WSS/TLS" } else { "WS/Plain" });
// Pending requests: map request_id to a oneshot sender for the response
let pending_requests: Arc<
@@ -307,66 +306,37 @@ impl CircleWsClient {
#[cfg(not(target_arch = "wasm32"))]
let connect_attempt = async {
- let mut request = connection_url
- .into_client_request()
- .map_err(|e| CircleWsClientError::ConnectionError(e.to_string()))?;
- let _headers = request.headers_mut();
- // You can add custom headers here if needed, for example:
- // headers.insert("My-Header", "My-Value".try_into().unwrap());
-
- let connector = TlsConnector::builder()
- .danger_accept_invalid_certs(true)
- .build()
- .map_err(|e| {
- CircleWsClientError::ConnectionError(format!(
- "Failed to create TLS connector: {}",
- e
- ))
- })?;
-
- let authority = request
- .uri()
- .authority()
- .ok_or_else(|| {
- CircleWsClientError::ConnectionError(
- "Invalid URL: missing authority".to_string(),
- )
- })?
- .as_str();
- let host = request.uri().host().unwrap_or_default();
-
- let stream = TcpStream::connect(authority).await.map_err(|e| {
- CircleWsClientError::ConnectionError(format!(
- "Failed to connect TCP stream: {}",
- e
- ))
- })?;
-
- let tls_stream = tokio_native_tls::TlsConnector::from(connector)
- .connect(host, stream)
- .await
- .map_err(|e| {
- CircleWsClientError::ConnectionError(format!(
- "Failed to establish TLS connection: {}",
- e
- ))
- })?;
-
- let (ws_stream, response) = tokio_tungstenite::client_async_with_config(
- request,
- MaybeTlsStream::NativeTls(tls_stream),
- None, // WebSocketConfig
- )
- .await
- .map_err(|e| CircleWsClientError::ConnectionError(e.to_string()))?;
-
- Ok((ws_stream, response))
+ // Check if this is a secure WebSocket connection
+ if connection_url.starts_with("wss://") {
+ // For WSS connections, use a custom TLS connector that accepts self-signed certificates
+ // This is for development/demo purposes only
+ use tokio_tungstenite::tungstenite::client::IntoClientRequest;
+
+ let request = connection_url.into_client_request()
+ .map_err(|e| CircleWsClientError::ConnectionError(format!("Invalid URL: {}", e)))?;
+
+ // Create a native-tls connector that accepts invalid certificates (for development)
+ let tls_connector = native_tls::TlsConnector::builder()
+ .danger_accept_invalid_certs(true)
+ .danger_accept_invalid_hostnames(true)
+ .build()
+ .map_err(|e| CircleWsClientError::ConnectionError(format!("TLS connector creation failed: {}", e)))?;
+
+ let connector = Connector::NativeTls(tls_connector);
+
+ warn!("⚠️ DEVELOPMENT MODE: Accepting self-signed certificates (NOT for production!)");
+ connect_async_tls_with_config(request, None, false, Some(connector))
+ .await
+ .map_err(|e| CircleWsClientError::ConnectionError(format!("WSS connection failed: {}", e)))
+ } else {
+ // For regular WS connections, use the standard method
+ connect_async(&connection_url)
+ .await
+ .map_err(|e| CircleWsClientError::ConnectionError(format!("WS connection failed: {}", e)))
+ }
};
#[cfg(not(target_arch = "wasm32"))]
- let ws_result: Result<
- (WebSocketStream>, Response),
- CircleWsClientError,
- > = connect_attempt.await;
+ let ws_result = connect_attempt.await;
match ws_result {
Ok(ws_conn_maybe_response) => {
diff --git a/src/launcher/.DS_Store b/src/launcher/.DS_Store
deleted file mode 100644
index 83cf3c47e7c57a7cad4866bf8ccc700610e5c03c..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 6148
zcmeHKJx{|x47F*8NGu&0Z$?7Uogq|VX6PS4Z6z35rJ`cb)?ZeB6C=+bK&pt24Dc-Z
zUe5NrQ{E8ABBHa`)l_66A{E?F_O=Yo^5z3O%g7k0cE)tRY&Y9M-Kl_aN3xR_S+1Y@
z{OiMaX45v!V%;tfsUCJ$x0mm4cm3?@M0We!KlKJQ8Wo@dRDcRlfq$(4dbU}09>|pn
zPys6NrGR}O3f!;nDMfyGAvp#8?X;n`;iV6gzOPV52^foV{ILDd{FH0X$ztg92d
zz@Urz%{V7-)|^n(Z%4d%xo928l?qUSV+BUBoml_BhyO7DACtJF0#x8nDWKzK-puey
z*;@xMXT7$-AK{;dS}(`qtr+O77#nNF7rnY-&&bz_U7*tucRG+i0;UU%3jBrwALdUc
AM*si-
diff --git a/src/launcher/README.md b/src/launcher/README.md
deleted file mode 100644
index 6035063..0000000
--- a/src/launcher/README.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# `launcher`: The Circles Orchestration Utility
-
-The `launcher` is a command-line utility designed to spawn, manage, and monitor multiple, isolated "Circle" instances. It reads a simple JSON configuration file and, for each entry, launches a dedicated `worker` process and a corresponding WebSocket server.
-
-This new architecture emphasizes isolation and robustness by running each Circle's worker as a separate OS process, identified by a unique public key.
-
-## Core Architectural Concepts
-
-- **Process-Based Isolation**: Instead of spawning workers as in-process tasks, the `launcher` spawns the `worker` binary as a separate OS process for each Circle. This ensures that a crash in one worker does not affect the launcher or other Circles.
-- **Public Key as Unique Identifier**: Each Circle is identified by a unique `secp256k1` public key, which is generated on startup. This key is used to name Redis queues and identify the Circle across the system, replacing the old numeric `id`.
-- **Task Submission via Redis**: The launcher uses the `rhai_client` library to submit initialization scripts as tasks to the worker's dedicated Redis queue. The worker listens on this queue, executes the script, and posts the result back to Redis.
-- **Dynamic Configuration**: All Circle instances are defined in a `circles.json` file, specifying their name, port, and an optional initialization script.
-
-## Features
-
-- **Multi-Circle Management**: Run multiple, independent WebSocket servers and Rhai script workers from a single command.
-- **Dynamic Key Generation**: Automatically generates a unique `secp256k1` keypair for each Circle on startup.
-- **Configuration via JSON**: Define all your Circle instances in a `circles.json` file.
-- **Graceful Shutdown**: Listens for `Ctrl+C` to initiate a graceful shutdown of all spawned servers and worker processes.
-- **Status Display**: On startup, it displays a convenient table showing the name, unique public key, worker queue, and WebSocket URL for each running Circle.
-- **Verbose Logging**: Supports multiple levels of verbosity (`-v`, `-vv`, `-vvv`) and a debug flag (`-d`) for detailed logging.
-
-## How to Use
-
-1. **Create `circles.json`**: Create a `circles.json` file. The launcher will look for it in the current directory by default. You can also provide a path as a command-line argument.
-
- *The `id` field is now obsolete and has been removed.*
-
- ```json
- [
- {
- "name": "OurWorld",
- "port": 8090,
- "script_path": "scripts/ourworld.rhai"
- },
- {
- "name": "Dunia Cybercity",
- "port": 8091,
- "script_path": "scripts/dunia_cybercity.rhai"
- }
- ]
- ```
-
-2. **Run the Launcher**: Execute the following command from the root of the `circles` project:
-
- ```bash
- # The launcher will find './circles.json' by default
- cargo run --package launcher
-
- # Or specify a path to the config file
- cargo run --package launcher -- ./examples/test_circles.json
- ```
-
-3. **Add Verbosity (Optional)**: For more detailed logs, use the verbosity flags:
-
- ```bash
- # Info-level logging for Actix
- cargo run --package launcher -- -v
-
- # Debug-level logging for project crates
- cargo run --package launcher -- -vv
-
- # Full debug logging
- cargo run --package launcher -- -vvv
- ```
-
-## What It Does
-
-For each entry in `circles.json`, the launcher will:
-1. Generate a new `secp256k1` keypair. The public key becomes the Circle's unique identifier.
-2. Spawn the `worker` binary as a child OS process, passing it the public key and Redis URL.
-3. Initialize a `server_ws` instance on the specified port.
-4. If a `script_path` is provided, it reads the script and submits it as a task to the worker's Redis queue. The `CALLER_PUBLIC_KEY` for this initial script is set to the Circle's own public key.
-
-This makes it an ideal tool for setting up complex, multi-instance development environments or for deploying a full suite of Circle services with strong process isolation.
\ No newline at end of file
diff --git a/src/launcher/examples/confirm_launch.rs b/src/launcher/examples/confirm_launch.rs
deleted file mode 100644
index 6c565da..0000000
--- a/src/launcher/examples/confirm_launch.rs
+++ /dev/null
@@ -1,114 +0,0 @@
-use rhai_client::RhaiClient;
-use std::io::{BufRead, BufReader};
-use std::process::{Child, Command, Stdio};
-use std::time::Duration;
-use tokio::sync::mpsc;
-
-const REDIS_URL: &str = "redis://127.0.0.1:6379";
-
-#[tokio::main]
-async fn main() -> Result<(), Box> {
- println!("--- Starting End-to-End Circle Launch Confirmation ---");
-
- // Start the launcher
- let mut launcher_process: Child = Command::new("cargo")
- .arg("run")
- .arg("--bin")
- .arg("launcher")
- .arg("--")
- .arg("./examples/test_circles.json")
- .stdout(Stdio::piped())
- .stderr(Stdio::piped()) // Pipe stderr to avoid interfering with the test output
- .spawn()?;
-
- println!(
- "Launcher process started with PID: {}",
- launcher_process.id()
- );
-
- let stdout = launcher_process
- .stdout
- .take()
- .expect("Failed to capture stdout");
- let mut reader = BufReader::new(stdout);
- let (tx, mut rx) = mpsc::channel::(1);
-
- // Spawn a task to read stdout and find the public key
- tokio::spawn(async move {
- let mut line = String::new();
- loop {
- if reader.read_line(&mut line).unwrap_or(0) > 0 {
- if line.contains("Public Key") {
- if let Some(key) = line.split(": ").last() {
- tx.send(key.trim().to_string()).await.ok();
- break; // Found the key, exit the loop
- }
- }
- line.clear();
- } else {
- break; // EOF
- }
- }
- });
-
- // Wait for the public key
- let public_key = match tokio::time::timeout(Duration::from_secs(10), rx.recv()).await {
- Ok(Some(key)) => key,
- _ => {
- launcher_process.kill()?;
- return Err("Did not receive public key from launcher within 10 seconds".into());
- }
- };
-
- println!("Found public key: {}", public_key);
-
- let client = RhaiClient::new(REDIS_URL)?;
-
- // Test 1: Verify that CIRCLE_PUBLIC_KEY is set correctly.
- println!("--- Test 1: Verifying CIRCLE_PUBLIC_KEY ---");
- let script_circle_pk = r#"CIRCLE_PUBLIC_KEY"#;
- println!("Submitting script to verify CIRCLE_PUBLIC_KEY...");
- let task_details_circle_pk = client
- .submit_script_and_await_result(
- &public_key,
- script_circle_pk.to_string(),
- "task_id".to_string(),
- Duration::from_secs(10),
- None, // Caller PK is not relevant for this constant.
- )
- .await?;
- println!("Received task details: {:?}", task_details_circle_pk);
- assert_eq!(task_details_circle_pk.status, "completed");
- assert_eq!(task_details_circle_pk.output, Some(public_key.to_string()));
- println!("✅ SUCCESS: Worker correctly reported its CIRCLE_PUBLIC_KEY.");
-
- // Test 2: Verify that CALLER_PUBLIC_KEY is set correctly when the launcher calls.
- // We simulate the launcher by passing the circle's own PK as the caller.
- println!("\n--- Test 2: Verifying CALLER_PUBLIC_KEY for init scripts ---");
- let script_caller_pk = r#"CALLER_PUBLIC_KEY"#;
- println!("Submitting script to verify CALLER_PUBLIC_KEY...");
- let task_details_caller_pk = client
- .submit_script_and_await_result(
- &public_key,
- script_caller_pk.to_string(),
- "task_id".to_string(),
- Duration::from_secs(10),
- Some(public_key.clone()), // Simulate launcher by setting caller to the circle itself.
- )
- .await?;
- println!("Received task details: {:?}", task_details_caller_pk);
- assert_eq!(task_details_caller_pk.status, "completed");
- assert_eq!(task_details_caller_pk.output, Some(public_key.to_string()));
- println!("✅ SUCCESS: Worker correctly reported CALLER_PUBLIC_KEY for init script.");
-
- // Gracefully shut down the launcher
- println!("Shutting down launcher process...");
- launcher_process.kill()?;
- tokio::task::spawn_blocking(move || {
- let _ = launcher_process.wait();
- })
- .await?;
- println!("--- End-to-End Test Finished Successfully ---");
-
- Ok(())
-}
diff --git a/src/launcher/examples/test_circles.json b/src/launcher/examples/test_circles.json
deleted file mode 100644
index 6b5b0de..0000000
--- a/src/launcher/examples/test_circles.json
+++ /dev/null
@@ -1,3 +0,0 @@
-[
- { "name": "Test Circle", "port": 9000, "script_path": "test_script.rhai" }
-]
\ No newline at end of file
diff --git a/src/launcher/examples/test_script.rhai b/src/launcher/examples/test_script.rhai
deleted file mode 100644
index 2cfcb1d..0000000
--- a/src/launcher/examples/test_script.rhai
+++ /dev/null
@@ -1,2 +0,0 @@
-// test_script.rhai
-"Hello from the test circle!"
\ No newline at end of file
diff --git a/src/launcher/launch_data/circle_db_test_circle.db/data/lookup/.inc b/src/launcher/launch_data/circle_db_test_circle.db/data/lookup/.inc
deleted file mode 100644
index 56a6051..0000000
--- a/src/launcher/launch_data/circle_db_test_circle.db/data/lookup/.inc
+++ /dev/null
@@ -1 +0,0 @@
-1
\ No newline at end of file
diff --git a/src/launcher/launch_data/circle_db_test_circle.db/data/lookup/data b/src/launcher/launch_data/circle_db_test_circle.db/data/lookup/data
deleted file mode 100644
index fe77ee9d3edc2324ba041f73b723d96d7de022ea..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 4000000
zcmeIu0Sy2E0K%a6Pi+qe5hx58Fkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5
zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM
z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*
z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd
z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA
zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj
zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r
z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@
z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK
sfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfPwdc0SuD>0RR91
diff --git a/src/launcher/launch_data/circle_db_test_circle.db/index/0.db b/src/launcher/launch_data/circle_db_test_circle.db/index/0.db
deleted file mode 100644
index 0ad682e3dd2bf565474087acb8a9ae3a4040d169..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 35
VcmZP&V^FxtxSs(G7$FoI2>>>+0nGpa
diff --git a/src/launcher/launch_data/circle_db_test_circle.db/index/lookup/.inc b/src/launcher/launch_data/circle_db_test_circle.db/index/lookup/.inc
deleted file mode 100644
index d8263ee..0000000
--- a/src/launcher/launch_data/circle_db_test_circle.db/index/lookup/.inc
+++ /dev/null
@@ -1 +0,0 @@
-2
\ No newline at end of file
diff --git a/src/launcher/launch_data/circle_db_test_circle.db/index/lookup/data b/src/launcher/launch_data/circle_db_test_circle.db/index/lookup/data
deleted file mode 100644
index 92d764d22ee42034d4f280bb0e7a296f3f868902..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 4000000
zcmeIu!3_Wa2m&$Oe`yok!j#AHZ_>@35+Fc;009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N
z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+
z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly
zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ
zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7
y2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk9|aZ}9{>UX
diff --git a/src/launcher/src/cmd/main.rs b/src/launcher/src/cmd/main.rs
deleted file mode 100644
index 9d76edb..0000000
--- a/src/launcher/src/cmd/main.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-use clap::Parser;
-use launcher::{run_launcher, Args};
-
-use launcher::CircleConfig;
-use log::{error, info};
-use std::error::Error as StdError; // Import the trait
-use std::fs;
-
-#[tokio::main]
-async fn main() -> Result<(), Box> {
- // Use the alias for clarity
- let args = Args::parse();
-
- let config_path = &args.config_path;
- if !config_path.exists() {
- error!(
- "Configuration file not found at {:?}. Please create circles.json.",
- config_path
- );
- // Create a simple string error that can be boxed into Box
- return Err(String::from("circles.json not found").into());
- }
-
- let config_content =
- fs::read_to_string(&config_path).map_err(|e| Box::new(e) as Box)?;
-
- let circle_configs: Vec = match serde_json::from_str(&config_content) {
- Ok(configs) => configs,
- Err(e) => {
- error!(
- "Failed to parse circles.json: {}. Ensure it's a valid JSON array of CircleConfig.",
- e
- );
- // Explicitly cast serde_json::Error to Box
- return Err(Box::new(e) as Box);
- }
- };
-
- if circle_configs.is_empty() {
- info!(
- "No circle configurations found in {}. Exiting.",
- config_path.display()
- );
- return Ok(());
- }
-
- // run_launcher already returns Result<(), Box>, so this should be fine.
- run_launcher(args, circle_configs).await
-}
diff --git a/src/launcher/src/lib.rs b/src/launcher/src/lib.rs
deleted file mode 100644
index cb9866a..0000000
--- a/src/launcher/src/lib.rs
+++ /dev/null
@@ -1,358 +0,0 @@
-use serde::{Deserialize, Serialize};
-use std::fs;
-use std::path::PathBuf;
-use std::str::FromStr;
-use std::sync::{Arc, Mutex};
-// std::process::{Command, Child, Stdio}; // All parts of this line are no longer used directly here
-use actix_web::dev::ServerHandle;
-use circle_ws_lib::{spawn_circle_server, ServerConfig};
-use clap::Parser;
-use comfy_table::{Cell, Row, Table};
-use log::{info, warn};
-use rhai_client::RhaiClient;
-use secp256k1::{rand, Secp256k1};
-use std::time::Duration;
-use tokio::signal;
-use tokio::task::JoinHandle;
-// use rhai::Engine; // No longer directly used, engine comes from create_heromodels_engine
-use engine::create_heromodels_engine;
-use heromodels::db::hero::OurDB;
-use rhailib_worker::spawn_rhai_worker; // Added
-use std::env; // Added
-use tokio::sync::mpsc; // Added
-
-const DEFAULT_REDIS_URL: &str = "redis://127.0.0.1:6379";
-
-#[derive(Parser, Debug, Clone)]
-#[command(author, version, about, long_about = None)]
-pub struct Args {
- /// Path to the circles.json configuration file
- #[arg(default_value = "./circles.json")]
- pub config_path: PathBuf,
-
- /// Optional path to write the output JSON file with circle details
- #[arg(long)]
- pub output: Option,
-
- /// Enable debug mode
- #[arg(short, long)]
- pub debug: bool,
-
- /// Verbosity level
- #[arg(short, long, action = clap::ArgAction::Count)]
- pub verbose: u8,
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub struct CircleConfig {
- pub name: String,
- pub port: u16,
- pub script_path: Option,
- pub public_key: Option,
- pub secret_key: Option,
-}
-
-#[derive(Serialize, Debug, Clone)]
-pub struct CircleOutput {
- pub name: String,
- pub public_key: String,
- pub secret_key: String,
- pub worker_queue: String,
- pub ws_url: String,
-}
-
-pub struct RunningCircleInfo {
- pub config: CircleConfig,
- pub worker_queue: String,
- pub ws_url: String,
- pub public_key: String,
- // pub worker_process: Child, // Changed
- pub worker_task_join_handle: JoinHandle>>, // Added
- pub worker_shutdown_tx: mpsc::Sender<()>, // Added
- pub ws_server_instance_handle: Arc>>,
- pub _ws_server_task_join_handle: JoinHandle>,
-}
-
-pub async fn setup_and_spawn_circles(
- circle_configs: Vec,
-) -> Result<(Vec>>, Vec), Box> {
- if circle_configs.is_empty() {
- warn!("No circle configurations found. Exiting.");
- return Ok((Vec::new(), Vec::new()));
- }
- info!("Loaded {} circle configurations.", circle_configs.len());
-
- let rhai_client = RhaiClient::new(DEFAULT_REDIS_URL)?;
- let mut running_circles_store: Vec>> = Vec::new();
- let mut circle_outputs: Vec = Vec::new();
-
- let data_dir = PathBuf::from("./launch_data");
- if !data_dir.exists() {
- fs::create_dir_all(&data_dir).map_err(|e| {
- format!(
- "Failed to create data directory '{}': {}",
- data_dir.display(),
- e
- )
- })?;
- info!("Created data directory: {}", data_dir.display());
- }
-
- for (idx, config) in circle_configs.into_iter().enumerate() {
- // Added enumerate for circle_id
- info!(
- "Initializing Circle Name: '{}', Port: {}",
- config.name, config.port
- );
-
- let secp = Secp256k1::new();
- let (secret_key, public_key) = if let (Some(sk_str), Some(pk_str)) =
- (&config.secret_key, &config.public_key)
- {
- info!("Using provided keypair for circle '{}'", config.name);
- let secret_key = secp256k1::SecretKey::from_str(sk_str)
- .map_err(|e| format!("Invalid secret key for circle '{}': {}", config.name, e))?;
- let public_key = secp256k1::PublicKey::from_str(pk_str)
- .map_err(|e| format!("Invalid public key for circle '{}': {}", config.name, e))?;
- if public_key != secp256k1::PublicKey::from_secret_key(&secp, &secret_key) {
- return Err(format!(
- "Provided public key does not match secret key for circle '{}'",
- config.name
- )
- .into());
- }
- (secret_key, public_key)
- } else {
- info!("Generating new keypair for circle '{}'", config.name);
- secp.generate_keypair(&mut rand::thread_rng())
- };
- let public_key_hex = public_key.to_string();
-
- // Spawn Rhai worker as a Tokio task
- let (worker_shutdown_tx, worker_shutdown_rx) = mpsc::channel(1);
-
- // --- Initialize OurDB and the Rhai Engine for this circle ---
- let db_path_str = format!("./launch_data/circle_db_{}.db", config.name);
- let db = Arc::new(OurDB::new(db_path_str, true)?);
- let engine = create_heromodels_engine(db.clone());
- // --- End Engine Initialization ---
-
- let redis_url = env::var("REDIS_URL").unwrap_or_else(|_| DEFAULT_REDIS_URL.to_string());
-
- // Using idx as a placeholder for circle_id. Consider a more robust ID if needed.
- let circle_id_for_worker = idx as u32;
- // Defaulting preserve_tasks to false. Make configurable if needed.
- let preserve_tasks = env::var("PRESERVE_TASKS").is_ok();
-
- let worker_task_join_handle = spawn_rhai_worker(
- circle_id_for_worker,
- public_key_hex.clone(),
- engine,
- redis_url.clone(),
- worker_shutdown_rx,
- preserve_tasks,
- );
-
- let worker_queue = format!("rhai_tasks:{}", public_key_hex);
- let ws_url = format!("ws://127.0.0.1:{}", config.port);
-
- // If a script is provided, read it and submit it to the worker
- if let Some(script_path_str) = &config.script_path {
- info!(
- "Found script for circle '{}' at path: {}",
- config.name, script_path_str
- );
- let script_path = PathBuf::from(script_path_str);
- if script_path.exists() {
- let script_content = fs::read_to_string(&script_path).map_err(|e| {
- format!(
- "Failed to read script file '{}': {}",
- script_path.display(),
- e
- )
- })?;
-
- info!("Submitting script to worker queue '{}'", worker_queue);
- let task_id = rhai_client
- .submit_script(
- &public_key_hex, // Use public key as the circle identifier
- script_content,
- Some(public_key_hex.clone()),
- )
- .await?;
- info!(
- "Script for circle '{}' submitted with task ID: {}",
- config.name, task_id
- );
- } else {
- warn!(
- "Script path '{}' for circle '{}' does not exist. Skipping.",
- script_path.display(),
- config.name
- );
- }
- }
-
- // Spawn the WebSocket server for the circle
- let server_config = ServerConfig {
- port: config.port,
- circle_name: config.name.clone(),
- circle_public_key: public_key_hex.clone(),
- redis_url: env::var("REDIS_URL").unwrap_or_else(|_| DEFAULT_REDIS_URL.to_string()), // Ensure server also gets correct redis_url
- host: "127.0.0.1".to_string(),
- enable_auth: false,
- cert_path: None,
- key_path: None,
- };
-
- let (ws_server_task_join_handle, ws_server_instance_handle) =
- spawn_circle_server(server_config.clone())?;
-
- circle_outputs.push(CircleOutput {
- name: config.name.clone(),
- public_key: public_key_hex.clone(),
- secret_key: secret_key.display_secret().to_string(),
- worker_queue: worker_queue.clone(),
- ws_url: ws_url.clone(),
- });
-
- running_circles_store.push(Arc::new(Mutex::new(RunningCircleInfo {
- config,
- worker_queue,
- ws_url,
- public_key: public_key_hex,
- worker_task_join_handle, // Changed
- worker_shutdown_tx, // Added
- ws_server_instance_handle: Arc::new(Mutex::new(Some(ws_server_instance_handle))),
- _ws_server_task_join_handle: ws_server_task_join_handle,
- })));
- }
- Ok((running_circles_store, circle_outputs))
-}
-
-pub async fn shutdown_circles(running_circles_store: Vec>>) {
- for circle_arc in &running_circles_store {
- let (name, worker_shutdown_tx, mut worker_task_join_handle_opt, server_handle_opt);
- {
- let mut circle_info = circle_arc.lock().unwrap();
- name = circle_info.config.name.clone();
- // Take ownership of the JoinHandle and Sender for shutdown
- // We need to replace them with something to satisfy the struct,
- // but they won't be used again for this instance.
- let (dummy_tx, _dummy_rx) = mpsc::channel(1);
- worker_shutdown_tx = std::mem::replace(&mut circle_info.worker_shutdown_tx, dummy_tx);
-
- // Create a dummy JoinHandle for replacement
- let dummy_join_handle = tokio::spawn(async {
- Ok(()) as Result<(), Box>
- });
- worker_task_join_handle_opt = Some(std::mem::replace(
- &mut circle_info.worker_task_join_handle,
- dummy_join_handle,
- ));
-
- server_handle_opt = circle_info.ws_server_instance_handle.lock().unwrap().take();
- }
-
- info!("Shutting down Circle: '{}'", name);
-
- // Shutdown worker task
- if let Err(e) = worker_shutdown_tx.send(()).await {
- warn!("Failed to send shutdown signal to worker for Circle '{}': {}. Worker might have already exited.", name, e);
- }
-
- if let Some(worker_task_join_handle) = worker_task_join_handle_opt.take() {
- match worker_task_join_handle.await {
- Ok(Ok(_)) => info!("Worker task for Circle '{}' shut down gracefully.", name),
- Ok(Err(e)) => warn!(
- "Worker task for Circle '{}' returned an error: {:?}",
- name, e
- ),
- Err(e) => warn!("Worker task for Circle '{}' panicked: {:?}", name, e),
- }
- } else {
- warn!("No worker task join handle found for Circle '{}'.", name);
- }
-
- // Shutdown WebSocket server
- if let Some(handle) = server_handle_opt {
- info!("Stopping WebSocket server for Circle '{}' ...", name);
- handle.stop(true).await;
- info!("WebSocket server for Circle '{}' stop signal sent.", name);
- } else {
- warn!(
- "No server handle to stop WebSocket server for Circle '{}'.",
- name
- );
- }
- }
-}
-
-pub async fn run_launcher(
- args: Args,
- circle_configs: Vec,
-) -> Result<(), Box> {
- if std::env::var("RUST_LOG").is_err() {
- let log_level = if args.debug {
- "debug".to_string()
- } else {
- match args.verbose {
- 0 => "info,actix_server=warn,actix_web=warn".to_string(),
- 1 => "info,circle_ws_lib=info,actix_server=info,actix_web=info".to_string(),
- 2 => "debug,launcher=debug,worker_lib=debug,circle_ws_lib=debug,actix_server=info,actix_web=info".to_string(),
- _ => "debug".to_string(),
- }
- };
- std::env::set_var("RUST_LOG", log_level);
- }
- env_logger::init();
-
- info!("Starting Circles Orchestrator...");
-
- let (running_circles_store, circle_outputs) = setup_and_spawn_circles(circle_configs).await?;
-
- if running_circles_store.is_empty() {
- warn!("No circles were started. Exiting.");
- return Ok(());
- }
-
- info!("All configured circles have been processed. Displaying circles table.");
-
- {
- let circles = running_circles_store
- .iter()
- .map(|arc_info| arc_info.lock().unwrap())
- .collect::>();
-
- let mut table = Table::new();
- table.set_header(vec!["Name", "Public Key", "Worker Queue", "WS URL"]);
-
- for info in circles.iter() {
- let mut row = Row::new();
- row.add_cell(Cell::new(&info.config.name));
- row.add_cell(Cell::new(&info.public_key));
- row.add_cell(Cell::new(&info.worker_queue));
- row.add_cell(Cell::new(&info.ws_url));
- table.add_row(row);
- }
- println!("{}", table);
- }
-
- if let Some(output_path) = args.output {
- info!("Writing circle details to {:?}", output_path);
- let json_output = serde_json::to_string_pretty(&circle_outputs)?;
- fs::write(&output_path, json_output)?;
- info!("Successfully wrote circle details to {:?}", output_path);
- }
-
- info!("Press Ctrl+C to shutdown all circles.");
- signal::ctrl_c().await?;
- info!("Ctrl-C received. Initiating graceful shutdown of all circles...");
-
- shutdown_circles(running_circles_store).await;
-
- tokio::time::sleep(Duration::from_secs(2)).await;
-
- info!("Orchestrator shut down complete.");
- Ok(())
-}
diff --git a/src/launcher/tests/spawn_test.rs b/src/launcher/tests/spawn_test.rs
deleted file mode 100644
index d8294e6..0000000
--- a/src/launcher/tests/spawn_test.rs
+++ /dev/null
@@ -1,120 +0,0 @@
-use futures_util::{SinkExt, StreamExt};
-use launcher::{setup_and_spawn_circles, shutdown_circles, CircleConfig};
-use secp256k1::Secp256k1;
-use tokio_tungstenite::connect_async;
-use url::Url;
-
-#[tokio::test]
-async fn test_launcher_starts_and_stops_circle_with_generated_keys() {
- // 1. Setup: Define the test circle configuration directly, without keys
- let test_circle_config = vec![CircleConfig {
- name: "test_circle_generated".to_string(),
- port: 8088, // Use a distinct port for testing
- script_path: None,
- public_key: None,
- secret_key: None,
- }];
-
- // 2. Action: Run the launcher setup with the direct config
- let (running_circles, outputs) = setup_and_spawn_circles(test_circle_config)
- .await
- .expect("Failed to setup and spawn circles");
-
- // 3. Verification: Check if the circle was spawned
- assert_eq!(running_circles.len(), 1, "Expected one running circle");
- assert_eq!(outputs.len(), 1, "Expected one circle output");
-
- let circle_output = &outputs[0];
- assert_eq!(circle_output.name, "test_circle_generated");
- assert!(!circle_output.public_key.is_empty()); // Key should be generated
- assert!(!circle_output.secret_key.is_empty());
-
- // 4. Verification: Check if the WebSocket server is connectable
- let ws_url = Url::parse(&circle_output.ws_url).expect("Failed to parse WS URL");
- let connection_attempt = connect_async(ws_url.to_string()).await;
- assert!(
- connection_attempt.is_ok(),
- "Failed to connect to WebSocket server"
- );
-
- if let Ok((ws_stream, _)) = connection_attempt {
- let (mut write, _read) = ws_stream.split();
- write
- .send(tokio_tungstenite::tungstenite::Message::Ping(vec![]))
- .await
- .expect("Failed to send ping");
- }
-
- // 5. Cleanup: Shutdown the circles
- shutdown_circles(running_circles).await;
-}
-
-#[tokio::test]
-async fn test_launcher_uses_provided_keypair() {
- // 1. Setup: Generate a keypair to provide to the config
- let secp = Secp256k1::new();
- let (secret_key, public_key) = secp.generate_keypair(&mut secp256k1::rand::thread_rng());
- let secret_key_str = secret_key.display_secret().to_string();
- let public_key_str = public_key.to_string();
-
- // 2. Setup: Define the test circle configuration with the generated keypair
- let test_circle_config = vec![CircleConfig {
- name: "test_circle_with_keys".to_string(),
- port: 8089, // Use another distinct port
- script_path: None,
- public_key: Some(public_key_str.clone()),
- secret_key: Some(secret_key_str.clone()),
- }];
-
- // 3. Action: Run the launcher setup
- let (running_circles, outputs) = setup_and_spawn_circles(test_circle_config)
- .await
- .expect("Failed to setup and spawn circles with provided keys");
-
- // 4. Verification: Check if the output public key matches the provided one
- assert_eq!(outputs.len(), 1, "Expected one circle output");
- let circle_output = &outputs[0];
- assert_eq!(circle_output.name, "test_circle_with_keys");
- assert_eq!(
- circle_output.public_key, public_key_str,
- "The public key in the output should match the one provided"
- );
- assert_eq!(
- circle_output.secret_key, secret_key_str,
- "The secret key in the output should match the one provided"
- );
-
- // 5. Cleanup
- shutdown_circles(running_circles).await;
-}
-
-#[tokio::test]
-async fn test_launcher_fails_with_mismatched_keypair() {
- // 1. Setup: Generate two different keypairs
- let secp = Secp256k1::new();
- let (secret_key1, _public_key1) = secp.generate_keypair(&mut secp256k1::rand::thread_rng());
- let (_secret_key2, public_key2) = secp.generate_keypair(&mut secp256k1::rand::thread_rng());
-
- let secret_key_str = secret_key1.display_secret().to_string();
- let public_key_str = public_key2.to_string(); // Mismatched public key
-
- // 2. Setup: Define config with mismatched keys
- let test_circle_config = vec![CircleConfig {
- name: "test_circle_mismatched_keys".to_string(),
- port: 8090,
- script_path: None,
- public_key: Some(public_key_str),
- secret_key: Some(secret_key_str),
- }];
-
- // 3. Action & Verification: Expect an error
- let result = setup_and_spawn_circles(test_circle_config).await;
- assert!(result.is_err(), "Expected an error due to mismatched keys");
- if let Err(e) = result {
- assert!(
- e.to_string()
- .contains("Provided public key does not match secret key"),
- "Error message did not contain expected text"
- );
- }
-}
diff --git a/src/server/.env.example b/src/server/.env.example
new file mode 100644
index 0000000..9b043b9
--- /dev/null
+++ b/src/server/.env.example
@@ -0,0 +1,10 @@
+# Webhook Configuration
+# Copy this file to .env and set your actual webhook secrets
+
+# Stripe webhook endpoint secret
+# Get this from your Stripe dashboard under Webhooks
+STRIPE_WEBHOOK_SECRET=whsec_your_stripe_webhook_secret_here
+
+# iDenfy webhook endpoint secret
+# Get this from your iDenfy dashboard under Webhooks
+IDENFY_WEBHOOK_SECRET=your_idenfy_webhook_secret_here
\ No newline at end of file
diff --git a/src/server/.gitignore b/src/server/.gitignore
new file mode 100644
index 0000000..cc38b0f
--- /dev/null
+++ b/src/server/.gitignore
@@ -0,0 +1,3 @@
+/target
+file:memdb_test_server*
+*.pem
\ No newline at end of file
diff --git a/src/server_ws/Cargo.lock b/src/server/Cargo.lock
similarity index 100%
rename from src/server_ws/Cargo.lock
rename to src/server/Cargo.lock
diff --git a/src/server_ws/Cargo.toml b/src/server/Cargo.toml
similarity index 60%
rename from src/server_ws/Cargo.toml
rename to src/server/Cargo.toml
index 8f0f8cc..8f8f9e2 100644
--- a/src/server_ws/Cargo.toml
+++ b/src/server/Cargo.toml
@@ -8,9 +8,28 @@ name = "circle_ws_lib"
path = "src/lib.rs"
[[bin]]
-name = "server_ws_binary"
+name = "circles_server"
path = "cmd/main.rs"
+[[example]]
+name = "wss_basic_example"
+path = "../../examples/wss_basic_example.rs"
+
+[[example]]
+name = "wss_auth_example"
+path = "../../examples/wss_auth_example.rs"
+required-features = ["auth"]
+
+[[example]]
+name = "wss_test_client"
+path = "../../examples/wss_test_client.rs"
+
+[[example]]
+name = "wss_server"
+path = "../../examples/wss_demo/wss_server.rs"
+required-features = ["auth"]
+
+
[dependencies]
rustls = "0.23.5"
rustls-pemfile = "2.1.2"
@@ -26,10 +45,18 @@ uuid = { workspace = true }
tokio = { workspace = true }
chrono = { workspace = true }
rhai_client = { path = "../../../rhailib/src/client" } # Corrected relative path
+thiserror = { workspace = true }
+heromodels = { path = "../../../db/heromodels" }
+
+# Webhook dependencies
+hmac = "0.12"
+sha2 = "0.10"
+dotenv = "0.15"
+bytes = "1.0"
+hex = { workspace = true }
# Authentication dependencies (optional)
secp256k1 = { workspace = true, optional = true }
-hex = { workspace = true, optional = true }
sha3 = { workspace = true, optional = true }
rand = { workspace = true, optional = true }
once_cell = { workspace = true }
@@ -38,13 +65,16 @@ clap = { workspace = true }
# Optional features for authentication
[features]
default = []
-auth = ["secp256k1", "hex", "sha3", "rand"]
+auth = ["secp256k1", "sha3", "rand"]
[dev-dependencies]
+redis = { version = "0.23.0", features = ["tokio-comp"] }
+uuid = { version = "1.2.2", features = ["v4"] }
tokio-tungstenite = { version = "0.19.0", features = ["native-tls"] }
futures-util = { workspace = true }
url = { workspace = true }
rhailib_worker = { path = "../../../rhailib/src/worker" }
-engine = { path = "../../../rhailib/src/engine" }
+rhailib_engine = { path = "../../../rhailib/src/engine" }
heromodels = { path = "../../../db/heromodels" }
tokio = { workspace = true, features = ["full"] }
+native-tls = "0.2"
diff --git a/src/server/README.md b/src/server/README.md
new file mode 100644
index 0000000..f34aba7
--- /dev/null
+++ b/src/server/README.md
@@ -0,0 +1,76 @@
+# `server`: The Circles WebSocket Server
+
+The `server` crate provides a secure, high-performance WebSocket server built with `Actix`. It is the core backend component of the `circles` ecosystem, responsible for handling client connections, processing JSON-RPC requests, and executing Rhai scripts in a secure manner.
+
+## Features
+
+- **`Actix` Framework**: Built on `Actix`, a powerful and efficient actor-based web framework.
+- **WebSocket Management**: Uses `actix-web-actors` to manage each client connection in its own isolated actor (`CircleWs`), ensuring robust and concurrent session handling.
+- **JSON-RPC 2.0 API**: Implements a JSON-RPC 2.0 API for all client-server communication. The API is formally defined in the root [openrpc.json](../../openrpc.json) file.
+- **Secure Authentication**: Features a built-in `secp256k1` signature-based authentication system to protect sensitive endpoints.
+- **Stateful Session Management**: The `CircleWs` actor maintains the authentication state for each client, granting or denying access to protected methods like `play`.
+- **Webhook Integration**: Supports HTTP webhook endpoints for external services (Stripe, iDenfy) with signature verification and script execution capabilities.
+
+## Core Components
+
+### `spawn_circle_server`
+
+This is the main entry point function for the server. It configures and starts the `Actix` HTTP server and sets up the WebSocket route with path-based routing (`/{circle_pk}`).
+
+### `CircleWs` Actor
+
+This `Actix` actor is the heart of the server's session management. A new instance of `CircleWs` is created for each client that connects. Its responsibilities include:
+- Handling the WebSocket connection lifecycle.
+- Parsing incoming JSON-RPC messages.
+- Managing the authentication state of the session (i.e., whether the client is authenticated or not).
+- Dispatching requests to the appropriate handlers (`fetch_nonce`, `authenticate`, and `play`).
+
+## Authentication
+
+The server provides a robust authentication mechanism to ensure that only authorized clients can execute scripts. The entire flow is handled over the WebSocket connection using two dedicated JSON-RPC methods:
+
+1. **`fetch_nonce`**: The client requests a unique, single-use nonce (a challenge) from the server.
+2. **`authenticate`**: The client sends back the nonce signed with its private key. The `CircleWs` actor verifies the signature to confirm the client's identity.
+
+For a more detailed breakdown of the authentication architecture, please see the [ARCHITECTURE.md](docs/ARCHITECTURE.md) file.
+
+## Webhook Integration
+
+The server also provides HTTP webhook endpoints for external services alongside the WebSocket functionality:
+
+- **Stripe Webhooks**: `POST /webhooks/stripe/{circle_pk}` - Handles Stripe payment events
+- **iDenfy Webhooks**: `POST /webhooks/idenfy/{circle_pk}` - Handles iDenfy KYC verification events
+
+### Webhook Features
+
+- **Signature Verification**: All webhooks use HMAC signature verification for security
+- **Script Execution**: Webhook events trigger Rhai script execution via the same Redis-based system
+- **Type Safety**: Webhook payload types are defined in the `heromodels` library for reusability
+- **Modular Architecture**: Separate handlers for each webhook provider with common utilities
+
+For detailed webhook architecture and configuration, see [WEBHOOK_ARCHITECTURE.md](WEBHOOK_ARCHITECTURE.md).
+
+## How to Run
+
+### As a Library
+
+The `server` is designed to be used as a library by the `launcher`, which is responsible for spawning a single multi-circle server instance that can handle multiple circles via path-based routing.
+
+To run the server via the launcher with circle public keys:
+```bash
+cargo run --package launcher -- -k -k [options]
+```
+
+The launcher will start a single `server` instance that can handle multiple circles through path-based WebSocket connections at `/{circle_pk}`.
+
+### Standalone Binary
+
+A standalone binary is also available for development and testing purposes. See [`cmd/README.md`](cmd/README.md) for detailed usage instructions.
+
+```bash
+# Basic standalone server
+cargo run
+
+# With authentication and TLS
+cargo run -- --auth --tls --cert cert.pem --key key.pem
+```
diff --git a/src/server/cmd/README.md b/src/server/cmd/README.md
new file mode 100644
index 0000000..51c29b9
--- /dev/null
+++ b/src/server/cmd/README.md
@@ -0,0 +1,142 @@
+# Circles WebSocket Server Binary
+
+A command-line WebSocket server for hosting Circles with authentication and TLS support.
+
+## Binary: Server
+
+### Installation
+
+Build the binary:
+```bash
+cargo build --release
+```
+
+### Usage
+
+```bash
+# Basic usage - starts server on localhost:8443
+cargo run
+
+# Custom host and port
+cargo run -- --host 0.0.0.0 --port 9000
+
+# Enable authentication
+cargo run -- --auth
+
+# Enable TLS/WSS with certificates
+cargo run -- --tls --cert /path/to/cert.pem --key /path/to/key.pem
+
+# Use separate TLS port
+cargo run -- --tls --cert cert.pem --key key.pem --tls-port 8444
+
+# Custom Redis URL
+cargo run -- --redis-url redis://localhost:6379/1
+
+# Increase verbosity
+cargo run -- -v # Debug logging
+cargo run -- -vv # Full debug logging
+cargo run -- -vvv # Trace logging
+```
+
+### Command-Line Options
+
+| Option | Short | Default | Description |
+|--------|-------|---------|-------------|
+| `--host` | `-H` | `127.0.0.1` | Server bind address |
+| `--port` | `-p` | `8443` | Server port |
+| `--redis-url` | | `redis://127.0.0.1/` | Redis connection URL |
+| `--auth` | | `false` | Enable secp256k1 authentication |
+| `--tls` | | `false` | Enable TLS/WSS support |
+| `--cert` | | | Path to TLS certificate file (required with --tls) |
+| `--key` | | | Path to TLS private key file (required with --tls) |
+| `--tls-port` | | | Separate port for TLS connections |
+| `--verbose` | `-v` | | Increase verbosity (stackable) |
+
+### Configuration Examples
+
+#### Development Server
+```bash
+# Simple development server
+cargo run
+
+# Development with authentication
+cargo run -- --auth
+```
+
+#### Production Server
+```bash
+# Production with TLS and authentication
+cargo run -- \
+ --host 0.0.0.0 \
+ --port 8080 \
+ --tls \
+ --tls-port 8443 \
+ --cert /etc/ssl/certs/circles.pem \
+ --key /etc/ssl/private/circles.key \
+ --auth \
+ --redis-url redis://redis-server:6379/0
+```
+
+#### Custom Redis Configuration
+```bash
+# Connect to remote Redis with authentication
+cargo run -- --redis-url redis://username:password@redis.example.com:6379/2
+```
+
+### Logging Levels
+
+The server supports multiple verbosity levels:
+
+- **Default** (`cargo run`): Shows only warnings and circle_ws_lib info
+- **Debug** (`-v`): Shows debug info for circle_ws_lib, info for actix
+- **Full Debug** (`-vv`): Shows debug for all components
+- **Trace** (`-vvv+`): Shows trace-level logging for everything
+
+### TLS/SSL Configuration
+
+When using `--tls`, you must provide both certificate and key files:
+
+```bash
+# Generate self-signed certificate for testing
+openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
+
+# Run server with TLS
+cargo run -- --tls --cert cert.pem --key key.pem
+```
+
+### Authentication
+
+When `--auth` is enabled, clients must complete secp256k1 authentication:
+1. Client connects to WebSocket
+2. Server sends authentication challenge
+3. Client signs challenge with private key
+4. Server verifies signature and grants access
+
+### Redis Integration
+
+The server uses Redis for:
+- Session management
+- Message persistence
+- Cross-instance communication (in clustered deployments)
+
+Supported Redis URL formats:
+- `redis://localhost/` - Local Redis, default database
+- `redis://localhost:6379/1` - Local Redis, database 1
+- `redis://user:pass@host:port/db` - Authenticated Redis
+- `rediss://host:port/` - Redis with TLS
+
+### Error Handling
+
+The server provides clear error messages for common configuration issues:
+- Missing TLS certificate or key files
+- Invalid Redis connection URLs
+- Port binding failures
+- Authentication setup problems
+
+### Dependencies
+
+- `actix-web`: Web server framework
+- `tokio-tungstenite`: WebSocket implementation
+- `redis`: Redis client
+- `rustls`: TLS implementation
+- `clap`: Command-line argument parsing
\ No newline at end of file
diff --git a/src/server/cmd/main.rs b/src/server/cmd/main.rs
new file mode 100644
index 0000000..f2b246c
--- /dev/null
+++ b/src/server/cmd/main.rs
@@ -0,0 +1,132 @@
+use circle_ws_lib::{spawn_circle_server, ServerConfig};
+use clap::Parser;
+use dotenv::dotenv;
+
+#[derive(Parser, Debug)]
+#[clap(author, version, about, long_about = None)]
+struct Args {
+ #[clap(short = 'H', long, value_parser, default_value = "127.0.0.1")]
+ host: String,
+
+ #[clap(short, long, value_parser, default_value_t = 8443)]
+ port: u16,
+
+ #[clap(long, value_parser, default_value = "redis://127.0.0.1/")]
+ redis_url: String,
+
+ #[clap(long, help = "Enable authentication")]
+ auth: bool,
+
+ #[clap(long, help = "Enable TLS/WSS")]
+ tls: bool,
+
+ #[clap(long, value_parser, help = "Path to TLS certificate file")]
+ cert: Option,
+
+ #[clap(long, value_parser, help = "Path to TLS private key file")]
+ key: Option,
+
+ #[clap(long, value_parser, help = "Separate port for TLS connections")]
+ tls_port: Option,
+
+ #[clap(short, long, action = clap::ArgAction::Count, help = "Increase verbosity (-v for debug, -vv for trace)")]
+ verbose: u8,
+
+ #[clap(long, help = "Remove timestamps from log output")]
+ no_timestamp: bool,
+
+ #[clap(long, help = "Enable webhook handling")]
+ webhooks: bool,
+}
+
+#[actix_web::main]
+async fn main() -> std::io::Result<()> {
+ let args = Args::parse();
+
+ // Load environment variables from .env file
+ if let Err(e) = dotenv() {
+ // Only warn if webhooks are enabled, as .env is only required for webhooks
+ if args.webhooks {
+ eprintln!("Warning: Could not load .env file: {}", e);
+ eprintln!("This is required when webhooks are enabled for webhook secrets.");
+ }
+ }
+
+ // Configure logging based on verbosity level
+ let log_config = match args.verbose {
+ 0 => {
+ // Default: suppress actix server logs, show only circle_ws_lib info and above
+ "warn,circle_ws_lib=info"
+ }
+ 1 => {
+ // -v: show debug for circle_ws_lib, info for actix
+ "info,circle_ws_lib=debug,actix_server=info"
+ }
+ 2 => {
+ // -vv: show debug for everything
+ "debug"
+ }
+ _ => {
+ // -vvv and above: show trace for everything
+ "trace"
+ }
+ };
+
+ std::env::set_var("RUST_LOG", log_config);
+
+ // Configure env_logger with or without timestamps
+ if args.no_timestamp {
+ env_logger::Builder::from_default_env()
+ .format_timestamp(None)
+ .init();
+ } else {
+ env_logger::init();
+ }
+
+ // Validate TLS configuration
+ if args.tls && (args.cert.is_none() || args.key.is_none()) {
+ eprintln!("Error: TLS is enabled but certificate or key path is missing");
+ eprintln!("Use --cert and --key to specify certificate and key files");
+ std::process::exit(1);
+ }
+
+ let config = ServerConfig {
+ host: args.host,
+ port: args.port,
+ redis_url: args.redis_url,
+ enable_auth: args.auth,
+ enable_tls: args.tls,
+ cert_path: args.cert,
+ key_path: args.key,
+ tls_port: args.tls_port,
+ enable_webhooks: args.webhooks,
+ };
+
+ println!("🚀 Starting Circles WebSocket Server");
+ println!("📋 Configuration:");
+ println!(" Host: {}", config.host);
+ println!(" Port: {}", config.port);
+ if let Some(tls_port) = config.tls_port {
+ println!(" TLS Port: {}", tls_port);
+ }
+ println!(" Authentication: {}", if config.enable_auth { "ENABLED" } else { "DISABLED" });
+ println!(" TLS/WSS: {}", if config.enable_tls { "ENABLED" } else { "DISABLED" });
+ println!(" Webhooks: {}", if config.enable_webhooks { "ENABLED" } else { "DISABLED" });
+
+ if config.enable_tls {
+ if let (Some(cert), Some(key)) = (&config.cert_path, &config.key_path) {
+ println!(" Certificate: {}", cert);
+ println!(" Private Key: {}", key);
+ }
+ }
+
+ if config.enable_webhooks {
+ println!(" Webhook secrets loaded from environment variables:");
+ println!(" - STRIPE_WEBHOOK_SECRET");
+ println!(" - IDENFY_WEBHOOK_SECRET");
+ }
+ println!();
+
+ let (server_task, _server_handle) = spawn_circle_server(config)?;
+ server_task.await?
+}
diff --git a/src/server/docs/ARCHITECTURE.md b/src/server/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..aafa330
--- /dev/null
+++ b/src/server/docs/ARCHITECTURE.md
@@ -0,0 +1,133 @@
+# `server` Architecture
+
+This document provides a detailed look into the internal architecture of the `server` crate, focusing on its `Actix`-based design, the structure of the authentication service, and the request lifecycle.
+
+## 1. Core Design: The `Actix` Actor System
+
+The `server` is built around the `Actix` actor framework, which allows for highly concurrent and stateful handling of network requests. The key components of this design are:
+
+- **`HttpServer`**: The main `Actix` server instance that listens for incoming TCP connections.
+- **`App`**: The application factory that defines the routes for the server.
+- **`CircleWs` Actor**: A dedicated actor that is spawned for each individual WebSocket connection. This is the cornerstone of the server's design, as it allows each client session to be managed in an isolated, stateful manner.
+
+When a client connects to the `/{circle_pk}` endpoint, the `HttpServer` upgrades the connection to a WebSocket and spawns a new `CircleWs` actor to handle it. The circle public key is extracted from the URL path to identify which circle the client wants to connect to. All further communication with that client, including the entire authentication flow, is then processed by this specific actor instance.
+
+## 2. Module Structure
+
+The `server` crate is organized into the following key modules:
+
+- **`lib.rs`**: The main library file that contains the `spawn_circle_server` function, which sets up and runs the `Actix` server. It also defines the `CircleWs` actor and its message handling logic for all JSON-RPC methods.
+- **`auth/`**: This module encapsulates all the logic related to the `secp256k1` authentication system.
+ - **`signature_verifier.rs`**: A self-contained utility module that provides the `verify_signature` function. This function performs the core cryptographic verification of the client's signed nonce.
+ - **`types.rs`**: Defines the data structures used within the authentication service.
+- **`webhook/`**: This module provides HTTP webhook handling capabilities for external services.
+ - **`mod.rs`**: Main webhook module with route configuration and exports.
+ - **`handlers/`**: Contains individual webhook handlers for different providers (Stripe, iDenfy).
+ - **`verifiers.rs`**: Signature verification utilities for webhook authenticity.
+ - **`types.rs`**: Local webhook types (configuration, errors, verification results).
+
+## 3. Request Lifecycle and Authentication Flow
+
+The diagram below illustrates the flow of a typical client interaction. The entire process, from fetching a nonce to executing a protected command, occurs over the WebSocket connection and is handled by the `CircleWs` actor.
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant ActixHttpServer as HttpServer
+ participant CircleWsActor as CircleWs Actor
+ participant SignatureVerifier as auth::signature_verifier
+
+ Client->>+ActixHttpServer: Establishes WebSocket connection
+ ActixHttpServer->>ActixHttpServer: Spawns a new CircleWsActor
+ ActixHttpServer-->>-Client: WebSocket connection established
+
+ Note over CircleWsActor: Session created, authenticated = false
+
+ Client->>+CircleWsActor: Sends "fetch_nonce" JSON-RPC message
+ CircleWsActor->>CircleWsActor: Generate and store nonce for pubkey
+ CircleWsActor-->>-Client: Returns nonce in JSON-RPC response
+
+ Client->>Client: Signs nonce with private key
+
+ Client->>+CircleWsActor: Sends "authenticate" JSON-RPC message
+ CircleWsActor->>+SignatureVerifier: verify_signature(pubkey, nonce, signature)
+ SignatureVerifier-->>-CircleWsActor: Returns verification result
+
+ alt Signature is Valid
+ CircleWsActor->>CircleWsActor: Set session state: authenticated = true
+ CircleWsActor-->>-Client: Returns success response
+ else Signature is Invalid
+ CircleWsActor-->>-Client: Returns error response
+ end
+
+ Note over CircleWsActor: Client is now authenticated
+
+ Client->>+CircleWsActor: Sends "play" JSON-RPC message
+ CircleWsActor->>CircleWsActor: Check if authenticated
+ alt Is Authenticated
+ CircleWsActor->>CircleWsActor: Get public key from authenticated connections map
+ CircleWsActor->>CircleWsActor: Execute Rhai script with public key
+ CircleWsActor-->>-Client: Returns script result
+ else Is Not Authenticated
+ CircleWsActor-->>-Client: Returns "Authentication Required" error
+ end
+```
+
+This architecture ensures a clear separation of concerns and a unified communication protocol:
+- The `HttpServer` handles connection management.
+- The `CircleWs` actor manages the entire session lifecycle, including state and all API logic.
+- The `auth` module provides a self-contained, reusable signature verification utility.
+
+## 4. Webhook Integration Architecture
+
+In addition to WebSocket connections, the server supports HTTP webhook endpoints for external services. This integration runs alongside the WebSocket functionality without interference.
+
+### Webhook Request Flow
+
+```mermaid
+sequenceDiagram
+ participant WS as Webhook Service
+ participant HS as HttpServer
+ participant WH as Webhook Handler
+ participant WV as Webhook Verifier
+ participant RC as RhaiClient
+ participant Redis as Redis
+
+ WS->>+HS: POST /webhooks/{provider}/{circle_pk}
+ HS->>+WH: Route to appropriate handler
+ WH->>WH: Extract circle_pk and signature
+ WH->>+WV: Verify webhook signature
+ WV->>WV: HMAC verification with provider secret
+ WV-->>-WH: Verification result + caller_id
+
+ alt Signature Valid
+ WH->>WH: Parse webhook payload (heromodels types)
+ WH->>+RC: Create RhaiClient with caller_id
+ RC->>+Redis: Execute webhook script
+ Redis-->>-RC: Script result
+ RC-->>-WH: Execution result
+ WH-->>-HS: HTTP 200 OK
+ else Signature Invalid
+ WH-->>-HS: HTTP 401 Unauthorized
+ end
+ HS-->>-WS: HTTP Response
+```
+
+### Key Webhook Components
+
+- **Modular Handlers**: Separate handlers for each webhook provider (Stripe, iDenfy)
+- **Signature Verification**: HMAC-based verification using provider-specific secrets
+- **Type Safety**: Webhook payload types defined in `heromodels` library for reusability
+- **Script Integration**: Uses the same Redis-based Rhai execution system as WebSocket connections
+- **Isolated Processing**: Webhook processing doesn't affect WebSocket connections
+
+### Webhook vs WebSocket Comparison
+
+| Aspect | WebSocket | Webhook |
+|--------|-----------|---------|
+| **Connection Type** | Persistent, bidirectional | HTTP request/response |
+| **Authentication** | secp256k1 signature-based | HMAC signature verification |
+| **State Management** | Stateful sessions via CircleWs actor | Stateless HTTP requests |
+| **Script Execution** | Direct via authenticated session | Via RhaiClient with provider caller_id |
+| **Use Case** | Interactive client applications | External service notifications |
+| **Data Types** | JSON-RPC messages | Provider-specific webhook payloads (heromodels) |
\ No newline at end of file
diff --git a/src/server_ws/README_AUTH.md b/src/server/docs/authentication.md
similarity index 85%
rename from src/server_ws/README_AUTH.md
rename to src/server/docs/authentication.md
index 5977856..f379517 100644
--- a/src/server_ws/README_AUTH.md
+++ b/src/server/docs/authentication.md
@@ -66,7 +66,7 @@ Clients can authenticate by including these query parameters in the WebSocket UR
**Example:**
```
-ws://localhost:8080/ws?pubkey=04abc123...&sig=def456...&nonce=nonce_123_abc
+ws://localhost:8080/{circle_pk}?pubkey=04abc123...&sig=def456...&nonce=nonce_123_abc
```
### Authentication Flow
@@ -79,29 +79,28 @@ ws://localhost:8080/ws?pubkey=04abc123...&sig=def456...&nonce=nonce_123_abc
### Basic Server (No Authentication)
```rust
-use circle_ws_lib::spawn_circle_ws_server;
+use circle_ws_lib::{ServerConfig, spawn_circle_server};
-let handle = spawn_circle_ws_server(
- 1, // circle_id
- "test_circle".to_string(),
- 8080, // port
+let config = ServerConfig::new(
+ "localhost".to_string(),
+ 8080,
"redis://localhost".to_string(),
- server_handle_tx,
);
+
+let (server_task, server_handle) = spawn_circle_server(config)?;
```
### Server with Authentication
```rust
-use circle_ws_lib::spawn_circle_ws_server_with_auth;
+use circle_ws_lib::{ServerConfig, spawn_circle_server};
-let handle = spawn_circle_ws_server_with_auth(
- 1, // circle_id
- "test_circle".to_string(),
- 8080, // port
+let config = ServerConfig::new(
+ "localhost".to_string(),
+ 8080,
"redis://localhost".to_string(),
- server_handle_tx,
- true, // enable_auth
-);
+).with_auth();
+
+let (server_task, server_handle) = spawn_circle_server(config)?;
```
## Client Integration
@@ -116,9 +115,9 @@ const { nonce } = await nonceResponse.json();
const signature = signMessage(privateKey, nonce);
const publicKey = derivePublicKey(privateKey);
-// 3. Connect with authentication
+// 3. Connect with authentication (replace {circle_pk} with actual circle public key)
const ws = new WebSocket(
- `ws://localhost:8080/ws?pubkey=${publicKey}&sig=${signature}&nonce=${nonce}`
+ `ws://localhost:8080/${circle_pk}?pubkey=${publicKey}&sig=${signature}&nonce=${nonce}`
);
```
@@ -127,16 +126,16 @@ const ws = new WebSocket(
use circle_ws_lib::auth::*;
// Request nonce. NonceClient will derive the HTTP API path from this WebSocket URL.
-let nonce_client = NonceClient::from_ws_url("ws://localhost:8080/ws")?;
+let nonce_client = NonceClient::from_ws_url("ws://localhost:8080/{circle_pk}")?;
let nonce_response = nonce_client.request_nonce(Some(public_key)).await?;
// Sign nonce
let signature = sign_message(&private_key, &nonce_response.nonce)?;
-// Connect with authentication
+// Connect with authentication (replace {circle_pk} with actual circle public key)
let ws_url = format!(
- "ws://localhost:8080/ws?pubkey={}&sig={}",
- public_key, signature
+ "ws://localhost:8080/{}?pubkey={}&sig={}",
+ circle_pk, public_key, signature
);
```
@@ -178,7 +177,7 @@ let ws_url = format!(
### Logging
```
-INFO WebSocket session started for server dedicated to: test_circle (authenticated: 04abc123...)
+INFO Incoming WebSocket connection for circle: 04abc123... (auth_enabled: true)
INFO Authentication successful for pubkey: 04abc123...
WARN Authentication failed: invalid signature
```
diff --git a/src/server/docs/webhooks.md b/src/server/docs/webhooks.md
new file mode 100644
index 0000000..956360c
--- /dev/null
+++ b/src/server/docs/webhooks.md
@@ -0,0 +1,357 @@
+# Webhook Integration Architecture
+
+## Overview
+
+This document outlines the architecture for adding webhook handling capabilities to the Circle WebSocket Server. The integration adds HTTP webhook endpoints alongside the existing WebSocket functionality without disrupting the current system.
+
+## Architecture Diagram
+
+```mermaid
+graph TB
+ subgraph "External Services"
+ A[Stripe Webhooks]
+ B[iDenfy Webhooks]
+ end
+
+ subgraph "Circle Server"
+ C[HTTP Router]
+ D[WebSocket Handler]
+ E[Webhook Handler]
+ F[Stripe Verifier]
+ G[iDenfy Verifier]
+ H[Script Dispatcher]
+ I[RhaiClientBuilder]
+ end
+
+ subgraph "Configuration"
+ J[.env File]
+ K[Environment Variables]
+ end
+
+ subgraph "Backend"
+ L[Redis]
+ M[Rhai Worker]
+ end
+
+ A --> |POST /webhooks/stripe/{circle_pk}| E
+ B --> |POST /webhooks/idenfy/{circle_pk}| E
+
+ C --> D
+ C --> E
+
+ E --> F
+ E --> G
+ F --> H
+ G --> H
+ H --> I
+ I --> L
+ L --> M
+
+ J --> K
+ K --> F
+ K --> G
+
+ D --> I
+```
+
+## URL Structure
+
+### Webhook Endpoints
+- **Stripe**: `POST /webhooks/stripe/{circle_pk}`
+- **iDenfy**: `POST /webhooks/idenfy/{circle_pk}`
+
+### Existing WebSocket Endpoints (Unchanged)
+- **WebSocket**: `GET /{circle_pk}` (upgrades to WebSocket)
+
+## Configuration
+
+### Environment Variables (.env file)
+```bash
+# Webhook secrets for signature verification
+STRIPE_WEBHOOK_SECRET=whsec_...
+IDENFY_WEBHOOK_SECRET=your_idenfy_secret
+
+# Existing configuration
+REDIS_URL=redis://127.0.0.1/
+```
+
+### Server Configuration Updates
+```rust
+pub struct ServerConfig {
+ // ... existing fields
+ pub stripe_webhook_secret: Option,
+ pub idenfy_webhook_secret: Option,
+}
+```
+
+## Webhook Processing Flow
+
+### 1. Request Reception
+```mermaid
+sequenceDiagram
+ participant WS as Webhook Service
+ participant CS as Circle Server
+ participant WV as Webhook Verifier
+ participant SD as Script Dispatcher
+ participant RC as RhaiClient
+ participant RW as Rhai Worker
+
+ WS->>CS: POST /webhooks/stripe/{circle_pk}
+ CS->>CS: Extract circle_pk from URL
+ CS->>CS: Read request body and headers
+ CS->>WV: Verify webhook signature
+
+ alt Stripe Webhook
+ WV->>WV: Verify Stripe signature using STRIPE_WEBHOOK_SECRET
+ WV->>WV: Deserialize to Stripe webhook payload
+ else iDenfy Webhook
+ WV->>WV: Verify iDenfy signature using IDENFY_WEBHOOK_SECRET
+ WV->>WV: Deserialize to iDenfy webhook payload
+ end
+
+ WV->>CS: Return verification result + parsed payload
+
+ alt Verification Success
+ CS->>SD: Dispatch appropriate script
+ SD->>RC: Create RhaiClientBuilder
+ RC->>RC: Set caller_id="stripe" or "idenfy"
+ RC->>RC: Set recipient_id=circle_pk
+ RC->>RC: Set script="stripe_webhook_received" or "idenfy_webhook_received"
+ RC->>RW: Execute via Redis
+ RW->>RC: Return result
+ RC->>CS: Script execution result
+ CS->>WS: HTTP 200 OK
+ else Verification Failed
+ CS->>WS: HTTP 401 Unauthorized
+ end
+```
+
+### 2. Signature Verification
+
+#### Stripe Verification
+- Uses `Stripe-Signature` header
+- HMAC-SHA256 verification with `STRIPE_WEBHOOK_SECRET`
+- Follows Stripe's webhook signature verification protocol
+
+#### iDenfy Verification
+- Uses appropriate iDenfy signature header
+- HMAC verification with `IDENFY_WEBHOOK_SECRET`
+- Follows iDenfy's webhook signature verification protocol
+
+### 3. Payload Deserialization
+
+#### Type Definitions in Heromodels Library
+
+Webhook payload types are now defined in the `heromodels` library for better code organization and reusability:
+
+- **Stripe Types**: Located in `heromodels::models::payment::stripe`
+- **iDenfy Types**: Located in `heromodels::models::identity::kyc`
+
+#### Stripe Payload Structure
+```rust
+// From heromodels::models::payment::StripeWebhookEvent
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct StripeWebhookEvent {
+ pub id: String,
+ pub object: String,
+ pub api_version: Option,
+ pub created: i64,
+ pub data: StripeEventData,
+ pub livemode: bool,
+ pub pending_webhooks: i32,
+ pub request: Option,
+ #[serde(rename = "type")]
+ pub event_type: String,
+}
+```
+
+#### iDenfy Payload Structure
+```rust
+// From heromodels::models::identity::IdenfyWebhookEvent
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct IdenfyWebhookEvent {
+ #[serde(rename = "clientId")]
+ pub client_id: String,
+ #[serde(rename = "scanRef")]
+ pub scan_ref: String,
+ pub status: String,
+ pub platform: String,
+ #[serde(rename = "startedAt")]
+ pub started_at: String,
+ #[serde(rename = "finishedAt")]
+ pub finished_at: Option,
+ pub data: Option,
+ // ... additional fields
+}
+```
+
+### 4. Script Execution
+
+#### Script Names
+- **Stripe**: `stripe_webhook_received`
+- **iDenfy**: `idenfy_webhook_received`
+
+#### Script Context
+The Rhai scripts will receive structured data:
+
+```javascript
+// For Stripe webhooks
+let webhook_data = {
+ "caller_id": "stripe",
+ "circle_id": "circle_public_key",
+ "event_type": "payment_intent.succeeded",
+ "event_id": "evt_...",
+ "created": 1234567890,
+ "livemode": false,
+ "data": { /* Stripe event data */ }
+};
+
+// For iDenfy webhooks
+let webhook_data = {
+ "caller_id": "idenfy",
+ "circle_id": "circle_public_key",
+ "final_decision": "APPROVED",
+ "platform": "PC",
+ "status": { /* iDenfy status data */ },
+ "data": { /* iDenfy verification data */ }
+};
+```
+
+## Implementation Structure
+
+### Current File Structure
+```
+src/server/src/
+├── webhook/
+│ ├── mod.rs # Main webhook module with route configuration
+│ ├── handlers/
+│ │ ├── mod.rs # Handler module exports
+│ │ ├── common.rs # Common utilities and app state
+│ │ ├── stripe.rs # Stripe webhook handler
+│ │ └── idenfy.rs # iDenfy webhook handler
+│ ├── verifiers.rs # Signature verification for all providers
+│ └── types.rs # Local webhook types (config, errors, etc.)
+└── .env # Environment configuration
+```
+
+### Heromodels Library Structure
+```
+heromodels/src/models/
+├── payment/
+│ ├── mod.rs # Payment module exports
+│ └── stripe.rs # Stripe webhook event types
+└── identity/
+ ├── mod.rs # Identity module exports
+ └── kyc.rs # iDenfy KYC webhook event types
+```
+
+### Key Architectural Changes
+- **Type Organization**: Webhook payload types moved to `heromodels` library for reusability
+- **Modular Handlers**: Separate handler files for each webhook provider
+- **Simplified Architecture**: Removed unnecessary dispatcher complexity
+- **Direct Script Execution**: Handlers directly use `RhaiClient` for script execution
+
+### Modified Files
+- `src/lib.rs` - Add webhook routes and module imports
+- `Cargo.toml` - Add heromodels dependency and webhook-related dependencies
+- `cmd/main.rs` - Load .env file and configure webhook secrets
+
+### Dependencies
+```toml
+[dependencies]
+# Existing dependencies...
+
+# Heromodels library for shared types
+heromodels = { path = "../../../db/heromodels" }
+
+# For webhook signature verification
+hmac = "0.12"
+sha2 = "0.10"
+hex = { workspace = true }
+
+# For environment variable loading
+dotenv = "0.15"
+
+# For HTTP request handling
+bytes = "1.0"
+thiserror = { workspace = true }
+```
+
+## Security Considerations
+
+### Signature Verification
+- **Mandatory**: All webhook requests must have valid signatures
+- **Timing Attack Protection**: Use constant-time comparison for signatures
+- **Secret Management**: Webhook secrets loaded from environment variables only
+
+### Error Handling
+- **No Information Leakage**: Generic error responses for invalid webhooks
+- **Logging**: Detailed logging for debugging (same as existing WebSocket errors)
+- **Graceful Degradation**: Webhook failures don't affect WebSocket functionality
+
+### Request Validation
+- **Content-Type**: Verify appropriate content types
+- **Payload Size**: No explicit limits initially (as requested)
+- **Rate Limiting**: Consider future implementation
+
+## Backward Compatibility
+
+### WebSocket Functionality
+- **Zero Impact**: Existing WebSocket routes and functionality unchanged
+- **Authentication**: WebSocket authentication system remains independent
+- **Performance**: No performance impact on WebSocket connections
+
+### Configuration
+- **Optional**: Webhook functionality only enabled when secrets are configured
+- **Graceful Fallback**: Server starts normally even without webhook configuration
+
+## Testing Strategy
+
+### Unit Tests
+- Webhook signature verification for both providers
+- Payload deserialization
+- Error handling scenarios
+
+### Integration Tests
+- End-to-end webhook processing
+- Script dispatch verification
+- Configuration loading
+
+### Mock Testing
+- Simulated Stripe webhook calls
+- Simulated iDenfy webhook calls
+- Invalid signature scenarios
+
+## Deployment Considerations
+
+### Environment Setup
+```bash
+# .env file in src/server/
+STRIPE_WEBHOOK_SECRET=whsec_1234567890abcdef...
+IDENFY_WEBHOOK_SECRET=your_idenfy_webhook_secret
+REDIS_URL=redis://127.0.0.1/
+```
+
+### Server Startup
+- Load .env file before server initialization
+- Validate webhook secrets if webhook endpoints are to be enabled
+- Log webhook endpoint availability
+
+### Monitoring
+- Log webhook reception and processing
+- Track script execution success/failure rates
+- Monitor webhook signature verification failures
+
+## Future Enhancements
+
+### Potential Additions
+- Additional webhook providers
+- Webhook retry mechanisms
+- Webhook event filtering
+- Rate limiting implementation
+- Webhook event queuing for high-volume scenarios
+
+### Scalability Considerations
+- Webhook processing can be made asynchronous if needed
+- Multiple server instances can handle webhooks independently
+- Redis-based script execution provides natural load distribution
\ No newline at end of file
diff --git a/src/server_ws/openrpc.json b/src/server/openrpc.json
similarity index 100%
rename from src/server_ws/openrpc.json
rename to src/server/openrpc.json
diff --git a/src/server_ws/src/auth/auth_middleware.rs b/src/server/src/auth/auth_middleware.rs
similarity index 100%
rename from src/server_ws/src/auth/auth_middleware.rs
rename to src/server/src/auth/auth_middleware.rs
diff --git a/src/server_ws/src/auth/mod.rs b/src/server/src/auth/mod.rs
similarity index 100%
rename from src/server_ws/src/auth/mod.rs
rename to src/server/src/auth/mod.rs
diff --git a/src/server_ws/src/auth/signature_verifier.rs b/src/server/src/auth/signature_verifier.rs
similarity index 100%
rename from src/server_ws/src/auth/signature_verifier.rs
rename to src/server/src/auth/signature_verifier.rs
diff --git a/src/server_ws/src/lib.rs b/src/server/src/lib.rs
similarity index 67%
rename from src/server_ws/src/lib.rs
rename to src/server/src/lib.rs
index 172a2f2..5b51f71 100644
--- a/src/server_ws/src/lib.rs
+++ b/src/server/src/lib.rs
@@ -1,9 +1,9 @@
use actix::prelude::*;
use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer};
use actix_web_actors::ws;
-use log::{debug, info}; // Removed unused error, warn
+use log::{debug, info, error}; // Added error for better logging
use once_cell::sync::Lazy;
-use rhai_client::{RhaiClient, RhaiClientError};
+use rhai_client::{RhaiClientBuilder, RhaiClientError};
use rustls::pki_types::PrivateKeyDer;
use rustls::ServerConfig as RustlsServerConfig;
use rustls_pemfile::{certs, pkcs8_private_keys};
@@ -15,6 +15,7 @@ use std::io::BufReader;
use std::sync::Mutex; // Removed unused Arc
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::task::JoinHandle;
+use thiserror::Error;
// Global store for server handles
// Global store for server handles, initialized with once_cell::sync::Lazy
@@ -27,13 +28,33 @@ static AUTHENTICATED_CONNECTIONS: Lazy, String>>> =
// Remove any lazy_static related code if it exists elsewhere, this is the correct static definition.
mod auth;
+mod webhook;
use crate::auth::signature_verifier::{generate_nonce, NonceResponse};
+use crate::webhook::{WebhookConfig, create_webhook_app_state, configure_webhook_routes};
// Re-export server handle type for external use
pub type ServerHandle = actix_web::dev::ServerHandle;
const TASK_TIMEOUT_DURATION: std::time::Duration = std::time::Duration::from_secs(10);
+#[derive(Error, Debug)]
+pub enum TlsConfigError {
+ #[error("Certificate file not found: {0}")]
+ CertificateNotFound(String),
+ #[error("Private key file not found: {0}")]
+ PrivateKeyNotFound(String),
+ #[error("Invalid certificate format: {0}")]
+ InvalidCertificate(String),
+ #[error("Invalid private key format: {0}")]
+ InvalidPrivateKey(String),
+ #[error("No private keys found in key file: {0}")]
+ NoPrivateKeys(String),
+ #[error("TLS configuration error: {0}")]
+ ConfigurationError(String),
+ #[error("IO error: {0}")]
+ IoError(#[from] std::io::Error),
+}
+
#[derive(Debug, Serialize, Deserialize)]
struct JsonRpcRequest {
jsonrpc: String,
@@ -254,27 +275,30 @@ impl CircleWs {
ctx.text(serde_json::to_string(&err_resp).unwrap());
return;
}
-
+
match serde_json::from_value::(params) {
Ok(play_params) => {
+ info!("Received play request from: {}", self.authenticated_pubkey.clone().unwrap_or_else(|| "anonymous".to_string()));
let script_content = play_params.script;
let circle_pk_clone = self.server_circle_public_key.clone();
let redis_url_clone = self.redis_url_for_client.clone();
- let rpc_id_clone = client_rpc_id.clone();
+ let _rpc_id_clone = client_rpc_id.clone();
let public_key = self.authenticated_pubkey.clone();
let fut = async move {
- match RhaiClient::new(&redis_url_clone) {
+ let caller_id = public_key.unwrap_or_else(|| "anonymous".to_string());
+ match RhaiClientBuilder::new()
+ .redis_url(&redis_url_clone)
+ .caller_id(&caller_id)
+ .build() {
Ok(rhai_client) => {
rhai_client
- .submit_script_and_await_result(
- &circle_pk_clone,
- rpc_id_clone.to_string(),
- script_content,
- TASK_TIMEOUT_DURATION,
- public_key,
- )
- .await
+ .new_play_request()
+ .recipient_id(&circle_pk_clone)
+ .script(&script_content)
+ .timeout(TASK_TIMEOUT_DURATION)
+ .await_response()
+ .await
}
Err(e) => Err(e),
}
@@ -463,54 +487,145 @@ impl StreamHandler> for CircleWs {
#[derive(Clone)]
pub struct ServerConfig {
- pub circle_name: String,
- pub circle_public_key: String,
pub host: String,
pub port: u16,
pub redis_url: String,
pub enable_auth: bool,
+ pub enable_tls: bool,
pub cert_path: Option,
pub key_path: Option,
+ pub tls_port: Option,
+ pub enable_webhooks: bool,
+}
+
+impl ServerConfig {
+ /// Create a new server configuration with TLS disabled
+ pub fn new(
+ host: String,
+ port: u16,
+ redis_url: String,
+ ) -> Self {
+ Self {
+ host,
+ port,
+ redis_url,
+ enable_auth: false,
+ enable_tls: false,
+ cert_path: None,
+ key_path: None,
+ tls_port: None,
+ enable_webhooks: false,
+ }
+ }
+
+ /// Enable TLS with certificate and key paths
+ pub fn with_tls(mut self, cert_path: String, key_path: String) -> Self {
+ self.enable_tls = true;
+ self.cert_path = Some(cert_path);
+ self.key_path = Some(key_path);
+ self
+ }
+
+ /// Set a separate port for TLS connections
+ pub fn with_tls_port(mut self, tls_port: u16) -> Self {
+ self.tls_port = Some(tls_port);
+ self
+ }
+
+ /// Enable authentication
+ pub fn with_auth(mut self) -> Self {
+ self.enable_auth = true;
+ self
+ }
+
+ /// Enable webhooks
+ pub fn with_webhooks(mut self) -> Self {
+ self.enable_webhooks = true;
+ self
+ }
+
+ /// Get the effective port for TLS connections
+ pub fn get_tls_port(&self) -> u16 {
+ self.tls_port.unwrap_or(self.port)
+ }
+
+ /// Check if TLS is properly configured
+ pub fn is_tls_configured(&self) -> bool {
+ self.enable_tls && self.cert_path.is_some() && self.key_path.is_some()
+ }
}
fn load_rustls_config(
cert_path: &str,
key_path: &str,
-) -> Result {
+) -> Result {
+ info!("Loading TLS configuration from cert: {}, key: {}", cert_path, key_path);
+
+ // Validate file existence
+ if !std::path::Path::new(cert_path).exists() {
+ return Err(TlsConfigError::CertificateNotFound(cert_path.to_string()));
+ }
+
+ if !std::path::Path::new(key_path).exists() {
+ return Err(TlsConfigError::PrivateKeyNotFound(key_path.to_string()));
+ }
+
let config = RustlsServerConfig::builder().with_no_client_auth();
- let cert_file = &mut BufReader::new(File::open(cert_path)?);
- let key_file = &mut BufReader::new(File::open(key_path)?);
+ // Load certificate file
+ let cert_file = &mut BufReader::new(File::open(cert_path)
+ .map_err(|e| TlsConfigError::ConfigurationError(format!("Failed to open certificate file: {}", e)))?);
+
+ // Load key file
+ let key_file = &mut BufReader::new(File::open(key_path)
+ .map_err(|e| TlsConfigError::ConfigurationError(format!("Failed to open key file: {}", e)))?);
- let cert_chain = certs(cert_file).map(|r| r.unwrap()).collect();
+ // Parse certificates
+ let cert_chain: Vec<_> = certs(cert_file)
+ .collect::, _>>()
+ .map_err(|e| TlsConfigError::InvalidCertificate(format!("Failed to parse certificates: {}", e)))?;
+
+ if cert_chain.is_empty() {
+ return Err(TlsConfigError::InvalidCertificate("No certificates found in certificate file".to_string()));
+ }
+
+ info!("Loaded {} certificate(s)", cert_chain.len());
+
+ // Parse private keys
let mut keys: Vec = pkcs8_private_keys(key_file)
- .map(|r| r.unwrap().into())
+ .collect::, _>>()
+ .map_err(|e| TlsConfigError::InvalidPrivateKey(format!("Failed to parse private key: {}", e)))?
+ .into_iter()
+ .map(|k| k.into())
.collect();
if keys.is_empty() {
- return Err(std::io::Error::new(
- std::io::ErrorKind::InvalidInput,
- "Could not locate PKCS 8 private keys.",
- ));
+ return Err(TlsConfigError::NoPrivateKeys(key_path.to_string()));
}
+
+ info!("Loaded {} private key(s)", keys.len());
- Ok(config.with_single_cert(cert_chain, keys.remove(0)).unwrap())
+ // Create TLS configuration
+ config.with_single_cert(cert_chain, keys.remove(0))
+ .map_err(|e| TlsConfigError::ConfigurationError(format!("Failed to create TLS configuration: {}", e)))
}
async fn ws_handler(
req: HttpRequest,
stream: web::Payload,
+ path: web::Path,
server_config: web::Data,
) -> Result {
- let circle_name = server_config.circle_name.clone();
+ let circle_pk = path.into_inner();
+
info!(
"Incoming WebSocket connection for circle: {} (auth_enabled: {})",
- circle_name, server_config.enable_auth
+ circle_pk, server_config.enable_auth
);
let ws_actor = CircleWs::new_configured(
- circle_name,
- server_config.circle_public_key.clone(),
+ format!("circle-{}", &circle_pk[..8]), // Use first 8 chars as display name
+ circle_pk,
server_config.redis_url.clone(),
server_config.enable_auth,
);
@@ -520,34 +635,122 @@ async fn ws_handler(
pub fn spawn_circle_server(
config: ServerConfig,
) -> std::io::Result<(JoinHandle>, ServerHandle)> {
- let server_name = config.circle_name.clone();
let host = config.host.clone();
let port = config.port;
+ // Validate TLS configuration if enabled
+ if config.enable_tls && !config.is_tls_configured() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ "TLS is enabled but certificate or key path is missing",
+ ));
+ }
+
let server_config_data = web::Data::new(config.clone());
+ // Setup webhook state if webhooks are enabled
+ let webhook_state = if config.enable_webhooks {
+ match WebhookConfig::from_env() {
+ Ok(webhook_config) => {
+ info!("🪝 Webhooks are ENABLED");
+
+ let webhook_app_state = create_webhook_app_state(
+ webhook_config,
+ config.redis_url.clone(),
+ "webhook_system".to_string()
+ );
+ Some(web::Data::new(webhook_app_state))
+ }
+ Err(e) => {
+ error!("Failed to load webhook configuration: {}", e);
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ format!("Webhook configuration error: {}", e)
+ ));
+ }
+ }
+ } else {
+ None
+ };
+
let http_server = HttpServer::new(move || {
- App::new()
+ let mut app = App::new()
.app_data(server_config_data.clone())
- .route("/ws", web::get().to(ws_handler))
+ .route("/{circle_pk}", web::get().to(ws_handler));
+
+ // Add webhook routes if enabled
+ if let Some(webhook_data) = &webhook_state {
+ app = app
+ .app_data(webhook_data.clone())
+ .configure(configure_webhook_routes);
+ }
+
+ app
});
- let server = if let (Some(cert_path), Some(key_path)) = (config.cert_path, config.key_path) {
- info!("TLS is ENABLED for server '{}'", server_name);
- let tls_config = load_rustls_config(&cert_path, &key_path)?;
- http_server.bind_rustls_0_23((host.as_str(), port), tls_config)?
+ let server = if config.enable_tls && config.is_tls_configured() {
+ let cert_path = config.cert_path.as_ref().unwrap();
+ let key_path = config.key_path.as_ref().unwrap();
+ let tls_port = config.get_tls_port();
+
+ info!("🔒 WSS (WebSocket Secure) is ENABLED for multi-circle server");
+ info!("📜 Certificate: {}", cert_path);
+ info!("🔑 Private key: {}", key_path);
+ info!("🌐 WSS URL pattern: wss://{}:{}/", host, tls_port);
+
+ match load_rustls_config(cert_path, key_path) {
+ Ok(tls_config) => {
+ info!("✅ TLS configuration loaded successfully");
+ http_server.bind_rustls_0_23((host.as_str(), tls_port), tls_config)
+ .map_err(|e| std::io::Error::new(
+ std::io::ErrorKind::AddrInUse,
+ format!("Failed to bind WSS server to {}:{}: {}", host, tls_port, e)
+ ))?
+ }
+ Err(e) => {
+ error!("❌ Failed to load TLS configuration: {}", e);
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ format!("TLS configuration error: {}", e)
+ ));
+ }
+ }
} else {
- info!("TLS is DISABLED for server '{}'", server_name);
- http_server.bind((host.as_str(), port))?
+ info!("🔓 WS (WebSocket) is ENABLED for multi-circle server (no TLS)");
+ info!("🌐 WS URL pattern: ws://{}:{}/", host, port);
+ http_server.bind((host.as_str(), port))
+ .map_err(|e| std::io::Error::new(
+ std::io::ErrorKind::AddrInUse,
+ format!("Failed to bind WS server to {}:{}: {}", host, port, e)
+ ))?
}
.run();
let handle = server.handle();
let server_task = tokio::spawn(server);
+ let protocol = if config.enable_tls { "WSS" } else { "WS" };
+ let effective_port = if config.enable_tls { config.get_tls_port() } else { port };
+
info!(
- "Circle WebSocket server '{}' running on {}:{}",
- server_name, host, port
+ "🚀 Multi-circle {} server running on {}:{}",
+ protocol, host, effective_port
);
+
+ if config.enable_auth {
+ info!("🔐 Authentication is ENABLED");
+ } else {
+ info!("🔓 Authentication is DISABLED");
+ }
+
+ if config.enable_webhooks {
+ info!("🪝 Webhooks are ENABLED");
+ info!("📡 Webhook endpoints:");
+ info!(" • Stripe: {}://{}:{}/webhooks/stripe/{{circle_pk}}", protocol, host, effective_port);
+ info!(" • iDenfy: {}://{}:{}/webhooks/idenfy/{{circle_pk}}", protocol, host, effective_port);
+ } else {
+ info!("🪝 Webhooks are DISABLED");
+ }
+
Ok((server_task, handle))
}
diff --git a/src/server/src/webhook/handlers/common.rs b/src/server/src/webhook/handlers/common.rs
new file mode 100644
index 0000000..58b43fb
--- /dev/null
+++ b/src/server/src/webhook/handlers/common.rs
@@ -0,0 +1,113 @@
+//! Common webhook handler utilities
+
+use crate::webhook::types::{WebhookConfig, WebhookError};
+use actix_web::http::header::HeaderMap;
+
+/// Application state for webhook handling
+#[derive(Clone)]
+pub struct WebhookAppState {
+ pub config: WebhookConfig,
+ pub redis_url: String,
+ pub caller_id: String,
+}
+
+/// Create webhook application state
+pub fn create_webhook_app_state(
+ config: WebhookConfig,
+ redis_url: String,
+ caller_id: String,
+) -> WebhookAppState {
+ WebhookAppState {
+ config,
+ redis_url,
+ caller_id,
+ }
+}
+
+/// Extract signature from request headers with the given header name
+pub fn extract_signature_header(
+ headers: &HeaderMap,
+ header_name: &str,
+) -> Result {
+ match headers.get(header_name) {
+ Some(sig) => match sig.to_str() {
+ Ok(s) => Ok(s.to_string()),
+ Err(_) => Err(WebhookError::InvalidSignature(format!(
+ "Invalid {} header format", header_name
+ ))),
+ },
+ None => Err(WebhookError::InvalidSignature(format!(
+ "Missing {} header", header_name
+ ))),
+ }
+}
+
+/// Create a standard error response for webhook failures
+pub fn create_error_response(error: &str, details: Option) -> serde_json::Value {
+ let mut response = serde_json::json!({
+ "error": error
+ });
+
+ if let Some(details) = details {
+ response["details"] = serde_json::Value::String(details);
+ }
+
+ response
+}
+
+/// Create a standard success response for webhook processing
+pub fn create_success_response(
+ message: &str,
+ additional_fields: Option>,
+) -> serde_json::Value {
+ let mut response = serde_json::json!({
+ "success": true,
+ "message": message
+ });
+
+ if let Some(fields) = additional_fields {
+ for (key, value) in fields {
+ response[key] = value;
+ }
+ }
+
+ response
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use actix_web::http::header::HeaderName;
+
+ #[test]
+ fn test_extract_signature_header_success() {
+ let mut headers = HeaderMap::new();
+ headers.insert(HeaderName::from_static("test-signature"), "test_value".parse().unwrap());
+
+ let result = extract_signature_header(&headers, "test-signature");
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), "test_value");
+ }
+
+ #[test]
+ fn test_extract_signature_header_missing() {
+ let headers = HeaderMap::new();
+
+ let result = extract_signature_header(&headers, "missing-header");
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_create_error_response() {
+ let response = create_error_response("Test error", Some("Test details".to_string()));
+ assert_eq!(response["error"], "Test error");
+ assert_eq!(response["details"], "Test details");
+ }
+
+ #[test]
+ fn test_create_success_response() {
+ let response = create_success_response("Test success", None);
+ assert_eq!(response["success"], true);
+ assert_eq!(response["message"], "Test success");
+ }
+}
\ No newline at end of file
diff --git a/src/server/src/webhook/handlers/idenfy.rs b/src/server/src/webhook/handlers/idenfy.rs
new file mode 100644
index 0000000..532e492
--- /dev/null
+++ b/src/server/src/webhook/handlers/idenfy.rs
@@ -0,0 +1,237 @@
+//! iDenfy webhook handler
+
+use crate::webhook::{
+ types::{WebhookProvider, WebhookError},
+ verifiers::verify_webhook_signature,
+ handlers::common::{WebhookAppState, extract_signature_header, create_error_response, create_success_response},
+};
+use heromodels::models::identity::IdenfyWebhookEvent;
+use actix_web::{web, HttpRequest, HttpResponse, Result as ActixResult};
+use bytes::Bytes;
+use log::{debug, error, info, warn};
+use serde_json;
+use rhai_client::RhaiClientBuilder;
+
+/// Execute an iDenfy webhook script
+async fn execute_idenfy_webhook_script(
+ redis_url: &str,
+ caller_id: &str,
+ circle_id: &str,
+ event: &IdenfyWebhookEvent,
+) -> Result {
+ info!(
+ "Executing idenfy_webhook_received script for circle_id: {}, client_id: {}, status: {}",
+ circle_id, event.client_id, event.status
+ );
+
+ // Create RhaiClient
+ let rhai_client = RhaiClientBuilder::new()
+ .redis_url(redis_url)
+ .caller_id(caller_id)
+ .build()
+ .map_err(|e| WebhookError::ScriptExecutionError(format!("Failed to create RhaiClient: {}", e)))?;
+
+ // Serialize the event as JSON payload
+ let event_json = serde_json::to_string(event)
+ .map_err(|e| WebhookError::PayloadParsingError(format!("Failed to serialize iDenfy event: {}", e)))?;
+
+ // Execute the script with circle_id, caller_id, and event payload
+ let script = format!(
+ "idenfy_webhook_received('{}', '{}', {})",
+ circle_id, caller_id, event_json
+ );
+
+ debug!("Executing script: {}", script);
+
+ match rhai_client
+ .new_play_request()
+ .recipient_id(circle_id)
+ .script(&script)
+ .timeout(std::time::Duration::from_secs(30))
+ .await_response()
+ .await
+ {
+ Ok(task_details) => {
+ if task_details.status == "completed" {
+ info!(
+ "Successfully executed idenfy_webhook_received for circle_id: {}",
+ circle_id
+ );
+
+ let output = task_details.output.unwrap_or_else(|| "null".to_string());
+
+ // Try to parse the result as JSON, fallback to string
+ let result = if let Ok(json_result) = serde_json::from_str::(&output) {
+ json_result
+ } else {
+ serde_json::Value::String(output)
+ };
+
+ Ok(result)
+ } else {
+ let error_message = task_details.error.unwrap_or_else(|| "Script execution failed".to_string());
+ error!(
+ "idenfy_webhook_received execution failed for circle_id: {}: {}",
+ circle_id, error_message
+ );
+
+ Err(WebhookError::ScriptExecutionError(error_message))
+ }
+ }
+ Err(e) => {
+ error!(
+ "Failed to execute idenfy_webhook_received for circle_id: {}: {}",
+ circle_id, e
+ );
+
+ Err(WebhookError::ScriptExecutionError(format!(
+ "Script execution failed: {}", e
+ )))
+ }
+ }
+}
+
+/// Handle iDenfy webhook requests
+pub async fn handle_idenfy_webhook(
+ req: HttpRequest,
+ path: web::Path,
+ body: Bytes,
+ data: web::Data,
+) -> ActixResult {
+ let circle_pk = path.into_inner();
+
+ debug!("Received iDenfy webhook for circle: {}", circle_pk);
+
+ // Extract iDenfy signature from headers
+ let signature = match extract_signature_header(req.headers(), "idenfy-signature") {
+ Ok(sig) => sig,
+ Err(e) => {
+ warn!("iDenfy signature header error: {}", e);
+ return Ok(HttpResponse::BadRequest().json(create_error_response(
+ "Invalid or missing idenfy-signature header",
+ Some(e.to_string()),
+ )));
+ }
+ };
+
+ // Verify webhook signature
+ let verification_result = match verify_webhook_signature(
+ &WebhookProvider::Idenfy,
+ &body,
+ &signature,
+ &data.config,
+ ) {
+ Ok(result) => result,
+ Err(e) => {
+ error!("iDenfy webhook signature verification failed: {}", e);
+ return Ok(HttpResponse::Unauthorized().json(create_error_response(
+ "Signature verification failed",
+ Some(e.to_string()),
+ )));
+ }
+ };
+
+ if !verification_result.is_valid {
+ warn!("iDenfy webhook signature verification failed for circle: {}", circle_pk);
+ return Ok(HttpResponse::Unauthorized().json(create_error_response(
+ "Invalid signature",
+ verification_result.error,
+ )));
+ }
+
+ // Parse iDenfy webhook event
+ let idenfy_event: IdenfyWebhookEvent = match serde_json::from_slice(&body) {
+ Ok(event) => event,
+ Err(e) => {
+ error!("Failed to parse iDenfy webhook payload: {}", e);
+ return Ok(HttpResponse::BadRequest().json(create_error_response(
+ "Invalid JSON payload",
+ Some(e.to_string()),
+ )));
+ }
+ };
+
+ info!(
+ "Processing iDenfy webhook event for client: {} (status: {}) for circle: {}",
+ idenfy_event.client_id, idenfy_event.status, circle_pk
+ );
+
+ // Execute script directly
+ match execute_idenfy_webhook_script(
+ &data.redis_url,
+ &verification_result.caller_id,
+ &circle_pk,
+ &idenfy_event,
+ ).await {
+ Ok(script_result) => {
+ info!(
+ "Successfully processed iDenfy webhook for client {} in circle: {}",
+ idenfy_event.client_id, circle_pk
+ );
+
+ let mut additional_fields = serde_json::Map::new();
+ additional_fields.insert("client_id".to_string(), serde_json::Value::String(idenfy_event.client_id.clone()));
+ additional_fields.insert("scan_ref".to_string(), serde_json::Value::String(idenfy_event.scan_ref.clone()));
+ additional_fields.insert("status".to_string(), serde_json::Value::String(idenfy_event.status.clone()));
+ additional_fields.insert("script_result".to_string(), script_result);
+
+ Ok(HttpResponse::Ok().json(create_success_response(
+ "iDenfy webhook processed successfully",
+ Some(additional_fields),
+ )))
+ }
+ Err(e) => {
+ error!(
+ "Failed to process iDenfy webhook for client {} in circle {}: {}",
+ idenfy_event.client_id, circle_pk, e
+ );
+
+ let mut additional_fields = serde_json::Map::new();
+ additional_fields.insert("client_id".to_string(), serde_json::Value::String(idenfy_event.client_id));
+ additional_fields.insert("scan_ref".to_string(), serde_json::Value::String(idenfy_event.scan_ref));
+
+ Ok(HttpResponse::InternalServerError().json({
+ let mut response = create_error_response("Webhook processing failed", Some(e.to_string()));
+ for (key, value) in additional_fields {
+ response[key] = value;
+ }
+ response
+ }))
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::webhook::handlers::common::create_webhook_app_state;
+ use crate::webhook::types::WebhookConfig;
+ use actix_web::{test, web, App};
+
+ #[actix_web::test]
+ async fn test_missing_idenfy_signature() {
+ let config = WebhookConfig {
+ idenfy_webhook_secret: "test_idenfy_secret".to_string(),
+ stripe_webhook_secret: "test_stripe_secret".to_string(),
+ };
+ let app_state = web::Data::new(create_webhook_app_state(
+ config,
+ "redis://127.0.0.1:6379".to_string(),
+ "test_caller".to_string(),
+ ));
+
+ let app = test::init_service(
+ App::new()
+ .app_data(app_state.clone())
+ .route("/webhooks/idenfy/{circle_pk}", web::post().to(handle_idenfy_webhook))
+ ).await;
+
+ let req = test::TestRequest::post()
+ .uri("/webhooks/idenfy/test_circle")
+ .set_payload("test payload")
+ .to_request();
+
+ let resp = test::call_service(&app, req).await;
+ assert_eq!(resp.status(), 400);
+ }
+}
\ No newline at end of file
diff --git a/src/server/src/webhook/handlers/mod.rs b/src/server/src/webhook/handlers/mod.rs
new file mode 100644
index 0000000..5ab77bf
--- /dev/null
+++ b/src/server/src/webhook/handlers/mod.rs
@@ -0,0 +1,6 @@
+//! Webhook handlers module
+
+pub mod stripe;
+pub mod idenfy;
+pub mod common;
+
diff --git a/src/server/src/webhook/handlers/stripe.rs b/src/server/src/webhook/handlers/stripe.rs
new file mode 100644
index 0000000..64b9a13
--- /dev/null
+++ b/src/server/src/webhook/handlers/stripe.rs
@@ -0,0 +1,235 @@
+//! Stripe webhook handler
+
+use crate::webhook::{
+ types::{WebhookProvider, WebhookError},
+ verifiers::verify_webhook_signature,
+ handlers::common::{WebhookAppState, extract_signature_header, create_error_response, create_success_response},
+};
+use heromodels::models::payment::StripeWebhookEvent;
+use actix_web::{web, HttpRequest, HttpResponse, Result as ActixResult};
+use bytes::Bytes;
+use log::{debug, error, info, warn};
+use serde_json;
+use rhai_client::RhaiClientBuilder;
+
+/// Execute a Stripe webhook script
+async fn execute_stripe_webhook_script(
+ redis_url: &str,
+ caller_id: &str,
+ circle_id: &str,
+ event: &StripeWebhookEvent,
+) -> Result {
+ info!(
+ "Executing stripe_webhook_received script for circle_id: {}, event_type: {}",
+ circle_id, event.event_type
+ );
+
+ // Create RhaiClient
+ let rhai_client = RhaiClientBuilder::new()
+ .redis_url(redis_url)
+ .caller_id(caller_id)
+ .build()
+ .map_err(|e| WebhookError::ScriptExecutionError(format!("Failed to create RhaiClient: {}", e)))?;
+
+ // Serialize the event as JSON payload
+ let event_json = serde_json::to_string(event)
+ .map_err(|e| WebhookError::PayloadParsingError(format!("Failed to serialize Stripe event: {}", e)))?;
+
+ // Execute the script with circle_id, caller_id, and event payload
+ let script = format!(
+ "stripe_webhook_received('{}', '{}', {})",
+ circle_id, caller_id, event_json
+ );
+
+ debug!("Executing script: {}", script);
+
+ match rhai_client
+ .new_play_request()
+ .recipient_id(circle_id)
+ .script(&script)
+ .timeout(std::time::Duration::from_secs(30))
+ .await_response()
+ .await
+ {
+ Ok(task_details) => {
+ if task_details.status == "completed" {
+ info!(
+ "Successfully executed stripe_webhook_received for circle_id: {}",
+ circle_id
+ );
+
+ let output = task_details.output.unwrap_or_else(|| "null".to_string());
+
+ // Try to parse the result as JSON, fallback to string
+ let result = if let Ok(json_result) = serde_json::from_str::(&output) {
+ json_result
+ } else {
+ serde_json::Value::String(output)
+ };
+
+ Ok(result)
+ } else {
+ let error_message = task_details.error.unwrap_or_else(|| "Script execution failed".to_string());
+ error!(
+ "stripe_webhook_received execution failed for circle_id: {}: {}",
+ circle_id, error_message
+ );
+
+ Err(WebhookError::ScriptExecutionError(error_message))
+ }
+ }
+ Err(e) => {
+ error!(
+ "Failed to execute stripe_webhook_received for circle_id: {}: {}",
+ circle_id, e
+ );
+
+ Err(WebhookError::ScriptExecutionError(format!(
+ "Script execution failed: {}", e
+ )))
+ }
+ }
+}
+
+/// Handle Stripe webhook requests
+pub async fn handle_stripe_webhook(
+ req: HttpRequest,
+ path: web::Path,
+ body: Bytes,
+ data: web::Data,
+) -> ActixResult {
+ let circle_pk = path.into_inner();
+
+ debug!("Received Stripe webhook for circle: {}", circle_pk);
+
+ // Extract Stripe signature from headers
+ let signature = match extract_signature_header(req.headers(), "stripe-signature") {
+ Ok(sig) => sig,
+ Err(e) => {
+ warn!("Stripe signature header error: {}", e);
+ return Ok(HttpResponse::BadRequest().json(create_error_response(
+ "Invalid or missing stripe-signature header",
+ Some(e.to_string()),
+ )));
+ }
+ };
+
+ // Verify webhook signature
+ let verification_result = match verify_webhook_signature(
+ &WebhookProvider::Stripe,
+ &body,
+ &signature,
+ &data.config,
+ ) {
+ Ok(result) => result,
+ Err(e) => {
+ error!("Stripe webhook signature verification failed: {}", e);
+ return Ok(HttpResponse::Unauthorized().json(create_error_response(
+ "Signature verification failed",
+ Some(e.to_string()),
+ )));
+ }
+ };
+
+ if !verification_result.is_valid {
+ warn!("Stripe webhook signature verification failed for circle: {}", circle_pk);
+ return Ok(HttpResponse::Unauthorized().json(create_error_response(
+ "Invalid signature",
+ verification_result.error,
+ )));
+ }
+
+ // Parse Stripe webhook event
+ let stripe_event: StripeWebhookEvent = match serde_json::from_slice(&body) {
+ Ok(event) => event,
+ Err(e) => {
+ error!("Failed to parse Stripe webhook payload: {}", e);
+ return Ok(HttpResponse::BadRequest().json(create_error_response(
+ "Invalid JSON payload",
+ Some(e.to_string()),
+ )));
+ }
+ };
+
+ info!(
+ "Processing Stripe webhook event: {} (type: {}) for circle: {}",
+ stripe_event.id, stripe_event.event_type, circle_pk
+ );
+
+ // Execute script directly
+ match execute_stripe_webhook_script(
+ &data.redis_url,
+ &verification_result.caller_id,
+ &circle_pk,
+ &stripe_event,
+ ).await {
+ Ok(script_result) => {
+ info!(
+ "Successfully processed Stripe webhook {} for circle: {}",
+ stripe_event.id, circle_pk
+ );
+
+ let mut additional_fields = serde_json::Map::new();
+ additional_fields.insert("event_id".to_string(), serde_json::Value::String(stripe_event.id.clone()));
+ additional_fields.insert("event_type".to_string(), serde_json::Value::String(stripe_event.event_type.clone()));
+ additional_fields.insert("script_result".to_string(), script_result);
+
+ Ok(HttpResponse::Ok().json(create_success_response(
+ "Stripe webhook processed successfully",
+ Some(additional_fields),
+ )))
+ }
+ Err(e) => {
+ error!(
+ "Failed to process Stripe webhook {} for circle {}: {}",
+ stripe_event.id, circle_pk, e
+ );
+
+ let mut additional_fields = serde_json::Map::new();
+ additional_fields.insert("event_id".to_string(), serde_json::Value::String(stripe_event.id));
+
+ Ok(HttpResponse::InternalServerError().json({
+ let mut response = create_error_response("Webhook processing failed", Some(e.to_string()));
+ for (key, value) in additional_fields {
+ response[key] = value;
+ }
+ response
+ }))
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::webhook::handlers::common::create_webhook_app_state;
+ use crate::webhook::types::WebhookConfig;
+ use actix_web::{test, web, App};
+
+ #[actix_web::test]
+ async fn test_missing_stripe_signature() {
+ let config = WebhookConfig {
+ stripe_webhook_secret: "test_stripe_secret".to_string(),
+ idenfy_webhook_secret: "test_idenfy_secret".to_string(),
+ };
+ let app_state = web::Data::new(create_webhook_app_state(
+ config,
+ "redis://127.0.0.1:6379".to_string(),
+ "test_caller".to_string(),
+ ));
+
+ let app = test::init_service(
+ App::new()
+ .app_data(app_state.clone())
+ .route("/webhooks/stripe/{circle_pk}", web::post().to(handle_stripe_webhook))
+ ).await;
+
+ let req = test::TestRequest::post()
+ .uri("/webhooks/stripe/test_circle")
+ .set_payload("test payload")
+ .to_request();
+
+ let resp = test::call_service(&app, req).await;
+ assert_eq!(resp.status(), 400);
+ }
+}
\ No newline at end of file
diff --git a/src/server/src/webhook/mod.rs b/src/server/src/webhook/mod.rs
new file mode 100644
index 0000000..2714595
--- /dev/null
+++ b/src/server/src/webhook/mod.rs
@@ -0,0 +1,31 @@
+//! Webhook handling module for Circle server
+//!
+//! This module provides webhook handling capabilities for external services
+//! like Stripe and iDenfy. It includes signature verification, payload
+//! deserialization, and script dispatching.
+
+pub mod types;
+pub mod handlers;
+pub mod verifiers;
+
+pub use types::*;
+pub use handlers::{stripe::handle_stripe_webhook, idenfy::handle_idenfy_webhook, common::*};
+
+use actix_web::{web, HttpResponse, Result as ActixResult};
+
+/// Configure webhook routes for the Actix-web application
+pub fn configure_webhook_routes(cfg: &mut web::ServiceConfig) {
+ cfg.service(
+ web::scope("/webhooks")
+ .route("/stripe/{circle_pk}", web::post().to(handle_stripe_webhook))
+ .route("/idenfy/{circle_pk}", web::post().to(handle_idenfy_webhook))
+ );
+}
+
+/// Health check endpoint for webhooks
+pub async fn webhook_health() -> ActixResult {
+ Ok(HttpResponse::Ok().json(serde_json::json!({
+ "status": "ok",
+ "service": "webhook_handler"
+ })))
+}
\ No newline at end of file
diff --git a/src/server/src/webhook/types.rs b/src/server/src/webhook/types.rs
new file mode 100644
index 0000000..89c7acd
--- /dev/null
+++ b/src/server/src/webhook/types.rs
@@ -0,0 +1,87 @@
+//! Webhook type definitions and payload structures
+
+use chrono::{DateTime, Utc};
+
+/// Webhook provider types
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum WebhookProvider {
+ Stripe,
+ Idenfy,
+}
+
+impl WebhookProvider {
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ WebhookProvider::Stripe => "stripe",
+ WebhookProvider::Idenfy => "idenfy",
+ }
+ }
+}
+
+/// Common webhook context passed to all webhook handlers
+#[derive(Debug, Clone)]
+pub struct WebhookContext {
+ pub circle_id: String,
+ pub provider: WebhookProvider,
+ pub caller_id: String,
+ pub timestamp: DateTime,
+}
+
+
+/// Webhook verification result
+#[derive(Debug)]
+pub struct WebhookVerificationResult {
+ pub is_valid: bool,
+ pub caller_id: String,
+ pub error: Option,
+}
+
+/// Webhook processing result
+#[derive(Debug)]
+pub struct WebhookProcessingResult {
+ pub success: bool,
+ pub message: String,
+ pub script_result: Option,
+}
+
+/// Configuration for webhook secrets loaded from environment
+#[derive(Debug, Clone)]
+pub struct WebhookConfig {
+ pub stripe_webhook_secret: String,
+ pub idenfy_webhook_secret: String,
+}
+
+impl WebhookConfig {
+ /// Load webhook configuration from environment variables
+ pub fn from_env() -> Result {
+ let stripe_secret = std::env::var("STRIPE_WEBHOOK_SECRET")
+ .map_err(|_| "STRIPE_WEBHOOK_SECRET environment variable not set")?;
+
+ let idenfy_secret = std::env::var("IDENFY_WEBHOOK_SECRET")
+ .map_err(|_| "IDENFY_WEBHOOK_SECRET environment variable not set")?;
+
+ Ok(WebhookConfig {
+ stripe_webhook_secret: stripe_secret,
+ idenfy_webhook_secret: idenfy_secret,
+ })
+ }
+}
+
+/// Error types for webhook processing
+#[derive(Debug, thiserror::Error)]
+pub enum WebhookError {
+ #[error("Invalid signature: {0}")]
+ InvalidSignature(String),
+
+ #[error("Payload parsing error: {0}")]
+ PayloadParsingError(String),
+
+ #[error("Configuration error: {0}")]
+ ConfigurationError(String),
+
+ #[error("Script execution error: {0}")]
+ ScriptExecutionError(String),
+
+ #[error("Internal error: {0}")]
+ InternalError(String),
+}
\ No newline at end of file
diff --git a/src/server/src/webhook/verifiers.rs b/src/server/src/webhook/verifiers.rs
new file mode 100644
index 0000000..c94a1d0
--- /dev/null
+++ b/src/server/src/webhook/verifiers.rs
@@ -0,0 +1,191 @@
+//! Webhook signature verification implementations
+
+use crate::webhook::types::{WebhookProvider, WebhookVerificationResult, WebhookConfig, WebhookError};
+use hmac::{Hmac, Mac};
+use sha2::Sha256;
+use hex;
+use log::{debug, warn};
+
+type HmacSha256 = Hmac;
+
+/// Trait for webhook signature verification
+pub trait WebhookVerifier {
+ fn verify_signature(
+ &self,
+ payload: &[u8],
+ signature: &str,
+ secret: &str,
+ ) -> Result;
+}
+
+/// Stripe webhook signature verifier
+pub struct StripeVerifier;
+
+impl WebhookVerifier for StripeVerifier {
+ fn verify_signature(
+ &self,
+ payload: &[u8],
+ signature: &str,
+ secret: &str,
+ ) -> Result {
+ debug!("Verifying Stripe webhook signature");
+
+ // Stripe signature format: "t=timestamp,v1=signature"
+ let mut timestamp = None;
+ let mut signatures = Vec::new();
+
+ for part in signature.split(',') {
+ if let Some(value) = part.strip_prefix("t=") {
+ timestamp = Some(value);
+ } else if let Some(value) = part.strip_prefix("v1=") {
+ signatures.push(value);
+ }
+ }
+
+ let timestamp = timestamp.ok_or_else(|| {
+ WebhookError::InvalidSignature("Missing timestamp in Stripe signature".to_string())
+ })?;
+
+ if signatures.is_empty() {
+ return Err(WebhookError::InvalidSignature(
+ "No v1 signatures found in Stripe signature".to_string()
+ ));
+ }
+
+ // Create the signed payload: timestamp.payload
+ let signed_payload = format!("{}.{}", timestamp, std::str::from_utf8(payload)
+ .map_err(|e| WebhookError::PayloadParsingError(format!("Invalid UTF-8: {}", e)))?);
+
+ // Compute HMAC-SHA256
+ let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
+ .map_err(|e| WebhookError::InvalidSignature(format!("Invalid secret: {}", e)))?;
+ mac.update(signed_payload.as_bytes());
+ let expected_signature = hex::encode(mac.finalize().into_bytes());
+
+ // Check if any of the provided signatures match
+ for sig in signatures {
+ if constant_time_eq(&expected_signature, sig) {
+ debug!("Stripe webhook signature verified successfully");
+ return Ok(WebhookVerificationResult {
+ is_valid: true,
+ caller_id: "stripe".to_string(),
+ error: None,
+ });
+ }
+ }
+
+ warn!("Stripe webhook signature verification failed");
+ Ok(WebhookVerificationResult {
+ is_valid: false,
+ caller_id: "stripe".to_string(),
+ error: Some("Signature mismatch".to_string()),
+ })
+ }
+}
+
+/// iDenfy webhook signature verifier
+pub struct IdenfyVerifier;
+
+impl WebhookVerifier for IdenfyVerifier {
+ fn verify_signature(
+ &self,
+ payload: &[u8],
+ signature: &str,
+ secret: &str,
+ ) -> Result {
+ debug!("Verifying iDenfy webhook signature");
+
+ // iDenfy uses HMAC-SHA256 with hex encoding
+ let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
+ .map_err(|e| WebhookError::InvalidSignature(format!("Invalid secret: {}", e)))?;
+ mac.update(payload);
+ let expected_signature = hex::encode(mac.finalize().into_bytes());
+
+ // Remove any "sha256=" prefix if present
+ let provided_signature = signature.strip_prefix("sha256=").unwrap_or(signature);
+
+ if constant_time_eq(&expected_signature, provided_signature) {
+ debug!("iDenfy webhook signature verified successfully");
+ Ok(WebhookVerificationResult {
+ is_valid: true,
+ caller_id: "idenfy".to_string(),
+ error: None,
+ })
+ } else {
+ warn!("iDenfy webhook signature verification failed");
+ Ok(WebhookVerificationResult {
+ is_valid: false,
+ caller_id: "idenfy".to_string(),
+ error: Some("Signature mismatch".to_string()),
+ })
+ }
+ }
+}
+
+/// Get the appropriate verifier for a webhook provider
+pub fn get_verifier(provider: &WebhookProvider) -> Box {
+ match provider {
+ WebhookProvider::Stripe => Box::new(StripeVerifier),
+ WebhookProvider::Idenfy => Box::new(IdenfyVerifier),
+ }
+}
+
+/// Verify webhook signature based on provider
+pub fn verify_webhook_signature(
+ provider: &WebhookProvider,
+ payload: &[u8],
+ signature: &str,
+ config: &WebhookConfig,
+) -> Result {
+ let verifier = get_verifier(provider);
+ let secret = match provider {
+ WebhookProvider::Stripe => &config.stripe_webhook_secret,
+ WebhookProvider::Idenfy => &config.idenfy_webhook_secret,
+ };
+
+ verifier.verify_signature(payload, signature, secret)
+}
+
+/// Constant-time string comparison to prevent timing attacks
+fn constant_time_eq(a: &str, b: &str) -> bool {
+ if a.len() != b.len() {
+ return false;
+ }
+
+ let mut result = 0u8;
+ for (byte_a, byte_b) in a.bytes().zip(b.bytes()) {
+ result |= byte_a ^ byte_b;
+ }
+
+ result == 0
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_constant_time_eq() {
+ assert!(constant_time_eq("hello", "hello"));
+ assert!(!constant_time_eq("hello", "world"));
+ assert!(!constant_time_eq("hello", "hello2"));
+ assert!(!constant_time_eq("hello2", "hello"));
+ }
+
+ #[test]
+ fn test_stripe_signature_parsing() {
+ let verifier = StripeVerifier;
+ let payload = b"test payload";
+ let secret = "test_secret";
+
+ // This would fail because we need a proper timestamp and signature
+ // but it tests the parsing logic
+ let result = verifier.verify_signature(
+ payload,
+ "invalid_format",
+ secret,
+ );
+
+ assert!(result.is_err());
+ }
+}
\ No newline at end of file
diff --git a/src/server_ws/tests/basic_integration_test.rs b/src/server/tests/basic_integration_test.rs
similarity index 78%
rename from src/server_ws/tests/basic_integration_test.rs
rename to src/server/tests/basic_integration_test.rs
index 2bac832..b1f1960 100644
--- a/src/server_ws/tests/basic_integration_test.rs
+++ b/src/server/tests/basic_integration_test.rs
@@ -1,5 +1,5 @@
use circle_ws_lib::{spawn_circle_server, ServerConfig};
-use engine::create_heromodels_engine;
+use rhailib_engine::create_heromodels_engine;
use futures_util::{SinkExt, StreamExt};
use heromodels::db::hero::OurDB;
use rhailib_worker::spawn_rhai_worker;
@@ -7,18 +7,20 @@ use serde_json::json;
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
+use uuid::Uuid;
#[tokio::test]
async fn test_server_startup_and_play() {
- let circle_pk = "test_pub_key_for_worker";
+ let circle_pk = Uuid::new_v4().to_string();
let redis_url = "redis://127.0.0.1/";
// --- Worker Setup ---
let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
let db = Arc::new(OurDB::new("file:memdb_test_server?mode=memory&cache=shared", true).unwrap());
- let engine = create_heromodels_engine(db);
+ let engine = create_heromodels_engine();
+ let worker_id = Uuid::new_v4().to_string();
let worker_handle = spawn_rhai_worker(
- 0,
+ worker_id,
circle_pk.to_string(),
engine,
redis_url.to_string(),
@@ -27,16 +29,11 @@ async fn test_server_startup_and_play() {
);
// --- Server Setup ---
- let config = ServerConfig {
- circle_name: "test_circle".to_string(),
- circle_public_key: circle_pk.to_string(),
- host: "127.0.0.1".to_string(),
- port: 9997, // Using a different port to avoid conflicts
- redis_url: redis_url.to_string(),
- enable_auth: false,
- cert_path: None,
- key_path: None,
- };
+ let config = ServerConfig::new(
+ "127.0.0.1".to_string(),
+ 9997, // Using a different port to avoid conflicts
+ redis_url.to_string(),
+ );
let (server_task, server_handle) = spawn_circle_server(config).unwrap();
let server_join_handle = tokio::spawn(server_task);
@@ -44,7 +41,7 @@ async fn test_server_startup_and_play() {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
// --- Client Connection and Test ---
- let ws_url = "ws://127.0.0.1:9997/ws";
+ let ws_url = format!("ws://127.0.0.1:9997/{}", circle_pk);
let (mut ws_stream, _) = connect_async(ws_url).await.expect("Failed to connect");
let play_req = json!({
diff --git a/src/server_ws/tests/connection_test.rs b/src/server/tests/connection_test.rs
similarity index 59%
rename from src/server_ws/tests/connection_test.rs
rename to src/server/tests/connection_test.rs
index 5cdf937..632c134 100644
--- a/src/server_ws/tests/connection_test.rs
+++ b/src/server/tests/connection_test.rs
@@ -5,22 +5,17 @@ use url::Url;
#[tokio::test]
async fn test_server_connection() {
- let config = ServerConfig {
- circle_name: "test_circle".to_string(),
- circle_public_key: "test_pub_key".to_string(),
- host: "127.0.0.1".to_string(),
- port: 9001,
- redis_url: "redis://127.0.0.1:6379".to_string(),
- enable_auth: false,
- cert_path: None,
- key_path: None,
- };
+ let config = ServerConfig::new(
+ "127.0.0.1".to_string(),
+ 9001,
+ "redis://127.0.0.1:6379".to_string(),
+ );
let (server_handle, _server_stop_handle) = spawn_circle_server(config).unwrap();
tokio::time::sleep(Duration::from_secs(1)).await;
- let url_str = "ws://127.0.0.1:9001/ws";
+ let url_str = "ws://127.0.0.1:9001/test_pub_key";
let url = Url::parse(url_str).unwrap();
let (ws_stream, _) = connect_async(url).await.expect("Failed to connect");
diff --git a/src/server_ws/tests/timeout_integration_test.rs b/src/server/tests/timeout_integration_test.rs
similarity index 88%
rename from src/server_ws/tests/timeout_integration_test.rs
rename to src/server/tests/timeout_integration_test.rs
index bcaebb3..5f1647a 100644
--- a/src/server_ws/tests/timeout_integration_test.rs
+++ b/src/server/tests/timeout_integration_test.rs
@@ -33,22 +33,17 @@ struct JsonRpcErrorDetails {
message: String,
}
-const SERVER_ADDRESS: &str = "ws://127.0.0.1:8088/ws";
+const SERVER_ADDRESS: &str = "ws://127.0.0.1:8088/test_pub_key_timeout";
const TEST_CIRCLE_NAME: &str = "test_timeout_circle";
const RHAI_TIMEOUT_SECONDS: u64 = 30; // Match server's default timeout
#[tokio::test]
async fn test_rhai_script_timeout() {
- let server_config = ServerConfig {
- circle_name: TEST_CIRCLE_NAME.to_string(),
- circle_public_key: "test_pub_key_timeout".to_string(),
- host: "127.0.0.1".to_string(),
- port: 8088,
- redis_url: "redis://127.0.0.1:6379".to_string(),
- enable_auth: false, // Auth not needed for this test
- cert_path: None,
- key_path: None,
- };
+ let server_config = ServerConfig::new(
+ "127.0.0.1".to_string(),
+ 8088,
+ "redis://127.0.0.1:6379".to_string(),
+ );
let (server_handle, _server_stop_handle) = spawn_circle_server(server_config).unwrap();
sleep(Duration::from_secs(2)).await; // Give server time to start
diff --git a/src/server/tests/wss_integration_test.rs b/src/server/tests/wss_integration_test.rs
new file mode 100644
index 0000000..511da09
--- /dev/null
+++ b/src/server/tests/wss_integration_test.rs
@@ -0,0 +1,85 @@
+use circle_ws_lib::{spawn_circle_server, ServerConfig};
+use std::time::Duration;
+use tokio::time::sleep;
+
+#[tokio::test]
+async fn test_basic_ws_server_startup() {
+ env_logger::init();
+
+ let config = ServerConfig::new(
+ "127.0.0.1".to_string(),
+ 8091, // Use a different port to avoid conflicts
+ "redis://127.0.0.1:6379".to_string(),
+ );
+
+ let (server_task, server_handle) = spawn_circle_server(config)
+ .expect("Failed to spawn circle server");
+
+ // Let the server run for a short time
+ sleep(Duration::from_millis(100)).await;
+
+ // Stop the server
+ server_handle.stop(true).await;
+
+ // Wait for the server task to complete
+ let _ = server_task.await;
+}
+
+#[tokio::test]
+async fn test_tls_server_configuration() {
+ env_logger::init();
+
+ // Test TLS configuration validation
+ let config = ServerConfig::new(
+ "127.0.0.1".to_string(),
+ 8092,
+ "redis://127.0.0.1:6379".to_string(),
+ )
+ .with_tls("nonexistent_cert.pem".to_string(), "nonexistent_key.pem".to_string())
+ .with_tls_port(8444);
+
+ // This should fail gracefully if cert files don't exist
+ match spawn_circle_server(config) {
+ Ok((server_task, server_handle)) => {
+ // If it succeeds (cert files exist), clean up
+ sleep(Duration::from_millis(100)).await;
+ server_handle.stop(true).await;
+ let _ = server_task.await;
+ println!("TLS server started successfully (cert files found)");
+ }
+ Err(e) => {
+ // Expected if cert files don't exist - this is fine for testing
+ println!("TLS server failed to start as expected: {}", e);
+ assert!(e.to_string().contains("Certificate") || e.to_string().contains("TLS"));
+ }
+ }
+}
+
+#[tokio::test]
+async fn test_server_config_validation() {
+ // Test that ServerConfig properly validates TLS settings
+ let config = ServerConfig::new(
+ "127.0.0.1".to_string(),
+ 8093,
+ "redis://127.0.0.1:6379".to_string(),
+ );
+
+ // Test basic configuration
+
+ assert_eq!(config.host, "127.0.0.1");
+ assert_eq!(config.port, 8093);
+ assert!(!config.enable_tls);
+ assert!(!config.enable_auth);
+
+ // Test TLS configuration
+ let tls_config = config
+ .with_tls("cert.pem".to_string(), "key.pem".to_string())
+ .with_tls_port(8445)
+ .with_auth();
+
+ assert!(tls_config.enable_tls);
+ assert!(tls_config.enable_auth);
+ assert_eq!(tls_config.get_tls_port(), 8445);
+ assert_eq!(tls_config.cert_path, Some("cert.pem".to_string()));
+ assert_eq!(tls_config.key_path, Some("key.pem".to_string()));
+}
\ No newline at end of file
diff --git a/src/server_ws/.DS_Store b/src/server_ws/.DS_Store
deleted file mode 100644
index 64142e8cb34334bf46baf1c054e251e42ba29a9f..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 6148
zcmeHKJ5B>J5S<}GT0xV7g3?#W4NSD@=yCzH34$W44J7n?uSkdjaRAPMxB_(W#xp2u
zv7$tR%t-dL=i^yFX>AV?@#JnXCK?e@fhNc@=n+x(nhwlaj4W%6XY;0BB`xNELgd>A
zw5D5{FR!}afAqey)J<7d%cg>+sBX3oj~`EGUF+fp>*~6@YD5?v(uOXPb3^MaC!M_H
z)^6VO`{(BME#>XR*Uc_>zekpzg5-B`2AlzBz!}(Y2JDV5j`y1e-@P;74D>J{=R-gf
zjE1dZIy%s$5&$@aItj*7OGr#GjE1cuED+XEpoX%Q7_8yY2lI=Dt)hk#Tk*k``Kx&0
zlpX7b|wjft-PXPDgV8Pw~t27WsLI&zu2g;GZ$T!*W(muqnG+zim(M+JJU}
sCL(^V3?LY1j{r>M963!!wFeo)FB-OrvWn>+ActixHttpServer: Establishes WebSocket connection
- ActixHttpServer->>ActixHttpServer: Spawns a new CircleWsActor
- ActixHttpServer-->>-Client: WebSocket connection established
-
- Note over CircleWsActor: Session created, authenticated = false
-
- Client->>+CircleWsActor: Sends "fetch_nonce" JSON-RPC message
- CircleWsActor->>CircleWsActor: Generate and store nonce for pubkey
- CircleWsActor-->>-Client: Returns nonce in JSON-RPC response
-
- Client->>Client: Signs nonce with private key
-
- Client->>+CircleWsActor: Sends "authenticate" JSON-RPC message
- CircleWsActor->>+SignatureVerifier: verify_signature(pubkey, nonce, signature)
- SignatureVerifier-->>-CircleWsActor: Returns verification result
-
- alt Signature is Valid
- CircleWsActor->>CircleWsActor: Set session state: authenticated = true
- CircleWsActor-->>-Client: Returns success response
- else Signature is Invalid
- CircleWsActor-->>-Client: Returns error response
- end
-
- Note over CircleWsActor: Client is now authenticated
-
- Client->>+CircleWsActor: Sends "play" JSON-RPC message
- CircleWsActor->>CircleWsActor: Check if authenticated
- alt Is Authenticated
- CircleWsActor->>CircleWsActor: Get public key from authenticated connections map
- CircleWsActor->>CircleWsActor: Execute Rhai script with public key
- CircleWsActor-->>-Client: Returns script result
- else Is Not Authenticated
- CircleWsActor-->>-Client: Returns "Authentication Required" error
- end
-```
-
-This architecture ensures a clear separation of concerns and a unified communication protocol:
-- The `HttpServer` handles connection management.
-- The `CircleWs` actor manages the entire session lifecycle, including state and all API logic.
-- The `auth` module provides a self-contained, reusable signature verification utility.
\ No newline at end of file
diff --git a/src/server_ws/README.md b/src/server_ws/README.md
deleted file mode 100644
index 5536294..0000000
--- a/src/server_ws/README.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# `server_ws`: The Circles WebSocket Server
-
-The `server_ws` crate provides a secure, high-performance WebSocket server built with `Actix`. It is the core backend component of the `circles` ecosystem, responsible for handling client connections, processing JSON-RPC requests, and executing Rhai scripts in a secure manner.
-
-## Features
-
-- **`Actix` Framework**: Built on `Actix`, a powerful and efficient actor-based web framework.
-- **WebSocket Management**: Uses `actix-web-actors` to manage each client connection in its own isolated actor (`CircleWs`), ensuring robust and concurrent session handling.
-- **JSON-RPC 2.0 API**: Implements a JSON-RPC 2.0 API for all client-server communication. The API is formally defined in the root [openrpc.json](../../openrpc.json) file.
-- **Secure Authentication**: Features a built-in `secp256k1` signature-based authentication system to protect sensitive endpoints.
-- **Stateful Session Management**: The `CircleWs` actor maintains the authentication state for each client, granting or denying access to protected methods like `play`.
-
-## Core Components
-
-### `spawn_circle_server`
-
-This is the main entry point function for the server. It configures and starts the `Actix` HTTP server and sets up the WebSocket route (`/ws`).
-
-### `CircleWs` Actor
-
-This `Actix` actor is the heart of the server's session management. A new instance of `CircleWs` is created for each client that connects. Its responsibilities include:
-- Handling the WebSocket connection lifecycle.
-- Parsing incoming JSON-RPC messages.
-- Managing the authentication state of the session (i.e., whether the client is authenticated or not).
-- Dispatching requests to the appropriate handlers (`fetch_nonce`, `authenticate`, and `play`).
-
-## Authentication
-
-The server provides a robust authentication mechanism to ensure that only authorized clients can execute scripts. The entire flow is handled over the WebSocket connection using two dedicated JSON-RPC methods:
-
-1. **`fetch_nonce`**: The client requests a unique, single-use nonce (a challenge) from the server.
-2. **`authenticate`**: The client sends back the nonce signed with its private key. The `CircleWs` actor verifies the signature to confirm the client's identity.
-
-For a more detailed breakdown of the authentication architecture, please see the [ARCHITECTURE.md](ARCHITECTURE.md) file.
-
-## How to Run
-
-The `server_ws` is not intended to be run as a standalone binary. It is designed to be used as a library by the `launcher`, which is responsible for spawning and managing one or more server instances based on the `circles.json` configuration file.
-
-To run the server, use the `launcher`:
-```bash
-cargo run --package launcher
-```
-
-The launcher will start a `server_ws` instance for each circle defined in your configuration, binding it to the specified port.
diff --git a/src/server_ws/cert.pem b/src/server_ws/cert.pem
deleted file mode 100644
index 6602b46..0000000
--- a/src/server_ws/cert.pem
+++ /dev/null
@@ -1,17 +0,0 @@
------BEGIN CERTIFICATE-----
-MIICwzCCAaugAwIBAgIJAMq5+65CHYssMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
-BAMMCWxvY2FsaG9zdDAeFw0yNTA2MTYxNDQxNDhaFw0yNjA2MTYxNDQxNDhaMBQx
-EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
-ggEBANA3I7aefAQarl6/PVkqHqaxcTrK6edywM44qPFGet3QwXCZMnemcQaTmibz
-dO/Y8k+i90pufHE+bksfv9xbTfrb3XOPi7GacyxV5VaatkbG18id8gx8vGgfIHYX
-4WKpdRJDY7GVlHBSRwIn81xB2EARUrq2p6VZ8Q3DduAu8ZI9MonRHaIjCt+g1j5b
-DVDIpTHPdYRxXidiNk59bLavX1Gxo23T00VUBS0SDPTa+PZNiZgJmXBrxfSJC8ee
-FlwGM7uUlcQDmzee17gkU/9ucYBX+rwljuaSHgM0taEZTPQBaUINwZdY3ReOirvy
-CHXeXvTJWDUCkXkZkP46QXA+BqUCAwEAAaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxo
-b3N0MA0GCSqGSIb3DQEBCwUAA4IBAQAp1YNwdAAeb48EfuozEDO3QGJ4lQlEC/gY
-XHOIhuovaIk+7YobYRUHEFs5o3BJxS/IzFB0yMMhZnwsRRDmtMTMjyAFhtlYwY4q
-ZutDJLps3ol0P9baDIHZfen3tlWl01KbkZnz3nzZnN62Ym8FWMCUPzuTLoGD3f5g
-VmltbMKvcgA59Lv9Ff+Wu79v4xdSHR9Fc0ZmVkhBpf/4hodX40vwPaNOA+5UBfe3
-bjronpr08sOUSiIeexwvSPnrUOTLLg45a3FMj8HlsmlZVugOfooGrUAAtp+9VVuF
-gCr1CgpRYFIk6B/asYMBHx9APwPenYsuQBfE9qrzCISjpV4IM0Ku
------END CERTIFICATE-----
diff --git a/src/server_ws/cmd/main.rs b/src/server_ws/cmd/main.rs
deleted file mode 100644
index d7756f0..0000000
--- a/src/server_ws/cmd/main.rs
+++ /dev/null
@@ -1,52 +0,0 @@
-use circle_ws_lib::{spawn_circle_server, ServerConfig};
-use clap::Parser;
-
-#[derive(Parser, Debug)]
-#[clap(author, version, about, long_about = None)]
-struct Args {
- #[clap(short, long, value_parser, default_value = "127.0.0.1")]
- host: String,
-
- #[clap(short, long, value_parser, default_value_t = 8080)]
- port: u16,
-
- #[clap(long, value_parser)]
- circle_name: String,
-
- #[clap(long, value_parser)]
- circle_public_key: String,
-
- #[clap(long, value_parser, default_value = "redis://127.0.0.1/")]
- redis_url: String,
-
- #[clap(long)]
- auth: bool,
-
- #[clap(long, value_parser)]
- cert: Option,
-
- #[clap(long, value_parser)]
- key: Option,
-}
-
-#[actix_web::main]
-async fn main() -> std::io::Result<()> {
- let args = Args::parse();
-
- std::env::set_var("RUST_LOG", "info,circle_ws_lib=debug");
- env_logger::init();
-
- let config = ServerConfig {
- circle_name: args.circle_name,
- circle_public_key: args.circle_public_key,
- host: args.host,
- port: args.port,
- redis_url: args.redis_url,
- enable_auth: args.auth,
- cert_path: args.cert,
- key_path: args.key,
- };
-
- let (server_task, _server_handle) = spawn_circle_server(config)?;
- server_task.await?
-}
diff --git a/src/server_ws/key.pem b/src/server_ws/key.pem
deleted file mode 100644
index 0cd26c7..0000000
--- a/src/server_ws/key.pem
+++ /dev/null
@@ -1,28 +0,0 @@
------BEGIN PRIVATE KEY-----
-MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQNyO2nnwEGq5e
-vz1ZKh6msXE6yunncsDOOKjxRnrd0MFwmTJ3pnEGk5om83Tv2PJPovdKbnxxPm5L
-H7/cW036291zj4uxmnMsVeVWmrZGxtfInfIMfLxoHyB2F+FiqXUSQ2OxlZRwUkcC
-J/NcQdhAEVK6tqelWfENw3bgLvGSPTKJ0R2iIwrfoNY+Ww1QyKUxz3WEcV4nYjZO
-fWy2r19RsaNt09NFVAUtEgz02vj2TYmYCZlwa8X0iQvHnhZcBjO7lJXEA5s3nte4
-JFP/bnGAV/q8JY7mkh4DNLWhGUz0AWlCDcGXWN0Xjoq78gh13l70yVg1ApF5GZD+
-OkFwPgalAgMBAAECggEASr2QR0xqjfqZ3w7c0bXp3MJTN9yAzAEILr5t/nXjQt5d
-obKMSCDp74dqTVMVBcela2q/Q+uXJftYEgVqlIn92u+zdXjwlycu8po+wbC1f4B/
-EteHfG4GH4YU2+9Ei2ryyESCc8uSxiLytFZs6BAGtTDCtp08XoGDowIoGN8g/fHt
-2BTUrmP3mdO6eITtxPfKTrwtcWvaav+d19FfXcGIGuOVwJkI9I5XIaVO8R2hyjKO
-wMf/VejGcSftqCnJ5vR9ZNx6b1wjhdZzpXfzirMpPcx5prZKG14N+hKIzmaF9l/l
-IpMYihX5Av0UcAEVE8Ierkji+MskVX2fRLlnuE5rPQKBgQDxf1BT8UJGu22gME9b
-ZV+YecafaN0ZHx7lyiZig+hxNnmrMCLtmhRctPszYcC8NIRf/CUQjO8WEbgaz3NQ
-Nu9uqSISrO0rLfqjcXNSONYW4++tM7nh9TWyoXr2WHGdOUDN4AQsdlGp/Yr0dJGR
-WO49o6P/30GmUGGLLypUinj1jwKBgQDcuCmHAN1kkRI/B6SHtZIgWvG7WmiqxSRj
-tUj6bcqe4WfdjmT/8VCdnq7dmKr4MOuYBnwp7tX4amFvX8q6eKMRcwQT8Vp2ai/p
-2AaElOk1STeiK96XhRJzUwHrEB6Gd0Y+ZtV6Y8498MftUsaJtlF3GH+px5bhP+BV
-F3P95f8uiwKBgBvKoP3oB86IB0emB5vnAKdVbEwwDzGy6SVcrCouL/PHZx0SPxLE
-5o78rOPu3fVKvekl5UYQUftiMqOzN/wFNmyvviIUEH8lnXBtv+24aLrdpFl0WHHu
-fC2Ac9whtYF4c0K+Avzy6Nb9PJ6BQ7dMWq5xRJBPqwhuu6r+1IpCHUV1AoGAWyau
-97Lyu0pyB5enaeghPb1xIRdYLFYYDhla1xkqWWzrEQNyUISe70kOHDOlP7QsR28+
-La8VrG56aJ1dwp55cYZXQ8kju81kliUBpBU/LiNbl1yYo2hwUzIPk7znbk6psrGT
-LeUz0j6ywh6yIUMq5401g68Kb2GKynlSDKPEO7sCgYEAtxXd/JI/JEF19dsHx412
-uH+AzTsHCtmXVL6d4a0S/27V5szql5M4LpusAY5TAuS1b8K+Tp7fp16AMyvbGq2A
-qQChbJS0fBevqrTXT6wlx1aCFVRNQskZO/JtaQ4a9edo8I788UzJ8O/dWycb+/o3
-P10PKW8nY2fmoj84tTu/Gxw=
------END PRIVATE KEY-----
diff --git a/tests/end_to_end_integration.rs b/tests/end_to_end_integration.rs
index a9155ac..29daa6e 100644
--- a/tests/end_to_end_integration.rs
+++ b/tests/end_to_end_integration.rs
@@ -1,8 +1,8 @@
-//! End-to-end example demonstrating client_ws and server_ws working together
+//! End-to-end example demonstrating client_ws and server working together
//!
//! This example:
//! 1. Starts a Redis server
-//! 2. Starts a server_ws instance with authentication enabled
+//! 2. Starts a server instance with authentication enabled
//! 3. Creates a client_ws instance with authentication
//! 4. Demonstrates the complete authentication flow
//! 5. Sends authenticated requests and receives responses