chore: seed sgclaw rust baseline
This commit is contained in:
37
src/lib.rs
Normal file
37
src/lib.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
pub mod pipe;
|
||||
pub mod security;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use pipe::{perform_handshake, BrowserPipeTool, PipeError, StdioTransport, Transport};
|
||||
use security::MacPolicy;
|
||||
|
||||
fn default_rules_path() -> PathBuf {
|
||||
std::env::current_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.join("resources")
|
||||
.join("rules.json")
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), PipeError> {
|
||||
let transport = Arc::new(StdioTransport::new(std::io::stdin(), std::io::stdout()));
|
||||
let handshake = perform_handshake(transport.as_ref(), Duration::from_secs(5))?;
|
||||
let mac_policy = MacPolicy::load_from_path(default_rules_path())?;
|
||||
let _browser_tool = BrowserPipeTool::new(transport.clone(), mac_policy, handshake.session_key)
|
||||
.with_response_timeout(Duration::from_secs(30));
|
||||
|
||||
eprintln!("sgclaw ready: agent_id={}", handshake.agent_id);
|
||||
|
||||
loop {
|
||||
match transport.recv_timeout(Duration::from_secs(3600)) {
|
||||
Ok(message) => {
|
||||
eprintln!("ignoring unsolicited browser message: {:?}", message);
|
||||
}
|
||||
Err(PipeError::Timeout) => continue,
|
||||
Err(PipeError::PipeClosed) => return Ok(()),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/main.rs
Normal file
10
src/main.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
if let Err(err) = sgclaw::run() {
|
||||
eprintln!("sgclaw failed: {err}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
103
src/pipe/browser_tool.rs
Normal file
103
src/pipe/browser_tool.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::pipe::protocol::{Action, AgentMessage, BrowserMessage, SecurityFields, Timing};
|
||||
use crate::pipe::{PipeError, Transport};
|
||||
use crate::security::{sign_command, MacPolicy};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CommandOutput {
|
||||
pub seq: u64,
|
||||
pub success: bool,
|
||||
pub data: Value,
|
||||
pub aom_snapshot: Vec<Value>,
|
||||
pub timing: Timing,
|
||||
}
|
||||
|
||||
pub struct BrowserPipeTool<T: Transport> {
|
||||
transport: Arc<T>,
|
||||
mac_policy: MacPolicy,
|
||||
session_key: Vec<u8>,
|
||||
next_seq: AtomicU64,
|
||||
response_timeout: Duration,
|
||||
}
|
||||
|
||||
impl<T: Transport> BrowserPipeTool<T> {
|
||||
pub fn new(transport: Arc<T>, mac_policy: MacPolicy, session_key: Vec<u8>) -> Self {
|
||||
Self {
|
||||
transport,
|
||||
mac_policy,
|
||||
session_key,
|
||||
next_seq: AtomicU64::new(1),
|
||||
response_timeout: Duration::from_secs(30),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_response_timeout(mut self, response_timeout: Duration) -> Self {
|
||||
self.response_timeout = response_timeout;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn invoke(
|
||||
&self,
|
||||
action: Action,
|
||||
params: Value,
|
||||
expected_domain: &str,
|
||||
) -> Result<CommandOutput, PipeError> {
|
||||
self.mac_policy.validate(&action, expected_domain)?;
|
||||
|
||||
let seq = self.next_seq.fetch_add(1, Ordering::Relaxed);
|
||||
let hmac = sign_command(&self.session_key, seq, &action, ¶ms, expected_domain)?;
|
||||
let command = AgentMessage::Command {
|
||||
seq,
|
||||
action,
|
||||
params,
|
||||
security: SecurityFields {
|
||||
expected_domain: expected_domain.to_string(),
|
||||
hmac,
|
||||
},
|
||||
};
|
||||
|
||||
self.transport.send(&command)?;
|
||||
|
||||
let started = Instant::now();
|
||||
loop {
|
||||
let Some(remaining) = self.response_timeout.checked_sub(started.elapsed()) else {
|
||||
return Err(PipeError::Timeout);
|
||||
};
|
||||
|
||||
match self.transport.recv_timeout(remaining)? {
|
||||
BrowserMessage::Response {
|
||||
seq: response_seq,
|
||||
success,
|
||||
data,
|
||||
aom_snapshot,
|
||||
timing,
|
||||
} if response_seq == seq => {
|
||||
return Ok(CommandOutput {
|
||||
seq: response_seq,
|
||||
success,
|
||||
data,
|
||||
aom_snapshot,
|
||||
timing,
|
||||
});
|
||||
}
|
||||
BrowserMessage::Response {
|
||||
seq: response_seq, ..
|
||||
} => {
|
||||
return Err(PipeError::Protocol(format!(
|
||||
"received response seq {response_seq} while waiting for {seq}"
|
||||
)));
|
||||
}
|
||||
BrowserMessage::Init { .. } => {
|
||||
return Err(PipeError::UnexpectedMessage(
|
||||
"received duplicate init after handshake".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/pipe/handshake.rs
Normal file
53
src/pipe/handshake.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::pipe::protocol::{supported_actions, AgentMessage, BrowserMessage, PROTOCOL_VERSION};
|
||||
use crate::pipe::{PipeError, Transport};
|
||||
use crate::security::derive_session_key;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HandshakeResult {
|
||||
pub agent_id: String,
|
||||
pub session_key: Vec<u8>,
|
||||
pub capabilities: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn perform_handshake<T: Transport>(
|
||||
transport: &T,
|
||||
timeout: Duration,
|
||||
) -> Result<HandshakeResult, PipeError> {
|
||||
let init = transport.recv_timeout(timeout)?;
|
||||
|
||||
match init {
|
||||
BrowserMessage::Init {
|
||||
version,
|
||||
hmac_seed,
|
||||
capabilities,
|
||||
} => {
|
||||
if version != PROTOCOL_VERSION {
|
||||
return Err(PipeError::Protocol(format!(
|
||||
"unsupported protocol version: {version}"
|
||||
)));
|
||||
}
|
||||
|
||||
let session_key = derive_session_key(&hmac_seed)?;
|
||||
let agent_id = Uuid::new_v4().to_string();
|
||||
let ack = AgentMessage::InitAck {
|
||||
version: PROTOCOL_VERSION.to_string(),
|
||||
agent_id: agent_id.clone(),
|
||||
supported_actions: supported_actions(),
|
||||
};
|
||||
transport.send(&ack)?;
|
||||
|
||||
Ok(HandshakeResult {
|
||||
agent_id,
|
||||
session_key,
|
||||
capabilities,
|
||||
})
|
||||
}
|
||||
other => Err(PipeError::UnexpectedMessage(format!(
|
||||
"expected init as first message, got {other:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
125
src/pipe/mod.rs
Normal file
125
src/pipe/mod.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
pub mod browser_tool;
|
||||
pub mod handshake;
|
||||
pub mod protocol;
|
||||
|
||||
pub use browser_tool::{BrowserPipeTool, CommandOutput};
|
||||
pub use handshake::{perform_handshake, HandshakeResult};
|
||||
pub use protocol::{
|
||||
supported_actions, Action, AgentMessage, BrowserMessage, SecurityFields, Timing,
|
||||
};
|
||||
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::sync::{mpsc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
const MAX_MESSAGE_BYTES: usize = 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PipeError {
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("json error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error("timeout while waiting for browser message")]
|
||||
Timeout,
|
||||
#[error("pipe closed")]
|
||||
PipeClosed,
|
||||
#[error("protocol error: {0}")]
|
||||
Protocol(String),
|
||||
#[error("unexpected message: {0}")]
|
||||
UnexpectedMessage(String),
|
||||
#[error("message too large: {0} bytes")]
|
||||
MessageTooLarge(usize),
|
||||
#[error(transparent)]
|
||||
Security(#[from] crate::security::SecurityError),
|
||||
}
|
||||
|
||||
pub trait Transport: Send + Sync {
|
||||
fn send(&self, message: &AgentMessage) -> Result<(), PipeError>;
|
||||
fn recv_timeout(&self, timeout: Duration) -> Result<BrowserMessage, PipeError>;
|
||||
}
|
||||
|
||||
pub struct StdioTransport {
|
||||
rx: Mutex<mpsc::Receiver<Result<BrowserMessage, PipeError>>>,
|
||||
writer: Mutex<Box<dyn Write + Send>>,
|
||||
}
|
||||
|
||||
impl StdioTransport {
|
||||
pub fn new<R, W>(reader: R, writer: W) -> Self
|
||||
where
|
||||
R: Read + Send + 'static,
|
||||
W: Write + Send + 'static,
|
||||
{
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let mut reader = BufReader::new(reader);
|
||||
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
match reader.read_line(&mut line) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
let line = line.trim_end_matches(&['\r', '\n'][..]);
|
||||
if line.is_empty() {
|
||||
let _ = tx.send(Err(PipeError::Protocol(
|
||||
"received empty JSON line".to_string(),
|
||||
)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if line.as_bytes().len() > MAX_MESSAGE_BYTES {
|
||||
let _ = tx.send(Err(PipeError::MessageTooLarge(line.len())));
|
||||
continue;
|
||||
}
|
||||
|
||||
let parsed =
|
||||
serde_json::from_str::<BrowserMessage>(line).map_err(PipeError::from);
|
||||
let _ = tx.send(parsed);
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = tx.send(Err(PipeError::Io(err)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
rx: Mutex::new(rx),
|
||||
writer: Mutex::new(Box::new(writer)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for StdioTransport {
|
||||
fn send(&self, message: &AgentMessage) -> Result<(), PipeError> {
|
||||
let payload = serde_json::to_vec(message)?;
|
||||
if payload.len() > MAX_MESSAGE_BYTES {
|
||||
return Err(PipeError::MessageTooLarge(payload.len()));
|
||||
}
|
||||
|
||||
let mut writer = self
|
||||
.writer
|
||||
.lock()
|
||||
.map_err(|_| PipeError::Protocol("writer lock poisoned".to_string()))?;
|
||||
writer.write_all(&payload)?;
|
||||
writer.write_all(b"\n")?;
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv_timeout(&self, timeout: Duration) -> Result<BrowserMessage, PipeError> {
|
||||
let rx = self
|
||||
.rx
|
||||
.lock()
|
||||
.map_err(|_| PipeError::Protocol("receiver lock poisoned".to_string()))?;
|
||||
match rx.recv_timeout(timeout) {
|
||||
Ok(result) => result,
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => Err(PipeError::Timeout),
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => Err(PipeError::PipeClosed),
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/pipe/protocol.rs
Normal file
115
src/pipe/protocol.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub const PROTOCOL_VERSION: &str = "1.0";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum BrowserMessage {
|
||||
Init {
|
||||
version: String,
|
||||
hmac_seed: String,
|
||||
#[serde(default)]
|
||||
capabilities: Vec<String>,
|
||||
},
|
||||
Response {
|
||||
seq: u64,
|
||||
success: bool,
|
||||
#[serde(default = "default_object")]
|
||||
data: Value,
|
||||
#[serde(default)]
|
||||
aom_snapshot: Vec<Value>,
|
||||
timing: Timing,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum AgentMessage {
|
||||
InitAck {
|
||||
version: String,
|
||||
agent_id: String,
|
||||
supported_actions: Vec<Action>,
|
||||
},
|
||||
Command {
|
||||
seq: u64,
|
||||
action: Action,
|
||||
params: Value,
|
||||
security: SecurityFields,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Action {
|
||||
Click,
|
||||
Type,
|
||||
Navigate,
|
||||
GetText,
|
||||
GetHtml,
|
||||
WaitForSelector,
|
||||
PageScreenshot,
|
||||
Select,
|
||||
ScrollTo,
|
||||
GetAomSnapshot,
|
||||
StorageSet,
|
||||
StorageGet,
|
||||
ZombieSpawn,
|
||||
ZombieKill,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Action::Click => "click",
|
||||
Action::Type => "type",
|
||||
Action::Navigate => "navigate",
|
||||
Action::GetText => "getText",
|
||||
Action::GetHtml => "getHtml",
|
||||
Action::WaitForSelector => "waitForSelector",
|
||||
Action::PageScreenshot => "pageScreenshot",
|
||||
Action::Select => "select",
|
||||
Action::ScrollTo => "scrollTo",
|
||||
Action::GetAomSnapshot => "getAomSnapshot",
|
||||
Action::StorageSet => "storageSet",
|
||||
Action::StorageGet => "storageGet",
|
||||
Action::ZombieSpawn => "zombieSpawn",
|
||||
Action::ZombieKill => "zombieKill",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SecurityFields {
|
||||
pub expected_domain: String,
|
||||
pub hmac: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Timing {
|
||||
pub queue_ms: u64,
|
||||
pub exec_ms: u64,
|
||||
}
|
||||
|
||||
pub fn supported_actions() -> Vec<Action> {
|
||||
vec![
|
||||
Action::Click,
|
||||
Action::Type,
|
||||
Action::Navigate,
|
||||
Action::GetText,
|
||||
Action::GetHtml,
|
||||
Action::WaitForSelector,
|
||||
Action::PageScreenshot,
|
||||
Action::Select,
|
||||
Action::ScrollTo,
|
||||
Action::GetAomSnapshot,
|
||||
Action::StorageSet,
|
||||
Action::StorageGet,
|
||||
Action::ZombieSpawn,
|
||||
Action::ZombieKill,
|
||||
]
|
||||
}
|
||||
|
||||
fn default_object() -> Value {
|
||||
json!({})
|
||||
}
|
||||
48
src/security/hmac.rs
Normal file
48
src/security/hmac.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde_json::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::pipe::Action;
|
||||
use crate::security::SecurityError;
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
pub fn derive_session_key(hmac_seed: &str) -> Result<Vec<u8>, SecurityError> {
|
||||
let seed = hex::decode(hmac_seed)?;
|
||||
if seed.is_empty() {
|
||||
return Err(SecurityError::InvalidSeed(
|
||||
"hmac_seed must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(seed);
|
||||
hasher.update(b"sgclaw-session-v1");
|
||||
Ok(hasher.finalize().to_vec())
|
||||
}
|
||||
|
||||
pub fn sign_command(
|
||||
session_key: &[u8],
|
||||
seq: u64,
|
||||
action: &Action,
|
||||
params: &Value,
|
||||
expected_domain: &str,
|
||||
) -> Result<String, SecurityError> {
|
||||
if session_key.is_empty() {
|
||||
return Err(SecurityError::InvalidSeed(
|
||||
"session key must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut mac = HmacSha256::new_from_slice(session_key)
|
||||
.map_err(|err| SecurityError::Hmac(err.to_string()))?;
|
||||
mac.update(seq.to_string().as_bytes());
|
||||
mac.update(b"|");
|
||||
mac.update(action.as_str().as_bytes());
|
||||
mac.update(b"|");
|
||||
mac.update(expected_domain.as_bytes());
|
||||
mac.update(b"|");
|
||||
mac.update(serde_json::to_string(params)?.as_bytes());
|
||||
|
||||
Ok(hex::encode(mac.finalize().into_bytes()))
|
||||
}
|
||||
111
src/security/mac_policy.rs
Normal file
111
src/security/mac_policy.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::pipe::Action;
|
||||
use crate::security::SecurityError;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MacPolicy {
|
||||
pub version: String,
|
||||
pub domains: DomainRules,
|
||||
pub pipe_actions: PipeActionRules,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DomainRules {
|
||||
pub allowed: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PipeActionRules {
|
||||
pub allowed: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub blocked: Vec<String>,
|
||||
}
|
||||
|
||||
impl MacPolicy {
|
||||
pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, SecurityError> {
|
||||
let contents = fs::read_to_string(path)?;
|
||||
let policy: Self = serde_json::from_str(&contents)?;
|
||||
policy.validate_rules()?;
|
||||
Ok(policy)
|
||||
}
|
||||
|
||||
pub fn from_json_str(contents: &str) -> Result<Self, SecurityError> {
|
||||
let policy: Self = serde_json::from_str(contents)?;
|
||||
policy.validate_rules()?;
|
||||
Ok(policy)
|
||||
}
|
||||
|
||||
pub fn validate(&self, action: &Action, expected_domain: &str) -> Result<(), SecurityError> {
|
||||
let action_name = action.as_str();
|
||||
if self
|
||||
.pipe_actions
|
||||
.blocked
|
||||
.iter()
|
||||
.any(|blocked| blocked == action_name)
|
||||
{
|
||||
return Err(SecurityError::ActionNotAllowed(action_name.to_string()));
|
||||
}
|
||||
|
||||
if !self
|
||||
.pipe_actions
|
||||
.allowed
|
||||
.iter()
|
||||
.any(|allowed| allowed == action_name)
|
||||
{
|
||||
return Err(SecurityError::ActionNotAllowed(action_name.to_string()));
|
||||
}
|
||||
|
||||
let normalized = normalize_domain(expected_domain);
|
||||
if normalized.is_empty() {
|
||||
return Err(SecurityError::DomainNotAllowed(expected_domain.to_string()));
|
||||
}
|
||||
|
||||
if !self
|
||||
.domains
|
||||
.allowed
|
||||
.iter()
|
||||
.map(|domain| normalize_domain(domain))
|
||||
.any(|allowed| allowed == normalized)
|
||||
{
|
||||
return Err(SecurityError::DomainNotAllowed(normalized));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_rules(&self) -> Result<(), SecurityError> {
|
||||
if self.version.trim().is_empty() {
|
||||
return Err(SecurityError::InvalidRules(
|
||||
"rules version must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
if self.domains.allowed.is_empty() {
|
||||
return Err(SecurityError::InvalidRules(
|
||||
"at least one allowed domain is required".to_string(),
|
||||
));
|
||||
}
|
||||
if self.pipe_actions.allowed.is_empty() {
|
||||
return Err(SecurityError::InvalidRules(
|
||||
"at least one allowed action is required".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_domain(raw: &str) -> String {
|
||||
raw.trim()
|
||||
.trim_start_matches("https://")
|
||||
.trim_start_matches("http://")
|
||||
.split('/')
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.split(':')
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
27
src/security/mod.rs
Normal file
27
src/security/mod.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
mod hmac;
|
||||
mod mac_policy;
|
||||
|
||||
pub use hmac::{derive_session_key, sign_command};
|
||||
pub use mac_policy::MacPolicy;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SecurityError {
|
||||
#[error("invalid hmac seed: {0}")]
|
||||
InvalidSeed(String),
|
||||
#[error("action is not allowed: {0}")]
|
||||
ActionNotAllowed(String),
|
||||
#[error("domain is not allowed: {0}")]
|
||||
DomainNotAllowed(String),
|
||||
#[error("invalid rules: {0}")]
|
||||
InvalidRules(String),
|
||||
#[error("hmac error: {0}")]
|
||||
Hmac(String),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Hex(#[from] hex::FromHexError),
|
||||
}
|
||||
Reference in New Issue
Block a user