use std::time::Duration;
use uni_plugin::Capability;
use wasmtime::Engine;
use wasmtime::component::{ComponentType, Lift, Linker, Lower};
use crate::error::WasmError;
use crate::host_state::HostState;
const DEFAULT_TIMEOUT_MS: u64 = 10_000;
const MAX_RESPONSE_BYTES: usize = 8 * 1024 * 1024;
#[derive(ComponentType, Lower, Lift)]
#[component(record)]
struct WasmHttpResponse {
status: u16,
body: Vec<u8>,
}
#[derive(ComponentType, Lower, Lift)]
#[component(record)]
struct WasmFnError {
code: u32,
message: String,
retryable: bool,
}
fn fn_err(code: u32, message: impl Into<String>) -> WasmFnError {
WasmFnError {
code,
message: message.into(),
retryable: false,
}
}
pub fn build_scalar_linker_v1(
engine: &Engine,
effective_caps: &uni_plugin::CapabilitySet,
) -> Result<Linker<HostState>, WasmError> {
let mut linker = Linker::<HostState>::new(engine);
wasmtime_wasi::p2::add_to_linker_sync(&mut linker)
.map_err(|e| WasmError::Instantiate(format!("link wasi: {e}")))?;
add_host_log(&mut linker)?;
add_host_trace_context(&mut linker)?;
if effective_caps
.iter()
.any(|c| matches!(c, Capability::Network { .. }))
{
add_host_net(&mut linker)?;
}
Ok(linker)
}
pub fn build_scalar_linker(
engine: &Engine,
effective_caps: &uni_plugin::CapabilitySet,
) -> Result<Linker<HostState>, WasmError> {
build_scalar_linker_v1(engine, effective_caps)
}
pub fn build_scalar_linker_v2(
engine: &Engine,
effective_caps: &uni_plugin::CapabilitySet,
) -> Result<Linker<HostState>, WasmError> {
let mut linker = build_scalar_linker_v1(engine, effective_caps)?;
let mut instance = linker
.instance("uni:plugin/host-log-v2")
.map_err(|e| WasmError::Instantiate(format!("link host-log-v2 instance: {e}")))?;
instance
.func_wrap(
"log",
|_store: wasmtime::StoreContextMut<'_, HostState>,
(level, message): (String, String)|
-> wasmtime::Result<()> {
emit_log(&level, &format!("[v2] {message}"));
Ok(())
},
)
.map_err(|e| WasmError::Instantiate(format!("link host-log-v2: {e}")))?;
Ok(linker)
}
fn add_host_log(linker: &mut Linker<HostState>) -> Result<(), WasmError> {
let mut instance = linker
.instance("uni:plugin/host-log")
.map_err(|e| WasmError::Instantiate(format!("link host-log instance: {e}")))?;
instance
.func_wrap(
"log",
|_store: wasmtime::StoreContextMut<'_, HostState>,
(level, message): (String, String)|
-> wasmtime::Result<()> {
emit_log(&level, &message);
Ok(())
},
)
.map_err(|e| WasmError::Instantiate(format!("link host-log: {e}")))?;
Ok(())
}
fn add_host_net(linker: &mut Linker<HostState>) -> Result<(), WasmError> {
let mut instance = linker
.instance("uni:plugin/host-net@0.1.0")
.map_err(|e| WasmError::Instantiate(format!("link host-net instance: {e}")))?;
instance
.func_wrap(
"http-get",
|store: wasmtime::StoreContextMut<'_, HostState>,
(url, timeout_ms, max_bytes): (String, u64, u32)|
-> wasmtime::Result<(Result<WasmHttpResponse, WasmFnError>,)> {
Ok((host_http(store.data(), &url, None, timeout_ms, max_bytes),))
},
)
.map_err(|e| WasmError::Instantiate(format!("link host-net http-get: {e}")))?;
instance
.func_wrap(
"http-post",
|store: wasmtime::StoreContextMut<'_, HostState>,
(url, body, timeout_ms, max_bytes): (String, Vec<u8>, u64, u32)|
-> wasmtime::Result<(Result<WasmHttpResponse, WasmFnError>,)> {
Ok((host_http(
store.data(),
&url,
Some(body),
timeout_ms,
max_bytes,
),))
},
)
.map_err(|e| WasmError::Instantiate(format!("link host-net http-post: {e}")))?;
Ok(())
}
fn add_host_trace_context(linker: &mut Linker<HostState>) -> Result<(), WasmError> {
let mut instance = linker
.instance("uni:plugin/host-trace-context@0.1.0")
.map_err(|e| WasmError::Instantiate(format!("link host-trace-context instance: {e}")))?;
instance
.func_wrap(
"get-traceparent",
|_store: wasmtime::StoreContextMut<'_, HostState>,
(): ()|
-> wasmtime::Result<(Option<String>,)> {
Ok((uni_plugin::observability::current_trace_context().to_traceparent(),))
},
)
.map_err(|e| {
WasmError::Instantiate(format!("link host-trace-context get-traceparent: {e}"))
})?;
Ok(())
}
fn host_http(
state: &HostState,
url: &str,
body: Option<Vec<u8>>,
timeout_ms: u64,
max_bytes: u32,
) -> Result<WasmHttpResponse, WasmFnError> {
if !state.effective.iter().any(|c| c.network_allows(url)) {
return Err(fn_err(
0xD20,
format!("host-net: url `{url}` not in granted Network allow-list"),
));
}
let Some(egress) = state.http.as_ref() else {
return Err(fn_err(0xD21, "host-net: no HTTP egress configured"));
};
let ceiling_ms = state
.effective
.iter()
.find_map(|c| match c {
Capability::WallClockMillisPerCall(ms) => Some(*ms),
_ => None,
})
.unwrap_or(DEFAULT_TIMEOUT_MS);
let timeout = Duration::from_millis(if timeout_ms == 0 {
ceiling_ms
} else {
timeout_ms.min(ceiling_ms)
});
let max = if max_bytes == 0 {
MAX_RESPONSE_BYTES
} else {
(max_bytes as usize).min(MAX_RESPONSE_BYTES)
};
let traceparent = uni_plugin::observability::current_trace_context().to_traceparent();
let tp = traceparent.as_deref();
let response = match body {
Some(b) => egress.post(url, &b, timeout, max, tp),
None => egress.get(url, timeout, max, tp),
}
.map_err(|e| fn_err(0xD22, format!("host-net(`{url}`): {e}")))?;
Ok(WasmHttpResponse {
status: response.status,
body: response.body,
})
}
fn emit_log(level: &str, message: &str) {
match level {
"error" => tracing::error!(target: "wasm-plugin", "{message}"),
"warn" => tracing::warn!(target: "wasm-plugin", "{message}"),
"info" => tracing::info!(target: "wasm-plugin", "{message}"),
"debug" => tracing::debug!(target: "wasm-plugin", "{message}"),
_ => tracing::trace!(target: "wasm-plugin", "{message}"),
}
}