1#![allow(clippy::too_many_lines)]
8
9use super::wasm;
10use cuenv_secrets::SecretError;
11use extism::{CurrentPlugin, Function, Manifest, Plugin, UserData, Val, ValType, Wasm};
12use std::sync::{LazyLock, Mutex};
13
14static SHARED_CORE: LazyLock<Mutex<Option<SharedCore>>> = LazyLock::new(|| Mutex::new(None));
16
17#[must_use]
25pub fn create_host_functions() -> Vec<Function> {
26 use std::time::{SystemTime, UNIX_EPOCH};
27
28 let random_fill = Function::new(
32 "random_fill_imported",
33 [ValType::I32],
34 [ValType::I64],
35 UserData::new(()),
36 |plugin: &mut CurrentPlugin, inputs: &[Val], outputs: &mut [Val], _: UserData<()>| {
37 let length = usize::try_from(inputs[0].unwrap_i32()).unwrap_or(0);
38
39 let mut bytes = vec![0u8; length];
41 getrandom::fill(&mut bytes)
42 .map_err(|e| extism::Error::msg(format!("Failed to generate random bytes: {e}")))?;
43
44 let handle = plugin
46 .memory_new(&bytes)
47 .map_err(|e| extism::Error::msg(format!("Failed to write bytes: {e}")))?;
48
49 #[expect(clippy::cast_possible_wrap)]
51 let offset = handle.offset() as i64;
52 outputs[0] = Val::I64(offset);
53 Ok(())
54 },
55 )
56 .with_namespace("op-extism-core");
57
58 let time_op_now = Function::new(
62 "unix_time_milliseconds_imported",
63 [],
64 [ValType::I64],
65 UserData::new(()),
66 |_plugin: &mut CurrentPlugin, _inputs: &[Val], outputs: &mut [Val], _: UserData<()>| {
67 #[expect(clippy::cast_possible_truncation)]
69 let now = SystemTime::now()
70 .duration_since(UNIX_EPOCH)
71 .unwrap_or_default()
72 .as_millis() as i64;
73 outputs[0] = Val::I64(now);
74 Ok(())
75 },
76 )
77 .with_namespace("op-now");
78
79 let time_zxcvbn = Function::new(
81 "unix_time_milliseconds_imported",
82 [],
83 [ValType::I64],
84 UserData::new(()),
85 |_plugin: &mut CurrentPlugin, _inputs: &[Val], outputs: &mut [Val], _: UserData<()>| {
86 #[expect(clippy::cast_possible_truncation)]
88 let now = SystemTime::now()
89 .duration_since(UNIX_EPOCH)
90 .unwrap_or_default()
91 .as_millis() as i64;
92 outputs[0] = Val::I64(now);
93 Ok(())
94 },
95 )
96 .with_namespace("zxcvbn");
97
98 let utc_offset = Function::new(
102 "utc_offset_seconds",
103 [],
104 [ValType::I64],
105 UserData::new(()),
106 |_plugin: &mut CurrentPlugin, _inputs: &[Val], outputs: &mut [Val], _: UserData<()>| {
107 let offset_seconds = i64::from(chrono::Local::now().offset().local_minus_utc());
109 outputs[0] = Val::I64(offset_seconds);
110 Ok(())
111 },
112 )
113 .with_namespace("op-time");
114
115 vec![random_fill, time_op_now, time_zxcvbn, utc_offset]
116}
117
118pub struct SharedCore {
123 plugin: Plugin,
124}
125
126impl SharedCore {
127 pub fn get_or_init() -> Result<&'static Mutex<Option<Self>>, SecretError> {
139 let mut guard = SHARED_CORE
140 .lock()
141 .map_err(|_| SecretError::ResolutionFailed {
142 name: "onepassword".to_string(),
143 message: "Failed to acquire shared core lock".to_string(),
144 })?;
145
146 if guard.is_none() {
147 let wasm_bytes = wasm::load_onepassword_wasm()?;
148
149 let manifest = Manifest::new([Wasm::data(wasm_bytes)]).with_allowed_hosts(
150 ["*.1password.com", "*.1password.ca", "*.1password.eu"]
151 .into_iter()
152 .map(String::from),
153 );
154
155 let host_functions = create_host_functions();
156 let plugin = Plugin::new(&manifest, host_functions, true).map_err(|e| {
157 SecretError::ResolutionFailed {
158 name: "onepassword".to_string(),
159 message: format!("Failed to initialize WASM plugin: {e}"),
160 }
161 })?;
162
163 *guard = Some(Self { plugin });
164 tracing::debug!("1Password WASM plugin initialized");
165 }
166
167 drop(guard);
169 Ok(&SHARED_CORE)
170 }
171
172 pub fn init_client(&mut self, token: &str) -> Result<u64, SecretError> {
184 let os = match std::env::consts::OS {
186 "macos" => "darwin",
187 other => other,
188 };
189 let arch = match std::env::consts::ARCH {
190 "aarch64" => "arm64",
191 "x86_64" => "amd64",
192 other => other,
193 };
194
195 let config = serde_json::json!({
197 "serviceAccountToken": token,
198 "programmingLanguage": "Go", "sdkVersion": "0030101", "integrationName": "cuenv",
201 "integrationVersion": env!("CARGO_PKG_VERSION"),
202 "requestLibraryName": "net/http",
203 "requestLibraryVersion": "go1.23.0",
204 "os": os,
205 "osVersion": "0.0.0",
206 "architecture": arch,
207 });
208
209 let config_bytes =
210 serde_json::to_vec(&config).map_err(|e| SecretError::ResolutionFailed {
211 name: "onepassword".to_string(),
212 message: format!("Failed to serialize config: {e}"),
213 })?;
214
215 let result = self
216 .plugin
217 .call::<_, String>("init_client", config_bytes)
218 .map_err(|e| SecretError::ResolutionFailed {
219 name: "onepassword".to_string(),
220 message: format!("Failed to initialize client: {e}"),
221 })?;
222
223 let response: serde_json::Value =
227 serde_json::from_str(&result).map_err(|e| SecretError::ResolutionFailed {
228 name: "onepassword".to_string(),
229 message: format!("Failed to parse init_client response: {e}"),
230 })?;
231
232 if let Some(error_name) = response.get("name") {
234 let message = response
235 .get("message")
236 .and_then(|m| m.as_str())
237 .unwrap_or("unknown error");
238 return Err(SecretError::ResolutionFailed {
239 name: "onepassword".to_string(),
240 message: format!("1Password error ({error_name}): {message}"),
241 });
242 }
243
244 let client_id = response
246 .as_u64()
247 .ok_or_else(|| SecretError::ResolutionFailed {
248 name: "onepassword".to_string(),
249 message: format!("Expected client ID number, got: {result}"),
250 })?;
251
252 tracing::debug!(client_id, "1Password client initialized");
253 Ok(client_id)
254 }
255
256 pub fn invoke(
271 &mut self,
272 client_id: u64,
273 method: &str,
274 params: &serde_json::Map<String, serde_json::Value>,
275 context: &str,
276 ) -> Result<String, SecretError> {
277 let request = serde_json::json!({
280 "invocation": {
281 "clientId": client_id,
282 "parameters": {
283 "name": method,
284 "parameters": params
285 }
286 }
287 });
288
289 let request_bytes =
290 serde_json::to_vec(&request).map_err(|e| SecretError::ResolutionFailed {
291 name: context.to_string(),
292 message: format!("Failed to serialize invoke request: {e}"),
293 })?;
294
295 let result = self
296 .plugin
297 .call::<_, String>("invoke", request_bytes)
298 .map_err(|e| SecretError::ResolutionFailed {
299 name: context.to_string(),
300 message: format!("1Password invoke failed: {e}"),
301 })?;
302
303 let response: serde_json::Value =
305 serde_json::from_str(&result).map_err(|e| SecretError::ResolutionFailed {
306 name: context.to_string(),
307 message: format!("Failed to parse invoke response: {e}"),
308 })?;
309
310 if let Some(error_name) = response.get("name") {
312 let message = response
313 .get("message")
314 .and_then(|m| m.as_str())
315 .unwrap_or("unknown error");
316 return Err(SecretError::ResolutionFailed {
317 name: context.to_string(),
318 message: format!("1Password error ({error_name}): {message}"),
319 });
320 }
321
322 Ok(result)
323 }
324
325 pub fn release_client(&mut self, client_id: u64) {
329 if let Ok(client_id_bytes) = serde_json::to_vec(&client_id) {
331 let _ = self
332 .plugin
333 .call::<_, String>("release_client", client_id_bytes);
334 }
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_shared_core_lazy_init() {
344 let _ = &SHARED_CORE;
347 }
348
349 #[test]
350 fn test_create_host_functions_returns_four_functions() {
351 let functions = create_host_functions();
352 assert_eq!(functions.len(), 4, "Should create 4 host functions");
353 }
354
355 #[test]
356 fn test_host_functions_are_valid() {
357 let functions = create_host_functions();
359
360 assert_eq!(functions.len(), 4, "Should create exactly 4 host functions");
366 }
367
368 #[test]
369 fn test_create_host_functions_can_be_created_multiple_times() {
370 let first = create_host_functions();
372 let second = create_host_functions();
373 assert_eq!(first.len(), second.len());
374 }
375
376 #[test]
377 fn test_shared_core_static_is_mutex() {
378 let guard = SHARED_CORE.lock();
380 assert!(guard.is_ok(), "Should be able to lock the mutex");
381 }
383
384 #[test]
385 fn test_os_mapping() {
386 let os = match std::env::consts::OS {
388 "macos" => "darwin",
389 other => other,
390 };
391
392 if std::env::consts::OS == "macos" {
394 assert_eq!(os, "darwin");
395 }
396 }
397
398 #[test]
399 fn test_arch_mapping() {
400 let arch = match std::env::consts::ARCH {
402 "aarch64" => "arm64",
403 "x86_64" => "amd64",
404 other => other,
405 };
406
407 if std::env::consts::ARCH == "aarch64" {
409 assert_eq!(arch, "arm64");
410 }
411 if std::env::consts::ARCH == "x86_64" {
413 assert_eq!(arch, "amd64");
414 }
415 }
416
417 #[test]
418 fn test_os_mapping_linux() {
419 let os = match "linux" {
421 "macos" => "darwin",
422 other => other,
423 };
424 assert_eq!(os, "linux");
425 }
426
427 #[test]
428 fn test_os_mapping_windows() {
429 let os = match "windows" {
431 "macos" => "darwin",
432 other => other,
433 };
434 assert_eq!(os, "windows");
435 }
436
437 #[test]
438 fn test_arch_mapping_arm() {
439 let arch = match "arm" {
441 "aarch64" => "arm64",
442 "x86_64" => "amd64",
443 other => other,
444 };
445 assert_eq!(arch, "arm");
446 }
447
448 #[test]
449 fn test_arch_mapping_riscv() {
450 let arch = match "riscv64" {
451 "aarch64" => "arm64",
452 "x86_64" => "amd64",
453 other => other,
454 };
455 assert_eq!(arch, "riscv64");
456 }
457
458 #[test]
459 fn test_host_functions_creates_random_fill() {
460 let functions = create_host_functions();
462 assert_eq!(functions.len(), 4);
464 }
465
466 #[test]
467 fn test_create_host_functions_no_side_effects() {
468 let _functions1 = create_host_functions();
470 let _functions2 = create_host_functions();
471 }
473
474 #[test]
475 fn test_shared_core_mutex_initial_state() {
476 let guard = SHARED_CORE.lock();
478 assert!(guard.is_ok());
479 let inner = guard.unwrap();
480 let _ = inner.is_none() || inner.is_some();
483 }
484
485 #[test]
486 fn test_get_or_init_without_wasm_returns_error() {
487 let result = SharedCore::get_or_init();
491 let _ = result.is_ok() || result.is_err();
493 }
494}