#![allow(clippy::too_many_lines)]
use super::wasm;
use cuenv_secrets::SecretError;
use extism::{CurrentPlugin, Function, Manifest, Plugin, UserData, Val, ValType, Wasm};
use std::sync::{LazyLock, Mutex};
static SHARED_CORE: LazyLock<Mutex<Option<SharedCore>>> = LazyLock::new(|| Mutex::new(None));
#[must_use]
pub fn create_host_functions() -> Vec<Function> {
use std::time::{SystemTime, UNIX_EPOCH};
let random_fill = Function::new(
"random_fill_imported",
[ValType::I32],
[ValType::I64],
UserData::new(()),
|plugin: &mut CurrentPlugin, inputs: &[Val], outputs: &mut [Val], _: UserData<()>| {
let length = usize::try_from(inputs[0].unwrap_i32()).unwrap_or(0);
let mut bytes = vec![0u8; length];
getrandom::fill(&mut bytes)
.map_err(|e| extism::Error::msg(format!("Failed to generate random bytes: {e}")))?;
let handle = plugin
.memory_new(&bytes)
.map_err(|e| extism::Error::msg(format!("Failed to write bytes: {e}")))?;
#[expect(clippy::cast_possible_wrap)]
let offset = handle.offset() as i64;
outputs[0] = Val::I64(offset);
Ok(())
},
)
.with_namespace("op-extism-core");
let time_op_now = Function::new(
"unix_time_milliseconds_imported",
[],
[ValType::I64],
UserData::new(()),
|_plugin: &mut CurrentPlugin, _inputs: &[Val], outputs: &mut [Val], _: UserData<()>| {
#[expect(clippy::cast_possible_truncation)]
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
outputs[0] = Val::I64(now);
Ok(())
},
)
.with_namespace("op-now");
let time_zxcvbn = Function::new(
"unix_time_milliseconds_imported",
[],
[ValType::I64],
UserData::new(()),
|_plugin: &mut CurrentPlugin, _inputs: &[Val], outputs: &mut [Val], _: UserData<()>| {
#[expect(clippy::cast_possible_truncation)]
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
outputs[0] = Val::I64(now);
Ok(())
},
)
.with_namespace("zxcvbn");
let utc_offset = Function::new(
"utc_offset_seconds",
[],
[ValType::I64],
UserData::new(()),
|_plugin: &mut CurrentPlugin, _inputs: &[Val], outputs: &mut [Val], _: UserData<()>| {
let offset_seconds = i64::from(chrono::Local::now().offset().local_minus_utc());
outputs[0] = Val::I64(offset_seconds);
Ok(())
},
)
.with_namespace("op-time");
vec![random_fill, time_op_now, time_zxcvbn, utc_offset]
}
pub struct SharedCore {
plugin: Plugin,
}
impl SharedCore {
pub fn get_or_init() -> Result<&'static Mutex<Option<Self>>, SecretError> {
let mut guard = SHARED_CORE
.lock()
.map_err(|_| SecretError::ResolutionFailed {
name: "onepassword".to_string(),
message: "Failed to acquire shared core lock".to_string(),
})?;
if guard.is_none() {
let wasm_bytes = wasm::load_onepassword_wasm()?;
let manifest = Manifest::new([Wasm::data(wasm_bytes)]).with_allowed_hosts(
["*.1password.com", "*.1password.ca", "*.1password.eu"]
.into_iter()
.map(String::from),
);
let host_functions = create_host_functions();
let plugin = Plugin::new(&manifest, host_functions, true).map_err(|e| {
SecretError::ResolutionFailed {
name: "onepassword".to_string(),
message: format!("Failed to initialize WASM plugin: {e}"),
}
})?;
*guard = Some(Self { plugin });
tracing::debug!("1Password WASM plugin initialized");
}
drop(guard);
Ok(&SHARED_CORE)
}
pub fn init_client(&mut self, token: &str) -> Result<u64, SecretError> {
let os = match std::env::consts::OS {
"macos" => "darwin",
other => other,
};
let arch = match std::env::consts::ARCH {
"aarch64" => "arm64",
"x86_64" => "amd64",
other => other,
};
let config = serde_json::json!({
"serviceAccountToken": token,
"programmingLanguage": "Go", "sdkVersion": "0030101", "integrationName": "cuenv",
"integrationVersion": env!("CARGO_PKG_VERSION"),
"requestLibraryName": "net/http",
"requestLibraryVersion": "go1.23.0",
"os": os,
"osVersion": "0.0.0",
"architecture": arch,
});
let config_bytes =
serde_json::to_vec(&config).map_err(|e| SecretError::ResolutionFailed {
name: "onepassword".to_string(),
message: format!("Failed to serialize config: {e}"),
})?;
let result = self
.plugin
.call::<_, String>("init_client", config_bytes)
.map_err(|e| SecretError::ResolutionFailed {
name: "onepassword".to_string(),
message: format!("Failed to initialize client: {e}"),
})?;
let response: serde_json::Value =
serde_json::from_str(&result).map_err(|e| SecretError::ResolutionFailed {
name: "onepassword".to_string(),
message: format!("Failed to parse init_client response: {e}"),
})?;
if let Some(error_name) = response.get("name") {
let message = response
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error");
return Err(SecretError::ResolutionFailed {
name: "onepassword".to_string(),
message: format!("1Password error ({error_name}): {message}"),
});
}
let client_id = response
.as_u64()
.ok_or_else(|| SecretError::ResolutionFailed {
name: "onepassword".to_string(),
message: format!("Expected client ID number, got: {result}"),
})?;
tracing::debug!(client_id, "1Password client initialized");
Ok(client_id)
}
pub fn invoke(
&mut self,
client_id: u64,
method: &str,
params: &serde_json::Map<String, serde_json::Value>,
context: &str,
) -> Result<String, SecretError> {
let request = serde_json::json!({
"invocation": {
"clientId": client_id,
"parameters": {
"name": method,
"parameters": params
}
}
});
let request_bytes =
serde_json::to_vec(&request).map_err(|e| SecretError::ResolutionFailed {
name: context.to_string(),
message: format!("Failed to serialize invoke request: {e}"),
})?;
let result = self
.plugin
.call::<_, String>("invoke", request_bytes)
.map_err(|e| SecretError::ResolutionFailed {
name: context.to_string(),
message: format!("1Password invoke failed: {e}"),
})?;
let response: serde_json::Value =
serde_json::from_str(&result).map_err(|e| SecretError::ResolutionFailed {
name: context.to_string(),
message: format!("Failed to parse invoke response: {e}"),
})?;
if let Some(error_name) = response.get("name") {
let message = response
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error");
return Err(SecretError::ResolutionFailed {
name: context.to_string(),
message: format!("1Password error ({error_name}): {message}"),
});
}
Ok(result)
}
pub fn release_client(&mut self, client_id: u64) {
if let Ok(client_id_bytes) = serde_json::to_vec(&client_id) {
let _ = self
.plugin
.call::<_, String>("release_client", client_id_bytes);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shared_core_lazy_init() {
let _ = &SHARED_CORE;
}
#[test]
fn test_create_host_functions_returns_four_functions() {
let functions = create_host_functions();
assert_eq!(functions.len(), 4, "Should create 4 host functions");
}
#[test]
fn test_host_functions_are_valid() {
let functions = create_host_functions();
assert_eq!(functions.len(), 4, "Should create exactly 4 host functions");
}
#[test]
fn test_create_host_functions_can_be_created_multiple_times() {
let first = create_host_functions();
let second = create_host_functions();
assert_eq!(first.len(), second.len());
}
#[test]
fn test_shared_core_static_is_mutex() {
let guard = SHARED_CORE.lock();
assert!(guard.is_ok(), "Should be able to lock the mutex");
}
#[test]
fn test_os_mapping() {
let os = match std::env::consts::OS {
"macos" => "darwin",
other => other,
};
if std::env::consts::OS == "macos" {
assert_eq!(os, "darwin");
}
}
#[test]
fn test_arch_mapping() {
let arch = match std::env::consts::ARCH {
"aarch64" => "arm64",
"x86_64" => "amd64",
other => other,
};
if std::env::consts::ARCH == "aarch64" {
assert_eq!(arch, "arm64");
}
if std::env::consts::ARCH == "x86_64" {
assert_eq!(arch, "amd64");
}
}
#[test]
fn test_os_mapping_linux() {
let os = match "linux" {
"macos" => "darwin",
other => other,
};
assert_eq!(os, "linux");
}
#[test]
fn test_os_mapping_windows() {
let os = match "windows" {
"macos" => "darwin",
other => other,
};
assert_eq!(os, "windows");
}
#[test]
fn test_arch_mapping_arm() {
let arch = match "arm" {
"aarch64" => "arm64",
"x86_64" => "amd64",
other => other,
};
assert_eq!(arch, "arm");
}
#[test]
fn test_arch_mapping_riscv() {
let arch = match "riscv64" {
"aarch64" => "arm64",
"x86_64" => "amd64",
other => other,
};
assert_eq!(arch, "riscv64");
}
#[test]
fn test_host_functions_creates_random_fill() {
let functions = create_host_functions();
assert_eq!(functions.len(), 4);
}
#[test]
fn test_create_host_functions_no_side_effects() {
let _functions1 = create_host_functions();
let _functions2 = create_host_functions();
}
#[test]
fn test_shared_core_mutex_initial_state() {
let guard = SHARED_CORE.lock();
assert!(guard.is_ok());
let inner = guard.unwrap();
let _ = inner.is_none() || inner.is_some();
}
#[test]
fn test_get_or_init_without_wasm_returns_error() {
let result = SharedCore::get_or_init();
let _ = result.is_ok() || result.is_err();
}
}