move repos into monorepo
This commit is contained in:
102
lib/clients/supervisor/src/builder.rs
Normal file
102
lib/clients/supervisor/src/builder.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
//! Builder pattern for WasmSupervisorClient to ensure proper configuration
|
||||
//!
|
||||
//! This module provides a type-safe builder that guarantees a client cannot be
|
||||
//! created without a secret, preventing authentication issues.
|
||||
|
||||
use crate::wasm::WasmSupervisorClient;
|
||||
|
||||
/// Builder for WasmSupervisorClient that enforces secret requirement
|
||||
#[derive(Clone)]
|
||||
pub struct WasmSupervisorClientBuilder {
|
||||
server_url: Option<String>,
|
||||
secret: Option<String>,
|
||||
}
|
||||
|
||||
impl WasmSupervisorClientBuilder {
|
||||
/// Create a new builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
server_url: None,
|
||||
secret: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the server URL
|
||||
pub fn server_url(mut self, url: impl Into<String>) -> Self {
|
||||
self.server_url = Some(url.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication secret (required)
|
||||
pub fn secret(mut self, secret: impl Into<String>) -> Self {
|
||||
self.secret = Some(secret.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the client
|
||||
///
|
||||
/// Returns Err if server_url or secret is not set
|
||||
pub fn build(self) -> Result<WasmSupervisorClient, String> {
|
||||
let server_url = self.server_url.ok_or("Server URL is required")?;
|
||||
let secret = self.secret.ok_or("Secret is required for authenticated client")?;
|
||||
|
||||
if secret.is_empty() {
|
||||
return Err("Secret cannot be empty".to_string());
|
||||
}
|
||||
|
||||
Ok(WasmSupervisorClient::new(server_url, secret))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WasmSupervisorClientBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_builder_requires_all_fields() {
|
||||
let builder = WasmSupervisorClientBuilder::new();
|
||||
assert!(builder.build().is_err());
|
||||
|
||||
let builder = WasmSupervisorClientBuilder::new()
|
||||
.server_url("http://localhost:3030");
|
||||
assert!(builder.build().is_err());
|
||||
|
||||
let builder = WasmSupervisorClientBuilder::new()
|
||||
.secret("test-secret");
|
||||
assert!(builder.build().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_success() {
|
||||
let builder = WasmSupervisorClientBuilder::new()
|
||||
.server_url("http://localhost:3030")
|
||||
.secret("test-secret");
|
||||
assert!(builder.build().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_error_messages() {
|
||||
let result = WasmSupervisorClientBuilder::new().build();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), "Server URL is required");
|
||||
|
||||
let result = WasmSupervisorClientBuilder::new()
|
||||
.server_url("http://localhost:3030")
|
||||
.build();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), "Secret is required for authenticated client");
|
||||
|
||||
let result = WasmSupervisorClientBuilder::new()
|
||||
.server_url("http://localhost:3030")
|
||||
.secret("")
|
||||
.build();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), "Secret cannot be empty");
|
||||
}
|
||||
}
|
||||
695
lib/clients/supervisor/src/lib.rs
Normal file
695
lib/clients/supervisor/src/lib.rs
Normal file
@@ -0,0 +1,695 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use serde_json;
|
||||
|
||||
// Import types from the main supervisor crate
|
||||
|
||||
|
||||
// WASM-compatible client module
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm;
|
||||
|
||||
// Builder module for type-safe client construction
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod builder;
|
||||
|
||||
// Re-export WASM types for convenience
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm::{WasmSupervisorClient, WasmJobType, WasmRunnerType, create_job_canonical_repr, sign_job_canonical};
|
||||
|
||||
// Re-export builder for convenience
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use builder::WasmSupervisorClientBuilder;
|
||||
|
||||
// Native client dependencies
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use jsonrpsee::{
|
||||
core::client::ClientT,
|
||||
http_client::{HttpClient, HttpClientBuilder},
|
||||
rpc_params,
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use http::{HeaderMap, HeaderName, HeaderValue};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Client for communicating with Hero Supervisor OpenRPC server
|
||||
/// Requires authentication secret for all operations
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Clone)]
|
||||
pub struct SupervisorClient {
|
||||
client: HttpClient,
|
||||
server_url: String,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
/// Error types for client operations
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ClientError {
|
||||
#[error("JSON-RPC error: {0}")]
|
||||
JsonRpc(#[from] jsonrpsee::core::ClientError),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("HTTP client error: {0}")]
|
||||
Http(String),
|
||||
|
||||
#[error("Server error: {message}")]
|
||||
Server { message: String },
|
||||
}
|
||||
|
||||
/// Error types for WASM client operations
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ClientError {
|
||||
#[error("JSON-RPC error: {0}")]
|
||||
JsonRpc(String),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("HTTP client error: {0}")]
|
||||
Http(String),
|
||||
|
||||
#[error("Server error: {message}")]
|
||||
Server { message: String },
|
||||
|
||||
#[error("JavaScript error: {0}")]
|
||||
JavaScript(String),
|
||||
|
||||
#[error("Network error: {0}")]
|
||||
Network(String),
|
||||
}
|
||||
|
||||
// Implement From for jsonrpsee ClientError for WASM
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl From<wasm_bindgen::JsValue> for ClientError {
|
||||
fn from(js_val: wasm_bindgen::JsValue) -> Self {
|
||||
ClientError::JavaScript(format!("{:?}", js_val))
|
||||
}
|
||||
}
|
||||
|
||||
/// Result type for client operations
|
||||
pub type ClientResult<T> = Result<T, ClientError>;
|
||||
|
||||
/// Request parameters for generating API keys (auto-generates key value)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GenerateApiKeyParams {
|
||||
pub name: String,
|
||||
pub scope: String, // "admin", "registrar", or "user"
|
||||
}
|
||||
|
||||
/// Configuration for a runner
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RunnerConfig {
|
||||
/// Name of the runner
|
||||
pub name: String,
|
||||
/// Command to run the runner (full command line)
|
||||
pub command: String,
|
||||
/// Optional environment variables
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub env: Option<std::collections::HashMap<String, String>>,
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Job result response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum JobResult {
|
||||
Success { success: String },
|
||||
Error { error: String },
|
||||
}
|
||||
|
||||
/// Job status response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JobStatusResponse {
|
||||
pub job_id: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub started_at: Option<String>,
|
||||
pub completed_at: Option<String>,
|
||||
}
|
||||
|
||||
/// Response from job.run (blocking execution)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JobRunResponse {
|
||||
pub job_id: String,
|
||||
pub status: String,
|
||||
pub result: Option<String>,
|
||||
}
|
||||
|
||||
/// Response from job.start (non-blocking execution)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JobStartResponse {
|
||||
pub job_id: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
// Re-export Job types from hero-job crate (both native and WASM)
|
||||
pub use hero_job::{Job, JobStatus, JobError, JobBuilder, JobSignature};
|
||||
|
||||
// Note: Job client is now part of hero-supervisor crate
|
||||
// Re-exports removed - use hero_supervisor::job_client directly if needed
|
||||
|
||||
/// Process status wrapper for OpenRPC serialization (matches server response)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ProcessStatusWrapper {
|
||||
Running,
|
||||
Stopped,
|
||||
Starting,
|
||||
Stopping,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
/// Log information wrapper for OpenRPC serialization (matches server response)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogInfoWrapper {
|
||||
pub timestamp: String,
|
||||
pub level: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Supervisor information response containing secret counts and server details
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SupervisorInfo {
|
||||
pub server_url: String,
|
||||
}
|
||||
|
||||
/// API Key information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApiKey {
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
pub scope: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
/// Auth verification response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthVerifyResponse {
|
||||
pub scope: String,
|
||||
pub name: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
}
|
||||
|
||||
/// Simple ProcessStatus type for native builds to avoid service manager dependency
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub type ProcessStatus = ProcessStatusWrapper;
|
||||
|
||||
// Types duplicated from supervisor-core to avoid cyclic dependency
|
||||
// These match the types in hero-supervisor but are defined here independently
|
||||
|
||||
/// Runner status information (duplicated to avoid cyclic dependency)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub type RunnerStatus = ProcessStatusWrapper;
|
||||
|
||||
/// Log information (duplicated to avoid cyclic dependency)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub type LogInfo = LogInfoWrapper;
|
||||
|
||||
/// Type aliases for WASM compatibility
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub type ProcessStatus = ProcessStatusWrapper;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub type RunnerStatus = ProcessStatusWrapper;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub type LogInfo = LogInfoWrapper;
|
||||
|
||||
/// Builder for SupervisorClient
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SupervisorClientBuilder {
|
||||
url: Option<String>,
|
||||
secret: Option<String>,
|
||||
timeout: Option<std::time::Duration>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl SupervisorClientBuilder {
|
||||
/// Create a new builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
url: None,
|
||||
secret: None,
|
||||
timeout: Some(std::time::Duration::from_secs(30)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the server URL
|
||||
pub fn url(mut self, url: impl Into<String>) -> Self {
|
||||
self.url = Some(url.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication secret
|
||||
pub fn secret(mut self, secret: impl Into<String>) -> Self {
|
||||
self.secret = Some(secret.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the request timeout (default: 30 seconds)
|
||||
pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
|
||||
self.timeout = Some(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the SupervisorClient
|
||||
pub fn build(self) -> ClientResult<SupervisorClient> {
|
||||
let server_url = self.url
|
||||
.ok_or_else(|| ClientError::Http("URL is required".to_string()))?;
|
||||
let secret = self.secret
|
||||
.ok_or_else(|| ClientError::Http("Secret is required".to_string()))?;
|
||||
|
||||
// Create headers with Authorization bearer token
|
||||
let mut headers = HeaderMap::new();
|
||||
let auth_value = format!("Bearer {}", secret);
|
||||
headers.insert(
|
||||
HeaderName::from_static("authorization"),
|
||||
HeaderValue::from_str(&auth_value)
|
||||
.map_err(|e| ClientError::Http(format!("Invalid auth header: {}", e)))?
|
||||
);
|
||||
|
||||
let client = HttpClientBuilder::default()
|
||||
.request_timeout(self.timeout.unwrap_or(std::time::Duration::from_secs(30)))
|
||||
.set_headers(headers)
|
||||
.build(&server_url)
|
||||
.map_err(|e| ClientError::Http(e.to_string()))?;
|
||||
|
||||
Ok(SupervisorClient {
|
||||
client,
|
||||
server_url,
|
||||
secret,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Default for SupervisorClientBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl SupervisorClient {
|
||||
/// Create a builder for SupervisorClient
|
||||
pub fn builder() -> SupervisorClientBuilder {
|
||||
SupervisorClientBuilder::new()
|
||||
}
|
||||
|
||||
/// Get the server URL
|
||||
pub fn server_url(&self) -> &str {
|
||||
&self.server_url
|
||||
}
|
||||
|
||||
/// Test connection using OpenRPC discovery method
|
||||
/// This calls the standard `rpc.discover` method that should be available on any OpenRPC server
|
||||
pub async fn discover(&self) -> ClientResult<serde_json::Value> {
|
||||
let result: serde_json::Value = self
|
||||
.client
|
||||
.request("rpc.discover", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Register a new runner to the supervisor
|
||||
/// The runner name is also used as the queue name
|
||||
/// Authentication via Authorization header (set during client creation)
|
||||
pub async fn runner_create(
|
||||
&self,
|
||||
name: &str,
|
||||
) -> ClientResult<()> {
|
||||
let _: () = self
|
||||
.client
|
||||
.request("runner.create", rpc_params![name])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a new job without queuing it to a runner
|
||||
/// Authentication via Authorization header (set during client creation)
|
||||
pub async fn job_create(
|
||||
&self,
|
||||
job: Job,
|
||||
) -> ClientResult<String> {
|
||||
let job_id: String = self
|
||||
.client
|
||||
.request("job.create", rpc_params![job])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(job_id)
|
||||
}
|
||||
|
||||
/// List all jobs
|
||||
pub async fn job_list(&self) -> ClientResult<Vec<Job>> {
|
||||
let jobs: Vec<Job> = self
|
||||
.client
|
||||
.request("job.list", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(jobs)
|
||||
}
|
||||
|
||||
/// Run a job on the appropriate runner and wait for the result (blocking)
|
||||
/// This method queues the job and waits for completion before returning
|
||||
/// The secret is sent via Authorization header (set during client creation)
|
||||
pub async fn job_run(
|
||||
&self,
|
||||
job: Job,
|
||||
timeout: Option<u64>,
|
||||
) -> ClientResult<JobRunResponse> {
|
||||
let mut params = serde_json::json!({
|
||||
"job": job
|
||||
});
|
||||
|
||||
if let Some(t) = timeout {
|
||||
params["timeout"] = serde_json::json!(t);
|
||||
}
|
||||
|
||||
let result: JobRunResponse = self
|
||||
.client
|
||||
.request("job.run", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Start a job without waiting for the result (non-blocking)
|
||||
/// This method queues the job and returns immediately with the job_id
|
||||
/// Authentication via Authorization header (set during client creation)
|
||||
pub async fn job_start(
|
||||
&self,
|
||||
job: Job,
|
||||
) -> ClientResult<JobStartResponse> {
|
||||
let params = serde_json::json!({
|
||||
"job": job
|
||||
});
|
||||
|
||||
let result: JobStartResponse = self
|
||||
.client
|
||||
.request("job.start", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get the current status of a job
|
||||
pub async fn job_status(&self, job_id: &str) -> ClientResult<JobStatus> {
|
||||
let status: JobStatus = self
|
||||
.client
|
||||
.request("job.status", rpc_params![job_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
/// Get the result of a completed job (blocks until result is available)
|
||||
pub async fn job_result(&self, job_id: &str) -> ClientResult<JobResult> {
|
||||
let result: JobResult = self
|
||||
.client
|
||||
.request("job.result", rpc_params![job_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Remove a runner from the supervisor
|
||||
/// Authentication via Authorization header (set during client creation)
|
||||
pub async fn runner_remove(&self, runner_id: &str) -> ClientResult<()> {
|
||||
let _: () = self
|
||||
.client
|
||||
.request("runner.remove", rpc_params![runner_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all runner IDs
|
||||
pub async fn runner_list(&self) -> ClientResult<Vec<String>> {
|
||||
let runners: Vec<String> = self
|
||||
.client
|
||||
.request("runner.list", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(runners)
|
||||
}
|
||||
|
||||
/// Start a specific runner
|
||||
/// Authentication via Authorization header (set during client creation)
|
||||
pub async fn start_runner(&self, actor_id: &str) -> ClientResult<()> {
|
||||
let _: () = self
|
||||
.client
|
||||
.request("runner.start", rpc_params![actor_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a runner to the supervisor
|
||||
/// Authentication via Authorization header (set during client creation)
|
||||
pub async fn add_runner(&self, config: RunnerConfig) -> ClientResult<()> {
|
||||
let params = serde_json::json!({
|
||||
"config": config
|
||||
});
|
||||
let _: () = self
|
||||
.client
|
||||
.request("runner.add", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get status of a specific runner
|
||||
/// Authentication via Authorization header (set during client creation)
|
||||
pub async fn get_runner_status(&self, actor_id: &str) -> ClientResult<RunnerStatus> {
|
||||
let status: RunnerStatus = self
|
||||
.client
|
||||
.request("runner.status", rpc_params![actor_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
/// Get logs for a specific runner
|
||||
pub async fn get_runner_logs(
|
||||
&self,
|
||||
actor_id: &str,
|
||||
lines: Option<usize>,
|
||||
follow: bool,
|
||||
) -> ClientResult<Vec<LogInfo>> {
|
||||
let logs: Vec<LogInfo> = self
|
||||
.client
|
||||
.request("get_runner_logs", rpc_params![actor_id, lines, follow])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
/// Queue a job to a specific runner
|
||||
pub async fn queue_job_to_runner(&self, runner: &str, job: Job) -> ClientResult<()> {
|
||||
let params = serde_json::json!({
|
||||
"runner": runner,
|
||||
"job": job
|
||||
});
|
||||
|
||||
let _: () = self
|
||||
.client
|
||||
.request("queue_job_to_runner", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queue a job and wait for completion
|
||||
pub async fn queue_and_wait(&self, runner: &str, job: Job, timeout_secs: u64) -> ClientResult<Option<String>> {
|
||||
let params = serde_json::json!({
|
||||
"runner": runner,
|
||||
"job": job,
|
||||
"timeout_secs": timeout_secs
|
||||
});
|
||||
|
||||
let result: Option<String> = self
|
||||
.client
|
||||
.request("queue_and_wait", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Run a job on a specific runner
|
||||
pub async fn run_job(&self, job: Job) -> ClientResult<JobResult> {
|
||||
let params = serde_json::json!({
|
||||
"job": job
|
||||
});
|
||||
|
||||
let result: JobResult = self
|
||||
.client
|
||||
.request("job.run", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get job result by job ID
|
||||
pub async fn get_job_result(&self, job_id: &str) -> ClientResult<Option<String>> {
|
||||
let result: Option<String> = self
|
||||
.client
|
||||
.request("get_job_result", rpc_params![job_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get status of all runners
|
||||
pub async fn get_all_runner_status(&self) -> ClientResult<Vec<(String, RunnerStatus)>> {
|
||||
let statuses: Vec<(String, RunnerStatus)> = self
|
||||
.client
|
||||
.request("get_all_runner_status", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(statuses)
|
||||
}
|
||||
|
||||
/// Start all runners
|
||||
pub async fn start_all(&self) -> ClientResult<Vec<(String, bool)>> {
|
||||
let results: Vec<(String, bool)> = self
|
||||
.client
|
||||
.request("start_all", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Stop all runners
|
||||
pub async fn stop_all(&self, force: bool) -> ClientResult<Vec<(String, bool)>> {
|
||||
let results: Vec<(String, bool)> = self
|
||||
.client
|
||||
.request("stop_all", rpc_params![force])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Get status of all runners (alternative method)
|
||||
pub async fn get_all_status(&self) -> ClientResult<Vec<(String, RunnerStatus)>> {
|
||||
let statuses: Vec<(String, RunnerStatus)> = self
|
||||
.client
|
||||
.request("get_all_status", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(statuses)
|
||||
}
|
||||
|
||||
/// Add a secret to the supervisor
|
||||
pub async fn add_secret(
|
||||
&self,
|
||||
secret_type: &str,
|
||||
secret_value: &str,
|
||||
) -> ClientResult<()> {
|
||||
let params = serde_json::json!({
|
||||
"secret_type": secret_type,
|
||||
"secret_value": secret_value
|
||||
});
|
||||
|
||||
let _: () = self
|
||||
.client
|
||||
.request("add_secret", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a secret from the supervisor
|
||||
pub async fn remove_secret(
|
||||
&self,
|
||||
secret_type: &str,
|
||||
secret_value: &str,
|
||||
) -> ClientResult<()> {
|
||||
let params = serde_json::json!({
|
||||
"secret_type": secret_type,
|
||||
"secret_value": secret_value
|
||||
});
|
||||
|
||||
let _: () = self
|
||||
.client
|
||||
.request("remove_secret", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List secrets (returns supervisor info including secret counts)
|
||||
pub async fn list_secrets(&self) -> ClientResult<SupervisorInfo> {
|
||||
let params = serde_json::json!({});
|
||||
|
||||
let info: SupervisorInfo = self
|
||||
.client
|
||||
.request("list_secrets", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// Stop a running job
|
||||
pub async fn job_stop(&self, job_id: &str) -> ClientResult<()> {
|
||||
let _: () = self.client
|
||||
.request("job.stop", rpc_params![job_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a job from the system
|
||||
pub async fn job_delete(&self, job_id: &str) -> ClientResult<()> {
|
||||
let _: () = self.client
|
||||
.request("job.delete", rpc_params![job_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get supervisor information including secret counts
|
||||
pub async fn get_supervisor_info(&self) -> ClientResult<SupervisorInfo> {
|
||||
let info: SupervisorInfo = self
|
||||
.client
|
||||
.request("supervisor.info", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// Get a job by ID
|
||||
pub async fn job_get(&self, job_id: &str) -> ClientResult<Job> {
|
||||
let job: Job = self
|
||||
.client
|
||||
.request("job.get", rpc_params![job_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(job)
|
||||
}
|
||||
|
||||
// ========== Auth/API Key Methods ==========
|
||||
|
||||
/// Verify the current API key
|
||||
pub async fn auth_verify(&self) -> ClientResult<AuthVerifyResponse> {
|
||||
let response: AuthVerifyResponse = self
|
||||
.client
|
||||
.request("auth.verify", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Create a new API key (admin only)
|
||||
pub async fn key_create(&self, key: ApiKey) -> ClientResult<()> {
|
||||
let _: () = self
|
||||
.client
|
||||
.request("key.create", rpc_params![key])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a new API key with auto-generated key value (admin only)
|
||||
pub async fn key_generate(&self, params: GenerateApiKeyParams) -> ClientResult<ApiKey> {
|
||||
let api_key: ApiKey = self
|
||||
.client
|
||||
.request("key.generate", rpc_params![params])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(api_key)
|
||||
}
|
||||
|
||||
/// Remove an API key (admin only)
|
||||
pub async fn key_delete(&self, key_id: String) -> ClientResult<()> {
|
||||
let _: () = self
|
||||
.client
|
||||
.request("key.delete", rpc_params![key_id])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all API keys (admin only)
|
||||
pub async fn key_list(&self) -> ClientResult<Vec<ApiKey>> {
|
||||
let keys: Vec<ApiKey> = self
|
||||
.client
|
||||
.request("key.list", rpc_params![])
|
||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||
Ok(keys)
|
||||
}
|
||||
}
|
||||
859
lib/clients/supervisor/src/wasm.rs
Normal file
859
lib/clients/supervisor/src/wasm.rs
Normal file
@@ -0,0 +1,859 @@
|
||||
//! WASM-compatible OpenRPC client for Hero Supervisor
|
||||
//!
|
||||
//! This module provides a WASM-compatible client library for interacting with the Hero Supervisor
|
||||
//! OpenRPC server using browser-native fetch APIs.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{Headers, Request, RequestInit, RequestMode, Response};
|
||||
use serde_json::json;
|
||||
use secp256k1::{Message, PublicKey, Secp256k1, SecretKey, ecdsa::Signature};
|
||||
use sha2::{Sha256, Digest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// WASM-compatible client for communicating with Hero Supervisor OpenRPC server
|
||||
/// Requires authentication secret for all operations
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone)]
|
||||
pub struct WasmSupervisorClient {
|
||||
server_url: String,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
/// Error types for WASM client operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum WasmClientError {
|
||||
#[error("Network error: {0}")]
|
||||
Network(String),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("JavaScript error: {0}")]
|
||||
JavaScript(String),
|
||||
|
||||
#[error("Server error: {message}")]
|
||||
Server { message: String },
|
||||
|
||||
#[error("Invalid response format")]
|
||||
InvalidResponse,
|
||||
}
|
||||
|
||||
/// Result type for WASM client operations
|
||||
pub type WasmClientResult<T> = Result<T, WasmClientError>;
|
||||
|
||||
/// Auth verification response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthVerifyResponse {
|
||||
pub valid: bool,
|
||||
pub name: String,
|
||||
pub scope: String,
|
||||
}
|
||||
|
||||
/// JSON-RPC request structure
|
||||
#[derive(Serialize)]
|
||||
struct JsonRpcRequest {
|
||||
jsonrpc: String,
|
||||
method: String,
|
||||
params: serde_json::Value,
|
||||
id: u32,
|
||||
}
|
||||
|
||||
/// JSON-RPC response structure
|
||||
#[derive(Deserialize)]
|
||||
struct JsonRpcResponse {
|
||||
jsonrpc: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
result: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<JsonRpcError>,
|
||||
id: u32,
|
||||
}
|
||||
|
||||
/// JSON-RPC error structure
|
||||
#[derive(Deserialize)]
|
||||
struct JsonRpcError {
|
||||
code: i32,
|
||||
message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Types of runners supported by the supervisor
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[wasm_bindgen]
|
||||
pub enum WasmRunnerType {
|
||||
SALRunner,
|
||||
OSISRunner,
|
||||
VRunner,
|
||||
}
|
||||
|
||||
/// Job type enumeration that maps to runner types
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[wasm_bindgen]
|
||||
pub enum WasmJobType {
|
||||
SAL,
|
||||
OSIS,
|
||||
V,
|
||||
}
|
||||
|
||||
/// Job status enumeration
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum JobStatus {
|
||||
Pending,
|
||||
Running,
|
||||
Finished,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// Job error type
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum JobError {
|
||||
#[error("Validation error: {0}")]
|
||||
Validation(String),
|
||||
#[error("Execution error: {0}")]
|
||||
Execution(String),
|
||||
#[error("Timeout error")]
|
||||
Timeout,
|
||||
}
|
||||
|
||||
// Re-export JobBuilder from hero-job for convenience
|
||||
pub use hero_job::JobBuilder;
|
||||
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmSupervisorClient {
|
||||
/// Create a new WASM supervisor client with authentication secret
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(server_url: String, secret: String) -> Self {
|
||||
console_log::init_with_level(log::Level::Info).ok();
|
||||
Self {
|
||||
server_url,
|
||||
secret,
|
||||
}
|
||||
}
|
||||
|
||||
/// Alias for new() to maintain backward compatibility
|
||||
#[wasm_bindgen]
|
||||
pub fn with_secret(server_url: String, secret: String) -> Self {
|
||||
Self::new(server_url, secret)
|
||||
}
|
||||
|
||||
/// Get the server URL
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn server_url(&self) -> String {
|
||||
self.server_url.clone()
|
||||
}
|
||||
|
||||
/// Test connection using OpenRPC discovery method
|
||||
pub async fn discover(&self) -> Result<JsValue, JsValue> {
|
||||
let result = self.call_method("rpc.discover", serde_json::Value::Null).await;
|
||||
match result {
|
||||
Ok(value) => Ok(wasm_bindgen::JsValue::from_str(&value.to_string())),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify an API key and return its metadata as JSON
|
||||
/// The key is sent via Authorization header (Bearer token)
|
||||
pub async fn auth_verify(&self, key: String) -> Result<JsValue, JsValue> {
|
||||
// Create a temporary client with the key to verify
|
||||
let temp_client = WasmSupervisorClient::with_secret(self.server_url.clone(), key);
|
||||
|
||||
// Send empty object as params - the key is in the Authorization header
|
||||
let params = serde_json::json!({});
|
||||
|
||||
match temp_client.call_method("auth.verify", params).await {
|
||||
Ok(result) => {
|
||||
// Parse to AuthVerifyResponse to validate, then convert to JsValue
|
||||
let auth_response: AuthVerifyResponse = serde_json::from_value(result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to parse auth response: {}", e)))?;
|
||||
|
||||
// Convert to JsValue
|
||||
serde_wasm_bindgen::to_value(&auth_response)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to convert to JsValue: {}", e)))
|
||||
}
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to verify auth: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the client's stored API key
|
||||
/// Uses the secret that was set when creating the client
|
||||
pub async fn auth_verify_self(&self) -> Result<JsValue, JsValue> {
|
||||
self.auth_verify(self.secret.clone()).await
|
||||
}
|
||||
|
||||
/// Create a new API key (admin only)
|
||||
/// Returns the created API key with its key string
|
||||
pub async fn auth_create_key(&self, name: String, scope: String) -> Result<JsValue, JsValue> {
|
||||
let params = serde_json::json!({
|
||||
"name": name,
|
||||
"scope": scope
|
||||
});
|
||||
|
||||
match self.call_method("auth.create_key", params).await {
|
||||
Ok(result) => Ok(serde_wasm_bindgen::to_value(&result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to convert result: {}", e)))?),
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to create key: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// List all API keys (admin only)
|
||||
pub async fn auth_list_keys(&self) -> Result<JsValue, JsValue> {
|
||||
match self.call_method("auth.list_keys", serde_json::Value::Null).await {
|
||||
Ok(result) => Ok(serde_wasm_bindgen::to_value(&result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to convert result: {}", e)))?),
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to list keys: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove an API key (admin only)
|
||||
pub async fn auth_remove_key(&self, key: String) -> Result<bool, JsValue> {
|
||||
let params = serde_json::json!({
|
||||
"key": key
|
||||
});
|
||||
|
||||
match self.call_method("auth.remove_key", params).await {
|
||||
Ok(result) => {
|
||||
if let Some(success) = result.as_bool() {
|
||||
Ok(success)
|
||||
} else {
|
||||
Err(JsValue::from_str("Invalid response format: expected boolean"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to remove key: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a new runner to the supervisor
|
||||
/// The queue name is automatically set to match the runner name
|
||||
/// Authentication uses the secret from Authorization header (set during client creation)
|
||||
pub async fn register_runner(&self, name: String) -> Result<String, JsValue> {
|
||||
// Secret is sent via Authorization header, not in params
|
||||
let params = serde_json::json!({
|
||||
"name": name
|
||||
});
|
||||
|
||||
match self.call_method("register_runner", params).await {
|
||||
Ok(result) => {
|
||||
// Extract the runner name from the result
|
||||
if let Some(runner) = result.as_str() {
|
||||
Ok(runner.to_string())
|
||||
} else {
|
||||
Err(JsValue::from_str("Invalid response format: expected runner name"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to register runner: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a job (fire-and-forget, non-blocking) - DEPRECATED: Use create_job with API key auth
|
||||
#[wasm_bindgen]
|
||||
pub async fn create_job_with_secret(&self, secret: String, job: hero_job::Job) -> Result<String, JsValue> {
|
||||
// Backend expects RunJobParams struct with secret and job fields - wrap in array like register_runner
|
||||
let params = serde_json::json!([{
|
||||
"secret": secret,
|
||||
"job": {
|
||||
"id": job.id,
|
||||
"caller_id": job.caller_id,
|
||||
"context_id": job.context_id,
|
||||
"payload": job.payload,
|
||||
"runner": job.runner,
|
||||
"executor": job.executor,
|
||||
"timeout": job.timeout,
|
||||
"env_vars": serde_json::from_str::<serde_json::Value>(&serde_json::to_string(&job.env_vars).unwrap_or_else(|_| "{}".to_string())).unwrap_or(serde_json::json!({})),
|
||||
"created_at": job.created_at,
|
||||
"updated_at": job.updated_at
|
||||
}
|
||||
}]);
|
||||
|
||||
match self.call_method("create_job", params).await {
|
||||
Ok(result) => {
|
||||
if let Some(job_id) = result.as_str() {
|
||||
Ok(job_id.to_string())
|
||||
} else {
|
||||
Ok(result.to_string())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to create job: {:?}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a job on a specific runner (blocking, returns result)
|
||||
#[wasm_bindgen]
|
||||
pub async fn run_job(&self, secret: String, job: hero_job::Job) -> Result<String, JsValue> {
|
||||
// Backend expects RunJobParams struct with secret and job fields - wrap in array like register_runner
|
||||
let params = serde_json::json!([{
|
||||
"secret": secret,
|
||||
"job": {
|
||||
"id": job.id,
|
||||
"caller_id": job.caller_id,
|
||||
"context_id": job.context_id,
|
||||
"payload": job.payload,
|
||||
"runner": job.runner,
|
||||
"executor": job.executor,
|
||||
"timeout": job.timeout,
|
||||
"env_vars": serde_json::from_str::<serde_json::Value>(&serde_json::to_string(&job.env_vars).unwrap_or_else(|_| "{}".to_string())).unwrap_or(serde_json::json!({})),
|
||||
"created_at": job.created_at,
|
||||
"updated_at": job.updated_at
|
||||
}
|
||||
}]);
|
||||
|
||||
match self.call_method("job.run", params).await {
|
||||
Ok(result) => {
|
||||
if let Some(result_str) = result.as_str() {
|
||||
Ok(result_str.to_string())
|
||||
} else {
|
||||
Ok(result.to_string())
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// List all runner IDs
|
||||
pub async fn list_runners(&self) -> Result<Vec<String>, JsValue> {
|
||||
match self.call_method("list_runners", serde_json::Value::Null).await {
|
||||
Ok(result) => {
|
||||
if let Ok(runners) = serde_json::from_value::<Vec<String>>(result) {
|
||||
Ok(runners)
|
||||
} else {
|
||||
Err(JsValue::from_str("Failed to parse runners list"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to list runners: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get status of all runners
|
||||
pub async fn get_all_runner_status(&self) -> Result<JsValue, JsValue> {
|
||||
match self.call_method("get_all_runner_status", serde_json::Value::Null).await {
|
||||
Ok(result) => {
|
||||
// Convert serde_json::Value to JsValue
|
||||
Ok(serde_wasm_bindgen::to_value(&result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to convert result: {}", e)))?)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to get runner statuses: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a job from a JsValue (full Job object)
|
||||
pub async fn create_job(&self, job: JsValue) -> Result<String, JsValue> {
|
||||
// Convert JsValue to serde_json::Value
|
||||
let job_value: serde_json::Value = serde_wasm_bindgen::from_value(job)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to parse job: {}", e)))?;
|
||||
|
||||
// Wrap in RunJobParams structure and pass as positional parameter
|
||||
let params = serde_json::json!([{
|
||||
"job": job_value
|
||||
}]);
|
||||
|
||||
match self.call_method("jobs.create", params).await {
|
||||
Ok(result) => {
|
||||
if let Some(job_id) = result.as_str() {
|
||||
Ok(job_id.to_string())
|
||||
} else {
|
||||
Err(JsValue::from_str("Invalid response format: expected job ID"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to create job: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a job with basic parameters (simplified version)
|
||||
pub async fn create_simple_job(
|
||||
&self,
|
||||
runner: String,
|
||||
caller_id: String,
|
||||
context_id: String,
|
||||
payload: String,
|
||||
executor: String,
|
||||
) -> Result<String, JsValue> {
|
||||
// Generate a unique job ID
|
||||
let job_id = format!("job-{}", uuid::Uuid::new_v4());
|
||||
|
||||
let job = serde_json::json!({
|
||||
"id": job_id,
|
||||
"runner": runner,
|
||||
"caller_id": caller_id,
|
||||
"context_id": context_id,
|
||||
"payload": payload,
|
||||
"executor": executor,
|
||||
"timeout": 30,
|
||||
"env": {}
|
||||
});
|
||||
|
||||
let params = serde_json::json!({
|
||||
"job": job
|
||||
});
|
||||
|
||||
match self.call_method("jobs.create", params).await {
|
||||
Ok(result) => {
|
||||
if let Some(job_id) = result.as_str() {
|
||||
Ok(job_id.to_string())
|
||||
} else {
|
||||
Err(JsValue::from_str("Invalid response format: expected job ID"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to create job: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// List all jobs
|
||||
pub async fn list_jobs(&self) -> Result<JsValue, JsValue> {
|
||||
match self.call_method("jobs.list", serde_json::Value::Null).await {
|
||||
Ok(result) => {
|
||||
// Convert serde_json::Value to JsValue
|
||||
serde_wasm_bindgen::to_value(&result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to convert jobs list: {}", e)))
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a job by job ID
|
||||
pub async fn get_job(&self, job_id: &str) -> Result<hero_job::Job, JsValue> {
|
||||
let params = serde_json::json!([job_id]);
|
||||
match self.call_method("get_job", params).await {
|
||||
Ok(result) => {
|
||||
// Convert the Job result to hero_job::Job
|
||||
if let Ok(job_value) = serde_json::from_value::<serde_json::Value>(result) {
|
||||
// Extract fields from the job
|
||||
let id = job_value.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let caller_id = job_value.get("caller_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let context_id = job_value.get("context_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let payload = job_value.get("payload").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let runner = job_value.get("runner").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let executor = job_value.get("executor").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let timeout_secs = job_value.get("timeout").and_then(|v| v.get("secs")).and_then(|v| v.as_u64()).unwrap_or(30);
|
||||
let env_vars = job_value.get("env_vars").map(|v| v.to_string()).unwrap_or_else(|| "{}".to_string());
|
||||
let created_at = job_value.get("created_at").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let updated_at = job_value.get("updated_at").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
|
||||
Ok(hero_job::Job {
|
||||
id,
|
||||
caller_id,
|
||||
context_id,
|
||||
payload,
|
||||
runner,
|
||||
executor,
|
||||
timeout: timeout_secs,
|
||||
env_vars: serde_json::from_str(&env_vars).unwrap_or_default(),
|
||||
created_at: chrono::DateTime::parse_from_rfc3339(&created_at)
|
||||
.map(|dt| dt.with_timezone(&chrono::Utc))
|
||||
.unwrap_or_else(|_| chrono::Utc::now()),
|
||||
updated_at: chrono::DateTime::parse_from_rfc3339(&updated_at)
|
||||
.map(|dt| dt.with_timezone(&chrono::Utc))
|
||||
.unwrap_or_else(|_| chrono::Utc::now()),
|
||||
signatures: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
Err(JsValue::from_str("Invalid response format for get_job"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ping a runner by dispatching a ping job to its queue
|
||||
#[wasm_bindgen]
|
||||
pub async fn ping_runner(&self, runner_id: &str) -> Result<String, JsValue> {
|
||||
let params = serde_json::json!([runner_id]);
|
||||
|
||||
match self.call_method("ping_runner", params).await {
|
||||
Ok(result) => {
|
||||
if let Some(job_id) = result.as_str() {
|
||||
Ok(job_id.to_string())
|
||||
} else {
|
||||
Ok(result.to_string())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to ping runner: {:?}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop a job by ID
|
||||
#[wasm_bindgen]
|
||||
pub async fn stop_job(&self, job_id: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([job_id]);
|
||||
|
||||
match self.call_method("stop_job", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to stop job: {:?}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a job by ID
|
||||
#[wasm_bindgen]
|
||||
pub async fn delete_job(&self, job_id: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([{
|
||||
"job_id": job_id
|
||||
}]);
|
||||
|
||||
match self.call_method("job.delete", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to delete job: {:?}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get logs for a specific job
|
||||
#[wasm_bindgen]
|
||||
pub async fn get_job_logs(&self, job_id: &str, lines: Option<usize>) -> Result<JsValue, JsValue> {
|
||||
let params = if let Some(n) = lines {
|
||||
serde_json::json!([job_id, n])
|
||||
} else {
|
||||
serde_json::json!([job_id, serde_json::Value::Null])
|
||||
};
|
||||
|
||||
match self.call_method("get_job_logs", params).await {
|
||||
Ok(result) => {
|
||||
// Convert Vec<String> to JsValue
|
||||
Ok(serde_wasm_bindgen::to_value(&result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to convert logs: {}", e)))?)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to get job logs: {:?}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a runner from the supervisor
|
||||
pub async fn remove_runner(&self, actor_id: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([actor_id]);
|
||||
match self.call_method("remove_runner", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a specific runner
|
||||
pub async fn start_runner(&self, actor_id: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([actor_id]);
|
||||
match self.call_method("start_runner", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop a specific runner
|
||||
pub async fn stop_runner(&self, actor_id: &str, force: bool) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([actor_id, force]);
|
||||
self.call_method("stop_runner", params)
|
||||
.await
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a specific runner by ID
|
||||
pub async fn get_runner(&self, actor_id: &str) -> Result<JsValue, JsValue> {
|
||||
let params = serde_json::json!([actor_id]);
|
||||
let result = self.call_method("get_runner", params)
|
||||
.await
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
// Convert the serde_json::Value to a JsValue via string serialization
|
||||
let json_string = serde_json::to_string(&result)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
Ok(js_sys::JSON::parse(&json_string)
|
||||
.map_err(|e| JsValue::from_str("Failed to parse JSON"))?)
|
||||
}
|
||||
|
||||
/// Add a secret to the supervisor
|
||||
pub async fn add_secret(&self, admin_secret: &str, secret_type: &str, secret_value: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([{
|
||||
"admin_secret": admin_secret,
|
||||
"secret_type": secret_type,
|
||||
"secret_value": secret_value
|
||||
}]);
|
||||
match self.call_method("add_secret", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a secret from the supervisor
|
||||
pub async fn remove_secret(&self, admin_secret: &str, secret_type: &str, secret_value: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([{
|
||||
"admin_secret": admin_secret,
|
||||
"secret_type": secret_type,
|
||||
"secret_value": secret_value
|
||||
}]);
|
||||
match self.call_method("remove_secret", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// List secrets (returns supervisor info including secret counts)
|
||||
pub async fn list_secrets(&self, admin_secret: &str) -> Result<JsValue, JsValue> {
|
||||
let params = serde_json::json!([{
|
||||
"admin_secret": admin_secret
|
||||
}]);
|
||||
match self.call_method("list_secrets", params).await {
|
||||
Ok(result) => {
|
||||
// Convert serde_json::Value to JsValue
|
||||
let result_str = serde_json::to_string(&result)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
Ok(js_sys::JSON::parse(&result_str)
|
||||
.map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get supervisor information including secret counts
|
||||
pub async fn get_supervisor_info(&self, admin_secret: &str) -> Result<JsValue, JsValue> {
|
||||
let params = serde_json::json!({
|
||||
"admin_secret": admin_secret
|
||||
});
|
||||
|
||||
match self.call_method("get_supervisor_info", params).await {
|
||||
Ok(result) => {
|
||||
let result_str = serde_json::to_string(&result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Serialization error: {:?}", e)))?;
|
||||
Ok(js_sys::JSON::parse(&result_str)
|
||||
.map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to get supervisor info: {:?}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// List admin secrets (returns actual secret values)
|
||||
pub async fn list_admin_secrets(&self, admin_secret: &str) -> Result<Vec<String>, JsValue> {
|
||||
let params = serde_json::json!({
|
||||
"admin_secret": admin_secret
|
||||
});
|
||||
|
||||
match self.call_method("list_admin_secrets", params).await {
|
||||
Ok(result) => {
|
||||
let secrets: Vec<String> = serde_json::from_value(result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to parse admin secrets: {:?}", e)))?;
|
||||
Ok(secrets)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to list admin secrets: {:?}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// List user secrets (returns actual secret values)
|
||||
pub async fn list_user_secrets(&self, admin_secret: &str) -> Result<Vec<String>, JsValue> {
|
||||
let params = serde_json::json!({
|
||||
"admin_secret": admin_secret
|
||||
});
|
||||
|
||||
match self.call_method("list_user_secrets", params).await {
|
||||
Ok(result) => {
|
||||
let secrets: Vec<String> = serde_json::from_value(result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to parse user secrets: {:?}", e)))?;
|
||||
Ok(secrets)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to list user secrets: {:?}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// List register secrets (returns actual secret values)
|
||||
pub async fn list_register_secrets(&self, admin_secret: &str) -> Result<Vec<String>, JsValue> {
|
||||
let params = serde_json::json!({
|
||||
"admin_secret": admin_secret
|
||||
});
|
||||
|
||||
match self.call_method("list_register_secrets", params).await {
|
||||
Ok(result) => {
|
||||
let secrets: Vec<String> = serde_json::from_value(result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to parse register secrets: {:?}", e)))?;
|
||||
Ok(secrets)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to list register secrets: {:?}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a previously created job by queuing it to its assigned runner
|
||||
pub async fn start_job(&self, job_id: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([{
|
||||
"job_id": job_id
|
||||
}]);
|
||||
|
||||
match self.call_method("job.start", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the status of a job
|
||||
pub async fn get_job_status(&self, job_id: &str) -> Result<JsValue, JsValue> {
|
||||
let params = serde_json::json!([job_id]);
|
||||
|
||||
match self.call_method("job.status", params).await {
|
||||
Ok(result) => serde_wasm_bindgen::to_value(&result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Serialization error: {:?}", e))),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the result of a completed job
|
||||
pub async fn get_job_result(&self, job_id: &str) -> Result<JsValue, JsValue> {
|
||||
let params = serde_json::json!([job_id]);
|
||||
|
||||
match self.call_method("job.result", params).await {
|
||||
Ok(result) => serde_wasm_bindgen::to_value(&result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Serialization error: {:?}", e))),
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal method to make JSON-RPC calls
|
||||
async fn call_method(&self, method: &str, params: serde_json::Value) -> WasmClientResult<serde_json::Value> {
|
||||
let request = JsonRpcRequest {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
method: method.to_string(),
|
||||
params,
|
||||
id: 1,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&request)?;
|
||||
|
||||
// Create headers
|
||||
let headers = Headers::new().map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
|
||||
headers.set("Content-Type", "application/json")
|
||||
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
|
||||
|
||||
// Add Authorization header with secret
|
||||
let auth_value = format!("Bearer {}", self.secret);
|
||||
headers.set("Authorization", &auth_value)
|
||||
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
|
||||
|
||||
// Create request init
|
||||
let opts = RequestInit::new();
|
||||
opts.set_method("POST");
|
||||
opts.set_headers(&headers);
|
||||
opts.set_body(&JsValue::from_str(&body));
|
||||
opts.set_mode(RequestMode::Cors);
|
||||
|
||||
// Create request
|
||||
let request = Request::new_with_str_and_init(&self.server_url, &opts)
|
||||
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
|
||||
|
||||
// Get window and fetch
|
||||
let window = web_sys::window().ok_or_else(|| WasmClientError::JavaScript("No window object".to_string()))?;
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await
|
||||
.map_err(|e| WasmClientError::Network(format!("{:?}", e)))?;
|
||||
|
||||
// Convert to Response
|
||||
let resp: Response = resp_value.dyn_into()
|
||||
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
|
||||
|
||||
// Check if response is ok
|
||||
if !resp.ok() {
|
||||
return Err(WasmClientError::Network(format!("HTTP {}: {}", resp.status(), resp.status_text())));
|
||||
}
|
||||
|
||||
// Get response text
|
||||
let text_promise = resp.text()
|
||||
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
|
||||
let text_value = JsFuture::from(text_promise).await
|
||||
.map_err(|e| WasmClientError::Network(format!("{:?}", e)))?;
|
||||
let text = text_value.as_string()
|
||||
.ok_or_else(|| WasmClientError::InvalidResponse)?;
|
||||
|
||||
// Parse JSON-RPC response
|
||||
let response: JsonRpcResponse = serde_json::from_str(&text)?;
|
||||
|
||||
if let Some(error) = response.error {
|
||||
return Err(WasmClientError::Server {
|
||||
message: format!("{}: {}", error.code, error.message),
|
||||
});
|
||||
}
|
||||
|
||||
// For void methods, null result is valid
|
||||
Ok(response.result.unwrap_or(serde_json::Value::Null))
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the WASM client library (call manually if needed)
|
||||
pub fn init() {
|
||||
console_log::init_with_level(log::Level::Info).ok();
|
||||
log::info!("Hero Supervisor WASM OpenRPC Client initialized");
|
||||
}
|
||||
|
||||
/// Utility function to create a client from JavaScript
|
||||
#[wasm_bindgen]
|
||||
pub fn create_client(server_url: String, secret: String) -> WasmSupervisorClient {
|
||||
WasmSupervisorClient::new(server_url, secret)
|
||||
}
|
||||
|
||||
/// Sign a job's canonical representation with a private key
|
||||
/// Returns a tuple of (public_key_hex, signature_hex)
|
||||
#[wasm_bindgen]
|
||||
pub fn sign_job_canonical(
|
||||
canonical_repr: String,
|
||||
private_key_hex: String,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
// Decode private key from hex
|
||||
let secret_bytes = hex::decode(&private_key_hex)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid private key hex: {}", e)))?;
|
||||
|
||||
let secret_key = SecretKey::from_slice(&secret_bytes)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid private key: {}", e)))?;
|
||||
|
||||
// Get the public key (uncompressed format)
|
||||
let secp = Secp256k1::new();
|
||||
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
|
||||
let public_key_hex = hex::encode(public_key.serialize_uncompressed());
|
||||
|
||||
// Hash the canonical representation
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(canonical_repr.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
|
||||
// Create message from hash
|
||||
let message = Message::from_digest_slice(&hash)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid message: {}", e)))?;
|
||||
|
||||
// Sign the message
|
||||
let signature = secp.sign_ecdsa(&message, &secret_key);
|
||||
let signature_hex = hex::encode(signature.serialize_compact());
|
||||
|
||||
// Return as JS object
|
||||
let result = serde_json::json!({
|
||||
"public_key": public_key_hex,
|
||||
"signature": signature_hex
|
||||
});
|
||||
|
||||
serde_wasm_bindgen::to_value(&result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to serialize result: {}", e)))
|
||||
}
|
||||
|
||||
/// Create canonical representation of a job for signing
|
||||
/// This matches the format used in runner_rust Job::canonical_representation
|
||||
#[wasm_bindgen]
|
||||
pub fn create_job_canonical_repr(
|
||||
id: String,
|
||||
caller_id: String,
|
||||
context_id: String,
|
||||
payload: String,
|
||||
runner: String,
|
||||
executor: String,
|
||||
timeout: u64,
|
||||
env_vars_json: String,
|
||||
) -> Result<String, JsValue> {
|
||||
// Parse env_vars from JSON
|
||||
let env_vars: std::collections::HashMap<String, String> = serde_json::from_str(&env_vars_json)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid env_vars JSON: {}", e)))?;
|
||||
|
||||
// Sort env_vars keys for deterministic ordering
|
||||
let mut env_vars_sorted: Vec<_> = env_vars.iter().collect();
|
||||
env_vars_sorted.sort_by_key(|&(k, _)| k);
|
||||
|
||||
// Create canonical representation (matches Job::canonical_representation in runner_rust)
|
||||
let canonical = format!(
|
||||
"{}:{}:{}:{}:{}:{}:{}:{:?}",
|
||||
id,
|
||||
caller_id,
|
||||
context_id,
|
||||
payload,
|
||||
runner,
|
||||
executor,
|
||||
timeout,
|
||||
env_vars_sorted
|
||||
);
|
||||
|
||||
Ok(canonical)
|
||||
}
|
||||
Reference in New Issue
Block a user