Implement comprehensive admin UI with job management and API key display
Admin UI Features:
- Complete job lifecycle: create, run, view status, view output, delete
- Job table with sorting, filtering, and real-time status updates
- Status polling with countdown timers for running jobs
- Job output modal with result/error display
- API keys management: create keys, list keys with secrets visible
- Sidebar toggle between runners and keys views
- Toast notifications for errors
- Modern dark theme UI with responsive design
Supervisor Improvements:
- Fixed job status persistence using client methods
- Refactored get_job_result to use client.get_status, get_result, get_error
- Changed runner_rust dependency from git to local path
- Authentication system with API key scopes (admin, user, register)
- Job listing with status fetching from Redis
- Services module for job and auth operations
OpenRPC Client:
- Added auth_list_keys method for fetching API keys
- WASM bindings for browser usage
- Proper error handling and type conversions
Build Status: ✅ All components build successfully
This commit is contained in:
267
clients/openrpc/Cargo.lock
generated
267
clients/openrpc/Cargo.lock
generated
@@ -666,22 +666,6 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||
dependencies = [
|
||||
"bitflags 2.9.3",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
@@ -690,7 +674,7 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags 2.9.3",
|
||||
"crossterm_winapi",
|
||||
"mio 1.0.4",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix 0.38.44",
|
||||
"signal-hook",
|
||||
@@ -822,6 +806,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1405,16 +1390,15 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clap",
|
||||
"env_logger 0.10.2",
|
||||
"hyper 1.7.0",
|
||||
"hyper-util",
|
||||
"jsonrpsee",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"redis 0.25.4",
|
||||
"reqwest 0.12.23",
|
||||
"runner_rust",
|
||||
"runner_rust 0.1.0 (git+https://git.ourworld.tf/herocode/runner_rust.git)",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
@@ -1430,19 +1414,21 @@ name = "hero-supervisor-openrpc-client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"console_log",
|
||||
"crossterm 0.27.0",
|
||||
"env_logger 0.11.8",
|
||||
"getrandom 0.2.16",
|
||||
"hero-supervisor",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"js-sys",
|
||||
"jsonrpsee",
|
||||
"log",
|
||||
"ratatui",
|
||||
"runner_rust",
|
||||
"runner_rust 0.1.0 (git+https://git.ourworld.tf/herocode/runner_rust.git?branch=main)",
|
||||
"secp256k1 0.29.1",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
@@ -1514,6 +1500,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
@@ -2499,18 +2491,6 @@ dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.4"
|
||||
@@ -2717,6 +2697,36 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osiris"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.ourworld.tf/herocode/osiris.git#097360ad12d2ea73ac4d38552889d97702d9a889"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"env_logger 0.10.2",
|
||||
"osiris_derive",
|
||||
"redis 0.24.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osiris_derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.ourworld.tf/herocode/osiris.git#097360ad12d2ea73ac4d38552889d97702d9a889"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ourdb"
|
||||
version = "0.1.0"
|
||||
@@ -2922,19 +2932,6 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
@@ -3097,15 +3094,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
@@ -3216,7 +3204,7 @@ dependencies = [
|
||||
"bitflags 2.9.3",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm 0.28.1",
|
||||
"crossterm",
|
||||
"instability",
|
||||
"itertools",
|
||||
"lru",
|
||||
@@ -3237,6 +3225,27 @@ dependencies = [
|
||||
"bitflags 2.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c580d9cbbe1d1b479e8d67cf9daf6a62c957e6846048408b80b43ac3f6af84cd"
|
||||
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"
|
||||
@@ -3565,18 +3574,22 @@ checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
|
||||
[[package]]
|
||||
name = "runner_rust"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.ourworld.tf/herocode/runner_rust.git?branch=main#268128f7fd53e9586288efd95f9288595c4a74e9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"clap",
|
||||
"crossterm 0.28.1",
|
||||
"crossterm",
|
||||
"env_logger 0.10.2",
|
||||
"hero_logger",
|
||||
"heromodels",
|
||||
"heromodels-derive",
|
||||
"heromodels_core",
|
||||
"hex",
|
||||
"log",
|
||||
"osiris",
|
||||
"rand 0.8.5",
|
||||
"ratatui",
|
||||
"redis 0.25.4",
|
||||
"rhai",
|
||||
@@ -3590,13 +3603,61 @@ dependencies = [
|
||||
"sal-postgresclient",
|
||||
"sal-process",
|
||||
"sal-redisclient",
|
||||
"sal-service-manager",
|
||||
"sal-text",
|
||||
"sal-vault",
|
||||
"sal-virt",
|
||||
"sal-zinit-client",
|
||||
"secp256k1 0.28.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "runner_rust"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.ourworld.tf/herocode/runner_rust.git#268128f7fd53e9586288efd95f9288595c4a74e9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"clap",
|
||||
"crossterm",
|
||||
"env_logger 0.10.2",
|
||||
"hero_logger",
|
||||
"heromodels",
|
||||
"heromodels-derive",
|
||||
"heromodels_core",
|
||||
"hex",
|
||||
"log",
|
||||
"osiris",
|
||||
"rand 0.8.5",
|
||||
"ratatui",
|
||||
"redis 0.25.4",
|
||||
"rhai",
|
||||
"rhailib_dsl",
|
||||
"sal-git",
|
||||
"sal-hetzner",
|
||||
"sal-kubernetes",
|
||||
"sal-mycelium",
|
||||
"sal-net",
|
||||
"sal-os",
|
||||
"sal-postgresclient",
|
||||
"sal-process",
|
||||
"sal-redisclient",
|
||||
"sal-text",
|
||||
"sal-vault",
|
||||
"sal-virt",
|
||||
"sal-zinit-client",
|
||||
"secp256k1 0.28.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"toml",
|
||||
@@ -3906,24 +3967,6 @@ dependencies = [
|
||||
"rhai",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sal-service-manager"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.ourworld.tf/herocode/herolib_rust.git#7afa5ea1c0d9bb240fd2a96e5a0a01b04372fa1c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"futures",
|
||||
"log",
|
||||
"once_cell",
|
||||
"plist",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"zinit-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sal-text"
|
||||
version = "0.1.0"
|
||||
@@ -4059,6 +4102,44 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
|
||||
dependencies = [
|
||||
"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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1-sys"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secrecy"
|
||||
version = "0.8.0"
|
||||
@@ -4124,6 +4205,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-wasm-bindgen"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
@@ -4253,8 +4345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"mio 1.0.4",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
@@ -4326,6 +4417,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"
|
||||
@@ -4727,7 +4828,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio 1.0.4",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
|
||||
@@ -34,6 +34,7 @@ env_logger = "0.11"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
serde-wasm-bindgen = "0.6"
|
||||
web-sys = { version = "0.3", features = [
|
||||
"console",
|
||||
"Request",
|
||||
@@ -45,6 +46,10 @@ web-sys = { version = "0.3", features = [
|
||||
] }
|
||||
console_log = "1.0"
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
# Crypto for signing
|
||||
secp256k1 = { version = "0.29", features = ["rand", "global-context"] }
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
@@ -39,7 +39,7 @@ pub mod wasm;
|
||||
|
||||
// Re-export WASM types for convenience
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm::{WasmSupervisorClient, WasmJob, WasmJobType, WasmRunnerType};
|
||||
pub use wasm::{WasmSupervisorClient, WasmJob, WasmJobType, WasmRunnerType, create_job_canonical_repr, sign_job_canonical};
|
||||
|
||||
// Native client dependencies
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -57,6 +57,7 @@ use std::path::PathBuf;
|
||||
pub struct SupervisorClient {
|
||||
client: HttpClient,
|
||||
server_url: String,
|
||||
secret: Option<String>,
|
||||
}
|
||||
|
||||
/// Error types for client operations
|
||||
@@ -225,6 +226,24 @@ impl SupervisorClient {
|
||||
Ok(Self {
|
||||
client,
|
||||
server_url,
|
||||
secret: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new supervisor client with authentication secret
|
||||
pub fn with_secret(server_url: impl Into<String>, secret: impl Into<String>) -> ClientResult<Self> {
|
||||
let server_url = server_url.into();
|
||||
let secret = secret.into();
|
||||
|
||||
let client = HttpClientBuilder::default()
|
||||
.request_timeout(std::time::Duration::from_secs(30))
|
||||
.build(&server_url)
|
||||
.map_err(|e| ClientError::Http(e.to_string()))?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
server_url,
|
||||
secret: Some(secret),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,20 @@
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{Request, RequestInit, RequestMode, Response, Headers};
|
||||
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
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone)]
|
||||
pub struct WasmSupervisorClient {
|
||||
server_url: String,
|
||||
secret: Option<String>,
|
||||
}
|
||||
|
||||
/// Error types for WASM client operations
|
||||
@@ -38,6 +43,14 @@ pub enum WasmClientError {
|
||||
/// 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 {
|
||||
@@ -199,11 +212,24 @@ pub struct WasmJob {
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmSupervisorClient {
|
||||
/// Create a new WASM supervisor client
|
||||
/// Create a new WASM supervisor client without authentication
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(server_url: String) -> Self {
|
||||
console_log::init_with_level(log::Level::Info).ok();
|
||||
Self { server_url }
|
||||
Self {
|
||||
server_url,
|
||||
secret: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new WASM supervisor client with authentication secret
|
||||
#[wasm_bindgen]
|
||||
pub fn with_secret(server_url: String, secret: String) -> Self {
|
||||
console_log::init_with_level(log::Level::Info).ok();
|
||||
Self {
|
||||
server_url,
|
||||
secret: Some(secret),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the server URL
|
||||
@@ -221,13 +247,88 @@ impl WasmSupervisorClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a new runner to the supervisor with secret authentication
|
||||
pub async fn register_runner(&self, secret: &str, name: &str, queue: &str) -> Result<String, JsValue> {
|
||||
let params = serde_json::json!([{
|
||||
"secret": secret,
|
||||
/// 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 with with_secret()
|
||||
pub async fn auth_verify_self(&self) -> Result<JsValue, JsValue> {
|
||||
let key = self.secret.as_ref()
|
||||
.ok_or_else(|| JsValue::from_str("Client not authenticated - use with_secret() to create authenticated client"))?;
|
||||
|
||||
self.auth_verify(key.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,
|
||||
"queue": queue
|
||||
}]);
|
||||
"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) => {
|
||||
@@ -238,13 +339,13 @@ impl WasmSupervisorClient {
|
||||
Err(JsValue::from_str("Invalid response format: expected runner name"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to register runner: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a job (fire-and-forget, non-blocking)
|
||||
/// Create a job (fire-and-forget, non-blocking) - DEPRECATED: Use create_job with API key auth
|
||||
#[wasm_bindgen]
|
||||
pub async fn create_job(&self, secret: String, job: WasmJob) -> Result<String, JsValue> {
|
||||
pub async fn create_job_with_secret(&self, secret: String, job: WasmJob) -> 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,
|
||||
@@ -326,17 +427,65 @@ impl WasmSupervisorClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// List all job IDs from Redis
|
||||
pub async fn list_jobs(&self) -> Result<Vec<String>, JsValue> {
|
||||
match self.call_method("jobs.list", serde_json::Value::Null).await {
|
||||
/// 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 Ok(jobs) = serde_json::from_value::<Vec<String>>(result) {
|
||||
Ok(jobs)
|
||||
if let Some(job_id) = result.as_str() {
|
||||
Ok(job_id.to_string())
|
||||
} else {
|
||||
Err(JsValue::from_str("Invalid response format for list_jobs"))
|
||||
Err(JsValue::from_str("Invalid response format: expected job ID"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
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))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,9 +559,11 @@ impl WasmSupervisorClient {
|
||||
/// 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]);
|
||||
let params = serde_json::json!([{
|
||||
"job_id": job_id
|
||||
}]);
|
||||
|
||||
match self.call_method("delete_job", params).await {
|
||||
match self.call_method("job.delete", params).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to delete job: {:?}", e)))
|
||||
}
|
||||
@@ -679,6 +830,54 @@ impl WasmJob {
|
||||
}
|
||||
|
||||
impl WasmSupervisorClient {
|
||||
/// List all jobs (returns full job objects as Vec<serde_json::Value>)
|
||||
/// This is not exposed to WASM directly due to type limitations
|
||||
pub async fn list_jobs(&self) -> Result<Vec<serde_json::Value>, JsValue> {
|
||||
let params = serde_json::json!([]);
|
||||
match self.call_method("jobs.list", params).await {
|
||||
Ok(result) => {
|
||||
if let Ok(jobs) = serde_json::from_value::<Vec<serde_json::Value>>(result) {
|
||||
Ok(jobs)
|
||||
} else {
|
||||
Err(JsValue::from_str("Invalid response format for jobs.list"))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<serde_json::Value, JsValue> {
|
||||
let params = serde_json::json!([job_id]);
|
||||
|
||||
match self.call_method("job.status", params).await {
|
||||
Ok(result) => Ok(result),
|
||||
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<serde_json::Value, JsValue> {
|
||||
let params = serde_json::json!([job_id]);
|
||||
|
||||
match self.call_method("job.result", params).await {
|
||||
Ok(result) => Ok(result),
|
||||
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 {
|
||||
@@ -694,6 +893,12 @@ impl WasmSupervisorClient {
|
||||
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 if secret is present
|
||||
if let Some(secret) = &self.secret {
|
||||
headers.set("Authorization", &format!("Bearer {}", secret))
|
||||
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
|
||||
}
|
||||
|
||||
// Create request init
|
||||
let opts = RequestInit::new();
|
||||
@@ -760,3 +965,82 @@ pub fn create_job(id: String, payload: String, executor: String, runner: String)
|
||||
pub fn create_client(server_url: String) -> WasmSupervisorClient {
|
||||
WasmSupervisorClient::new(server_url)
|
||||
}
|
||||
|
||||
/// 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
|
||||
let secp = Secp256k1::new();
|
||||
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
|
||||
let public_key_hex = hex::encode(public_key.serialize());
|
||||
|
||||
// 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