use std::collections::HashMap;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
#[cfg(feature = "postgres")]
use deadpool_postgres::Pool;
use uuid::Uuid;
use crate::tools::wasm::capabilities::{
Capabilities, EndpointPattern, HttpCapability, RateLimitConfig, SecretsCapability,
ToolInvokeCapability,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrustLevel {
System,
Verified,
User,
}
impl std::fmt::Display for TrustLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TrustLevel::System => write!(f, "system"),
TrustLevel::Verified => write!(f, "verified"),
TrustLevel::User => write!(f, "user"),
}
}
}
impl std::str::FromStr for TrustLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"system" => Ok(TrustLevel::System),
"verified" => Ok(TrustLevel::Verified),
"user" => Ok(TrustLevel::User),
_ => Err(format!("Unknown trust level: {}", s)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolStatus {
Active,
Disabled,
Quarantined,
}
impl std::fmt::Display for ToolStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToolStatus::Active => write!(f, "active"),
ToolStatus::Disabled => write!(f, "disabled"),
ToolStatus::Quarantined => write!(f, "quarantined"),
}
}
}
impl std::str::FromStr for ToolStatus {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"active" => Ok(ToolStatus::Active),
"disabled" => Ok(ToolStatus::Disabled),
"quarantined" => Ok(ToolStatus::Quarantined),
_ => Err(format!("Unknown status: {}", s)),
}
}
}
#[derive(Debug, Clone)]
pub struct StoredWasmTool {
pub id: Uuid,
pub user_id: String,
pub name: String,
pub version: String,
pub wit_version: String,
pub description: String,
pub parameters_schema: serde_json::Value,
pub source_url: Option<String>,
pub trust_level: TrustLevel,
pub status: ToolStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug)]
pub struct StoredWasmToolWithBinary {
pub tool: StoredWasmTool,
pub wasm_binary: Vec<u8>,
pub binary_hash: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct StoredCapabilities {
pub id: Uuid,
pub wasm_tool_id: Uuid,
pub http_allowlist: Vec<EndpointPattern>,
pub allowed_secrets: Vec<String>,
pub tool_aliases: HashMap<String, String>,
pub requests_per_minute: u32,
pub requests_per_hour: u32,
pub max_request_body_bytes: i64,
pub max_response_body_bytes: i64,
pub workspace_read_prefixes: Vec<String>,
pub http_timeout_secs: i32,
}
impl StoredCapabilities {
pub fn to_capabilities(&self) -> Capabilities {
let mut caps = Capabilities::default();
if !self.workspace_read_prefixes.is_empty() {
caps = caps.with_workspace_read(self.workspace_read_prefixes.clone());
}
if !self.http_allowlist.is_empty() {
caps.http = Some(HttpCapability {
allowlist: self.http_allowlist.clone(),
credentials: HashMap::new(), rate_limit: RateLimitConfig {
requests_per_minute: self.requests_per_minute,
requests_per_hour: self.requests_per_hour,
},
max_request_bytes: self.max_request_body_bytes as usize,
max_response_bytes: self.max_response_body_bytes as usize,
timeout: std::time::Duration::from_secs(self.http_timeout_secs as u64),
});
}
if !self.tool_aliases.is_empty() {
caps.tool_invoke = Some(ToolInvokeCapability {
aliases: self.tool_aliases.clone(),
rate_limit: RateLimitConfig {
requests_per_minute: self.requests_per_minute,
requests_per_hour: self.requests_per_hour,
},
});
}
if !self.allowed_secrets.is_empty() {
caps.secrets = Some(SecretsCapability {
allowed_names: self.allowed_secrets.clone(),
});
}
caps
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum WasmStorageError {
#[error("Tool not found: {0}")]
NotFound(String),
#[error("Tool is disabled")]
Disabled,
#[error("Tool is quarantined")]
Quarantined,
#[error("Binary integrity check failed: hash mismatch")]
IntegrityCheckFailed,
#[error("Database error: {0}")]
Database(String),
#[error("Invalid data: {0}")]
InvalidData(String),
}
#[async_trait]
pub trait WasmToolStore: Send + Sync {
async fn store(&self, params: StoreToolParams) -> Result<StoredWasmTool, WasmStorageError>;
async fn get(&self, user_id: &str, name: &str) -> Result<StoredWasmTool, WasmStorageError>;
async fn get_with_binary(
&self,
user_id: &str,
name: &str,
) -> Result<StoredWasmToolWithBinary, WasmStorageError>;
async fn get_capabilities(
&self,
tool_id: Uuid,
) -> Result<Option<StoredCapabilities>, WasmStorageError>;
async fn list(&self, user_id: &str) -> Result<Vec<StoredWasmTool>, WasmStorageError>;
async fn update_status(
&self,
user_id: &str,
name: &str,
status: ToolStatus,
) -> Result<(), WasmStorageError>;
async fn delete(&self, user_id: &str, name: &str) -> Result<bool, WasmStorageError>;
}
pub struct StoreToolParams {
pub user_id: String,
pub name: String,
pub version: String,
pub wit_version: String,
pub description: String,
pub wasm_binary: Vec<u8>,
pub parameters_schema: serde_json::Value,
pub source_url: Option<String>,
pub trust_level: TrustLevel,
}
pub fn compute_binary_hash(binary: &[u8]) -> Vec<u8> {
let hash = blake3::hash(binary);
hash.as_bytes().to_vec()
}
pub fn verify_binary_integrity(binary: &[u8], expected_hash: &[u8]) -> bool {
let actual_hash = compute_binary_hash(binary);
actual_hash == expected_hash
}
#[cfg(feature = "postgres")]
pub struct PostgresWasmToolStore {
pool: Pool,
}
#[cfg(feature = "postgres")]
impl PostgresWasmToolStore {
pub fn new(pool: Pool) -> Self {
Self { pool }
}
}
#[cfg(feature = "postgres")]
#[async_trait]
impl WasmToolStore for PostgresWasmToolStore {
async fn store(&self, params: StoreToolParams) -> Result<StoredWasmTool, WasmStorageError> {
let mut client = self
.pool
.get()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let binary_hash = compute_binary_hash(¶ms.wasm_binary);
let id = Uuid::new_v4();
let now = Utc::now();
let tx = client
.transaction()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
tx.execute(
"DELETE FROM wasm_tools WHERE user_id = $1 AND name = $2",
&[¶ms.user_id, ¶ms.name],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let row = tx
.query_one(
r#"
INSERT INTO wasm_tools (
id, user_id, name, version, wit_version, description, wasm_binary, binary_hash,
parameters_schema, source_url, trust_level, status, created_at, updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'active', $12, $12)
RETURNING id, user_id, name, version, wit_version, description, parameters_schema,
source_url, trust_level, status, created_at, updated_at
"#,
&[
&id,
¶ms.user_id,
¶ms.name,
¶ms.version,
¶ms.wit_version,
¶ms.description,
¶ms.wasm_binary,
&binary_hash,
¶ms.parameters_schema,
¶ms.source_url,
¶ms.trust_level.to_string(),
&now,
],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let tool = row_to_tool(&row)?;
tx.commit()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
Ok(tool)
}
async fn get(&self, user_id: &str, name: &str) -> Result<StoredWasmTool, WasmStorageError> {
let client = self
.pool
.get()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let row = client
.query_opt(
r#"
SELECT id, user_id, name, version, wit_version, description, parameters_schema,
source_url, trust_level, status, created_at, updated_at
FROM wasm_tools
WHERE user_id = $1 AND name = $2 AND status = 'active'
"#,
&[&user_id, &name],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
match row {
Some(r) => {
let tool = row_to_tool(&r)?;
match tool.status {
ToolStatus::Active => Ok(tool),
ToolStatus::Disabled => Err(WasmStorageError::Disabled),
ToolStatus::Quarantined => Err(WasmStorageError::Quarantined),
}
}
None => Err(WasmStorageError::NotFound(name.to_string())),
}
}
async fn get_with_binary(
&self,
user_id: &str,
name: &str,
) -> Result<StoredWasmToolWithBinary, WasmStorageError> {
let client = self
.pool
.get()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let row = client
.query_opt(
r#"
SELECT id, user_id, name, version, wit_version, description, wasm_binary, binary_hash,
parameters_schema, source_url, trust_level, status, created_at, updated_at
FROM wasm_tools
WHERE user_id = $1 AND name = $2 AND status = 'active'
"#,
&[&user_id, &name],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
match row {
Some(r) => {
let wasm_binary: Vec<u8> = r.get("wasm_binary");
let binary_hash: Vec<u8> = r.get("binary_hash");
if !verify_binary_integrity(&wasm_binary, &binary_hash) {
tracing::error!(
user_id = user_id,
name = name,
"WASM binary integrity check failed"
);
return Err(WasmStorageError::IntegrityCheckFailed);
}
let tool = row_to_tool(&r)?;
match tool.status {
ToolStatus::Active => Ok(StoredWasmToolWithBinary {
tool,
wasm_binary,
binary_hash,
}),
ToolStatus::Disabled => Err(WasmStorageError::Disabled),
ToolStatus::Quarantined => Err(WasmStorageError::Quarantined),
}
}
None => Err(WasmStorageError::NotFound(name.to_string())),
}
}
async fn get_capabilities(
&self,
tool_id: Uuid,
) -> Result<Option<StoredCapabilities>, WasmStorageError> {
let client = self
.pool
.get()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let row = client
.query_opt(
r#"
SELECT id, wasm_tool_id, http_allowlist, allowed_secrets, tool_aliases,
requests_per_minute, requests_per_hour, max_request_body_bytes,
max_response_body_bytes, workspace_read_prefixes, http_timeout_secs
FROM tool_capabilities
WHERE wasm_tool_id = $1
"#,
&[&tool_id],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
match row {
Some(r) => {
let http_allowlist_json: serde_json::Value = r.get("http_allowlist");
let tool_aliases_json: serde_json::Value = r.get("tool_aliases");
let http_allowlist: Vec<EndpointPattern> =
serde_json::from_value(http_allowlist_json).unwrap_or_default();
let tool_aliases: HashMap<String, String> =
serde_json::from_value(tool_aliases_json).unwrap_or_default();
Ok(Some(StoredCapabilities {
id: r.get("id"),
wasm_tool_id: r.get("wasm_tool_id"),
http_allowlist,
allowed_secrets: r.get("allowed_secrets"),
tool_aliases,
requests_per_minute: r.get::<_, i32>("requests_per_minute") as u32,
requests_per_hour: r.get::<_, i32>("requests_per_hour") as u32,
max_request_body_bytes: r.get("max_request_body_bytes"),
max_response_body_bytes: r.get("max_response_body_bytes"),
workspace_read_prefixes: r.get("workspace_read_prefixes"),
http_timeout_secs: r.get("http_timeout_secs"),
}))
}
None => Ok(None),
}
}
async fn list(&self, user_id: &str) -> Result<Vec<StoredWasmTool>, WasmStorageError> {
let client = self
.pool
.get()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let rows = client
.query(
r#"
SELECT id, user_id, name, version, wit_version, description,
parameters_schema, source_url, trust_level, status, created_at, updated_at
FROM wasm_tools
WHERE user_id = $1
ORDER BY name
"#,
&[&user_id],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
rows.into_iter().map(|r| row_to_tool(&r)).collect()
}
async fn update_status(
&self,
user_id: &str,
name: &str,
status: ToolStatus,
) -> Result<(), WasmStorageError> {
let client = self
.pool
.get()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let result = client
.execute(
"UPDATE wasm_tools SET status = $1, updated_at = NOW() WHERE user_id = $2 AND name = $3",
&[&status.to_string(), &user_id, &name],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
if result == 0 {
return Err(WasmStorageError::NotFound(name.to_string()));
}
Ok(())
}
async fn delete(&self, user_id: &str, name: &str) -> Result<bool, WasmStorageError> {
let client = self
.pool
.get()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let result = client
.execute(
"DELETE FROM wasm_tools WHERE user_id = $1 AND name = $2",
&[&user_id, &name],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
Ok(result > 0)
}
}
#[cfg(feature = "postgres")]
fn row_to_tool(row: &tokio_postgres::Row) -> Result<StoredWasmTool, WasmStorageError> {
let trust_level_str: String = row.get("trust_level");
let status_str: String = row.get("status");
Ok(StoredWasmTool {
id: row.get("id"),
user_id: row.get("user_id"),
name: row.get("name"),
version: row.get("version"),
wit_version: row.get("wit_version"),
description: row.get("description"),
parameters_schema: row.get("parameters_schema"),
source_url: row.get("source_url"),
trust_level: trust_level_str
.parse()
.map_err(WasmStorageError::InvalidData)?,
status: status_str.parse().map_err(WasmStorageError::InvalidData)?,
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
})
}
#[cfg(feature = "libsql")]
pub struct LibSqlWasmToolStore {
db: std::sync::Arc<libsql::Database>,
}
#[cfg(feature = "libsql")]
impl LibSqlWasmToolStore {
pub fn new(db: std::sync::Arc<libsql::Database>) -> Self {
Self { db }
}
async fn connect(&self) -> Result<libsql::Connection, WasmStorageError> {
let conn = self
.db
.connect()
.map_err(|e| WasmStorageError::Database(format!("Connection failed: {}", e)))?;
conn.query("PRAGMA busy_timeout = 5000", ())
.await
.map_err(|e| {
WasmStorageError::Database(format!("Failed to set busy_timeout: {}", e))
})?;
Ok(conn)
}
}
#[cfg(feature = "libsql")]
#[async_trait]
impl WasmToolStore for LibSqlWasmToolStore {
async fn store(&self, params: StoreToolParams) -> Result<StoredWasmTool, WasmStorageError> {
let binary_hash = compute_binary_hash(¶ms.wasm_binary);
let id = Uuid::new_v4();
let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let schema_str = serde_json::to_string(¶ms.parameters_schema)
.map_err(|e| WasmStorageError::InvalidData(e.to_string()))?;
let conn = self.connect().await?;
let tx = conn
.transaction()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
tx.execute(
"DELETE FROM wasm_tools WHERE user_id = ?1 AND name = ?2",
libsql::params![params.user_id.as_str(), params.name.as_str()],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
tx.execute(
r#"
INSERT INTO wasm_tools (
id, user_id, name, version, wit_version, description, wasm_binary, binary_hash,
parameters_schema, source_url, trust_level, status, created_at, updated_at
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, 'active', ?12, ?12)
"#,
libsql::params![
id.to_string(),
params.user_id.as_str(),
params.name.as_str(),
params.version.as_str(),
params.wit_version.as_str(),
params.description.as_str(),
libsql::Value::Blob(params.wasm_binary),
libsql::Value::Blob(binary_hash),
schema_str.as_str(),
libsql_wasm_opt_text(params.source_url.as_deref()),
params.trust_level.to_string(),
now.as_str(),
],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let mut rows = tx
.query(
r#"
SELECT id, user_id, name, version, wit_version, description, parameters_schema,
source_url, trust_level, status, created_at, updated_at
FROM wasm_tools
WHERE user_id = ?1 AND name = ?2
"#,
libsql::params![params.user_id.as_str(), params.name.as_str()],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let row = rows
.next()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?
.ok_or_else(|| {
WasmStorageError::Database("Insert succeeded but row not found".into())
})?;
let tool = libsql_row_to_tool(&row)?;
tx.commit()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
Ok(tool)
}
async fn get(&self, user_id: &str, name: &str) -> Result<StoredWasmTool, WasmStorageError> {
let conn = self.connect().await?;
let mut rows = conn
.query(
r#"
SELECT id, user_id, name, version, wit_version, description, parameters_schema,
source_url, trust_level, status, created_at, updated_at
FROM wasm_tools
WHERE user_id = ?1 AND name = ?2 AND status = 'active'
"#,
libsql::params![user_id, name],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
match rows
.next()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?
{
Some(row) => {
let tool = libsql_row_to_tool(&row)?;
match tool.status {
ToolStatus::Active => Ok(tool),
ToolStatus::Disabled => Err(WasmStorageError::Disabled),
ToolStatus::Quarantined => Err(WasmStorageError::Quarantined),
}
}
None => Err(WasmStorageError::NotFound(name.to_string())),
}
}
async fn get_with_binary(
&self,
user_id: &str,
name: &str,
) -> Result<StoredWasmToolWithBinary, WasmStorageError> {
let conn = self.connect().await?;
let mut rows = conn
.query(
r#"
SELECT id, user_id, name, version, wit_version, description, wasm_binary, binary_hash,
parameters_schema, source_url, trust_level, status, created_at, updated_at
FROM wasm_tools
WHERE user_id = ?1 AND name = ?2 AND status = 'active'
"#,
libsql::params![user_id, name],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
match rows
.next()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?
{
Some(row) => {
let wasm_binary: Vec<u8> = row
.get(6)
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let binary_hash: Vec<u8> = row
.get(7)
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
if !verify_binary_integrity(&wasm_binary, &binary_hash) {
tracing::error!(
user_id = user_id,
name = name,
"WASM binary integrity check failed"
);
return Err(WasmStorageError::IntegrityCheckFailed);
}
let tool = libsql_row_to_tool_with_offset(&row)?;
match tool.status {
ToolStatus::Active => Ok(StoredWasmToolWithBinary {
tool,
wasm_binary,
binary_hash,
}),
ToolStatus::Disabled => Err(WasmStorageError::Disabled),
ToolStatus::Quarantined => Err(WasmStorageError::Quarantined),
}
}
None => Err(WasmStorageError::NotFound(name.to_string())),
}
}
async fn get_capabilities(
&self,
tool_id: Uuid,
) -> Result<Option<StoredCapabilities>, WasmStorageError> {
let conn = self.connect().await?;
let mut rows = conn
.query(
r#"
SELECT id, wasm_tool_id, http_allowlist, allowed_secrets, tool_aliases,
requests_per_minute, requests_per_hour, max_request_body_bytes,
max_response_body_bytes, workspace_read_prefixes, http_timeout_secs
FROM tool_capabilities
WHERE wasm_tool_id = ?1
"#,
libsql::params![tool_id.to_string()],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
match rows
.next()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?
{
Some(row) => {
let id_str: String = row
.get(0)
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let tool_id_str: String = row
.get(1)
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let http_allowlist_str: String = row.get::<String>(2).unwrap_or_default();
let allowed_secrets_str: String = row.get::<String>(3).unwrap_or_default();
let tool_aliases_str: String = row.get::<String>(4).unwrap_or_default();
let rpm: i64 = row.get::<i64>(5).unwrap_or(60);
let rph: i64 = row.get::<i64>(6).unwrap_or(1000);
let max_req: i64 = row.get::<i64>(7).unwrap_or(1048576);
let max_resp: i64 = row.get::<i64>(8).unwrap_or(10485760);
let ws_prefixes_str: String = row.get::<String>(9).unwrap_or_default();
let timeout: i64 = row.get::<i64>(10).unwrap_or(30);
let http_allowlist: Vec<EndpointPattern> =
serde_json::from_str(&http_allowlist_str).unwrap_or_default();
let allowed_secrets: Vec<String> =
serde_json::from_str(&allowed_secrets_str).unwrap_or_default();
let tool_aliases: HashMap<String, String> =
serde_json::from_str(&tool_aliases_str).unwrap_or_default();
let workspace_read_prefixes: Vec<String> =
serde_json::from_str(&ws_prefixes_str).unwrap_or_default();
Ok(Some(StoredCapabilities {
id: id_str
.parse()
.map_err(|e: uuid::Error| WasmStorageError::InvalidData(e.to_string()))?,
wasm_tool_id: tool_id_str
.parse()
.map_err(|e: uuid::Error| WasmStorageError::InvalidData(e.to_string()))?,
http_allowlist,
allowed_secrets,
tool_aliases,
requests_per_minute: rpm as u32,
requests_per_hour: rph as u32,
max_request_body_bytes: max_req,
max_response_body_bytes: max_resp,
workspace_read_prefixes,
http_timeout_secs: timeout as i32,
}))
}
None => Ok(None),
}
}
async fn list(&self, user_id: &str) -> Result<Vec<StoredWasmTool>, WasmStorageError> {
let conn = self.connect().await?;
let mut rows = conn
.query(
r#"
SELECT id, user_id, name, version, wit_version, description, parameters_schema,
source_url, trust_level, status, created_at, updated_at
FROM wasm_tools
WHERE user_id = ?1
ORDER BY name
"#,
libsql::params![user_id],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let mut tools = Vec::new();
while let Some(row) = rows
.next()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?
{
tools.push(libsql_row_to_tool(&row)?);
}
Ok(tools)
}
async fn update_status(
&self,
user_id: &str,
name: &str,
status: ToolStatus,
) -> Result<(), WasmStorageError> {
let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let conn = self.connect().await?;
let result = conn
.execute(
"UPDATE wasm_tools SET status = ?1, updated_at = ?2 WHERE user_id = ?3 AND name = ?4",
libsql::params![status.to_string(), now.as_str(), user_id, name],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
if result == 0 {
return Err(WasmStorageError::NotFound(name.to_string()));
}
Ok(())
}
async fn delete(&self, user_id: &str, name: &str) -> Result<bool, WasmStorageError> {
let conn = self.connect().await?;
let result = conn
.execute(
"DELETE FROM wasm_tools WHERE user_id = ?1 AND name = ?2",
libsql::params![user_id, name],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
Ok(result > 0)
}
}
#[cfg(feature = "libsql")]
fn libsql_wasm_opt_text(s: Option<&str>) -> libsql::Value {
match s {
Some(s) => libsql::Value::Text(s.to_string()),
None => libsql::Value::Null,
}
}
#[cfg(feature = "libsql")]
fn libsql_wasm_parse_ts(s: &str) -> Result<DateTime<Utc>, WasmStorageError> {
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
return Ok(dt.with_timezone(&Utc));
}
if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") {
return Ok(ndt.and_utc());
}
if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
return Ok(ndt.and_utc());
}
Err(WasmStorageError::InvalidData(format!(
"unparseable timestamp: {:?}",
s
)))
}
#[cfg(feature = "libsql")]
fn libsql_row_to_tool(row: &libsql::Row) -> Result<StoredWasmTool, WasmStorageError> {
libsql_row_to_tool_at(row, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
}
#[cfg(feature = "libsql")]
fn libsql_row_to_tool_with_offset(row: &libsql::Row) -> Result<StoredWasmTool, WasmStorageError> {
libsql_row_to_tool_at(row, 0, 1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13)
}
#[cfg(feature = "libsql")]
#[allow(clippy::too_many_arguments)]
fn libsql_row_to_tool_at(
row: &libsql::Row,
id_idx: i32,
user_id_idx: i32,
name_idx: i32,
version_idx: i32,
wit_version_idx: i32,
description_idx: i32,
schema_idx: i32,
source_url_idx: i32,
trust_level_idx: i32,
status_idx: i32,
created_at_idx: i32,
updated_at_idx: i32,
) -> Result<StoredWasmTool, WasmStorageError> {
let id_str: String = row
.get(id_idx)
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let trust_level_str: String = row
.get(trust_level_idx)
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let status_str: String = row
.get(status_idx)
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let schema_str: String = row
.get(schema_idx)
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let created_at_str: String = row
.get(created_at_idx)
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
let updated_at_str: String = row
.get(updated_at_idx)
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
Ok(StoredWasmTool {
id: id_str
.parse()
.map_err(|e: uuid::Error| WasmStorageError::InvalidData(e.to_string()))?,
user_id: row
.get(user_id_idx)
.map_err(|e| WasmStorageError::Database(e.to_string()))?,
name: row
.get(name_idx)
.map_err(|e| WasmStorageError::Database(e.to_string()))?,
version: row
.get(version_idx)
.map_err(|e| WasmStorageError::Database(e.to_string()))?,
wit_version: row
.get(wit_version_idx)
.map_err(|e| WasmStorageError::Database(e.to_string()))?,
description: row
.get(description_idx)
.map_err(|e| WasmStorageError::Database(e.to_string()))?,
parameters_schema: serde_json::from_str(&schema_str).unwrap_or_default(),
source_url: row
.get::<String>(source_url_idx)
.ok()
.filter(|s| !s.is_empty()),
trust_level: trust_level_str
.parse()
.map_err(WasmStorageError::InvalidData)?,
status: status_str.parse().map_err(WasmStorageError::InvalidData)?,
created_at: libsql_wasm_parse_ts(&created_at_str)?,
updated_at: libsql_wasm_parse_ts(&updated_at_str)?,
})
}
#[cfg(test)]
mod tests {
use crate::tools::wasm::storage::{
ToolStatus, TrustLevel, compute_binary_hash, verify_binary_integrity,
};
#[test]
fn test_compute_hash() {
let binary = b"(module)";
let hash = compute_binary_hash(binary);
assert_eq!(hash.len(), 32); }
#[test]
fn test_verify_integrity_success() {
let binary = b"test wasm binary content";
let hash = compute_binary_hash(binary);
assert!(verify_binary_integrity(binary, &hash));
}
#[test]
fn test_verify_integrity_failure() {
let binary = b"test wasm binary content";
let hash = compute_binary_hash(binary);
let tampered = b"tampered wasm binary content";
assert!(!verify_binary_integrity(tampered, &hash));
}
#[test]
fn test_trust_level_parse() {
assert_eq!("system".parse::<TrustLevel>().unwrap(), TrustLevel::System);
assert_eq!(
"verified".parse::<TrustLevel>().unwrap(),
TrustLevel::Verified
);
assert_eq!("user".parse::<TrustLevel>().unwrap(), TrustLevel::User);
assert!("invalid".parse::<TrustLevel>().is_err());
}
#[test]
fn test_status_parse() {
assert_eq!("active".parse::<ToolStatus>().unwrap(), ToolStatus::Active);
assert_eq!(
"disabled".parse::<ToolStatus>().unwrap(),
ToolStatus::Disabled
);
assert_eq!(
"quarantined".parse::<ToolStatus>().unwrap(),
ToolStatus::Quarantined
);
assert!("invalid".parse::<ToolStatus>().is_err());
}
}