diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 789ca12a10..c87638ff3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: libayatana-appindicator3-dev \ librsvg2-dev \ patchelf - - run: cargo clippy --workspace -- -D warnings + - run: cargo clippy --workspace --all-targets -- -D warnings fmt: name: Format @@ -106,6 +106,22 @@ jobs: run: cargo install cargo-audit --locked - run: cargo audit + # ── WhatsApp gateway Node dependency audit ──────────────────────────────── + gateway-audit: + name: Gateway npm audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install dependencies (frozen lockfile) + working-directory: packages/whatsapp-gateway + run: npm ci + - name: Audit for high/critical vulnerabilities + working-directory: packages/whatsapp-gateway + run: npm audit --audit-level=high + # ── Secrets scanning (prevent accidental credential commits) ────────────── secrets: name: Secrets Scan diff --git a/Cargo.lock b/Cargo.lock index e880570247..f30243e281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,7 +139,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -150,7 +150,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -893,7 +893,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1555,7 +1555,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1805,7 +1805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2743,7 +2743,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3255,9 +3255,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lettre" -version = "0.11.21" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabda5859ee7c06b995b9d1165aa52c39110e079ef609db97178d86aeb051fa7" +checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349" dependencies = [ "async-trait", "base64 0.22.1", @@ -3739,7 +3739,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5105,7 +5105,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.3", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -5143,7 +5143,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -5709,7 +5709,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5768,7 +5768,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6314,7 +6314,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7050,7 +7050,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7622,7 +7622,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8495,7 +8495,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/SECURITY.md b/SECURITY.md index 273d2cab00..44859504e4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ | Version | Supported | |---------|--------------------| -| 0.3.x | :white_check_mark: | +| 0.6.x | :white_check_mark: | ## Reporting a Vulnerability diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index cebb1f599a..5e1e0368c4 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -17,6 +17,7 @@ use openfang_types::agent::{AgentId, AgentIdentity, AgentManifest}; use std::collections::HashMap; use std::sync::{Arc, LazyLock}; use std::time::Instant; +use zeroize::Zeroizing; /// Shared application state. /// @@ -43,6 +44,11 @@ pub struct AppState { /// Thread-safe mutable budget config. Updated via PUT /api/budget. /// Initialized from `kernel.config.budget` at startup. pub budget_config: Arc>, + /// Runtime API key store. Replaces `std::env::set_var`/`remove_var` (unsound in + /// multithreaded contexts). Keys written here are checked by `detect_auth_with_keys` + /// and presence checks throughout the API layer. Uses `std::sync::RwLock` (not + /// tokio's) so synchronous helpers like `build_field_json` can read without awaiting. + pub api_keys: Arc>>>, } /// POST /api/agents — Spawn a new agent. @@ -2392,15 +2398,18 @@ fn is_channel_configured(config: &openfang_types::config::ChannelsConfig, name: } } -/// Build a JSON field descriptor, checking env var presence but never exposing secrets. +/// Build a JSON field descriptor, checking key presence but never exposing secrets. /// For non-secret fields, includes the actual config value from `config_values` if available. fn build_field_json( f: &ChannelField, config_values: Option<&serde_json::Value>, + api_keys: &HashMap>, ) -> serde_json::Value { let has_value = f .env_var - .map(|ev| std::env::var(ev).map(|v| !v.is_empty()).unwrap_or(false)) + .map(|ev| { + api_keys.contains_key(ev) || std::env::var(ev).map(|v| !v.is_empty()).unwrap_or(false) + }) .unwrap_or(false); let mut field = serde_json::json!({ "key": f.key, @@ -2651,6 +2660,7 @@ pub async fn list_channels(State(state): State>) -> impl IntoRespo // Read the live channels config (updated on every hot-reload) instead of the // stale boot-time kernel.config, so newly configured channels show correctly. let live_channels = state.channels_config.read().await; + let api_keys = state.api_keys.read().unwrap_or_else(|e| e.into_inner()); let mut channels = Vec::new(); let mut configured_count = 0u32; @@ -2667,7 +2677,10 @@ pub async fn list_channels(State(state): State>) -> impl IntoRespo .filter(|f| f.required && f.env_var.is_some()) .all(|f| { f.env_var - .map(|ev| std::env::var(ev).map(|v| !v.is_empty()).unwrap_or(false)) + .map(|ev| { + api_keys.contains_key(ev) + || std::env::var(ev).map(|v| !v.is_empty()).unwrap_or(false) + }) .unwrap_or(true) }); @@ -2675,7 +2688,7 @@ pub async fn list_channels(State(state): State>) -> impl IntoRespo let fields: Vec = meta .fields .iter() - .map(|f| build_field_json(f, config_vals.as_ref())) + .map(|f| build_field_json(f, config_vals.as_ref(), &api_keys)) .collect(); channels.push(serde_json::json!({ @@ -2751,10 +2764,13 @@ pub async fn configure_channel( Json(serde_json::json!({"error": format!("Failed to write secret: {e}")})), ); } - // SAFETY: We are the only writer; this is a single-threaded config operation - unsafe { - std::env::set_var(env_var, value); - } + // Store in runtime key map so detect_auth_with_keys and presence checks + // reflect the new key without calling set_var (unsound in async context). + state + .api_keys + .write() + .unwrap_or_else(|e| e.into_inner()) + .insert(env_var.to_string(), Zeroizing::new(value.to_string())); // Also write the env var NAME to config.toml so the channel section // is not empty and the kernel knows which env var to read. config_fields.insert( @@ -2835,10 +2851,11 @@ pub async fn remove_channel( for field_def in meta.fields { if let Some(env_var) = field_def.env_var { let _ = remove_secret_env(&secrets_path, env_var); - // SAFETY: Single-threaded config operation - unsafe { - std::env::remove_var(env_var); - } + state + .api_keys + .write() + .unwrap_or_else(|e| e.into_inner()) + .remove(env_var); } } @@ -2881,6 +2898,7 @@ pub async fn remove_channel( /// (for Telegram). When provided, sends a real test message to verify the bot can /// post to that channel. pub async fn test_channel( + State(state): State>, Path(name): Path, raw_body: axum::body::Bytes, ) -> impl IntoResponse { @@ -2894,12 +2912,23 @@ pub async fn test_channel( } }; + // Clone the keys map immediately so no RwLockReadGuard (which is !Send) + // is held across any await point in this async function. + let api_keys: HashMap> = state + .api_keys + .read() + .unwrap_or_else(|e| e.into_inner()) + .clone(); // Check all required env vars are set let mut missing = Vec::new(); for field_def in meta.fields { if field_def.required { if let Some(env_var) = field_def.env_var { - if std::env::var(env_var).map(|v| v.is_empty()).unwrap_or(true) { + let has = api_keys.contains_key(env_var) + || std::env::var(env_var) + .map(|v| !v.is_empty()) + .unwrap_or(false); + if !has { missing.push(env_var); } } @@ -2929,7 +2958,7 @@ pub async fn test_channel( .map(|s| s.to_string()); if let Some(target_id) = target { - match send_channel_test_message(&name, &target_id).await { + match send_channel_test_message(&name, &target_id, &api_keys).await { Ok(()) => { return ( StatusCode::OK, @@ -2961,14 +2990,24 @@ pub async fn test_channel( } /// Send a real test message to a specific channel/chat on the given platform. -async fn send_channel_test_message(channel_name: &str, target_id: &str) -> Result<(), String> { +async fn send_channel_test_message( + channel_name: &str, + target_id: &str, + api_keys: &HashMap>, +) -> Result<(), String> { + let resolve = |var: &str| -> Result { + api_keys + .get(var) + .map(|z| z.to_string()) + .or_else(|| std::env::var(var).ok().filter(|v| !v.is_empty())) + .ok_or_else(|| format!("{var} not set")) + }; let client = reqwest::Client::new(); let test_msg = "OpenFang test message — your channel is connected!"; match channel_name { "discord" => { - let token = std::env::var("DISCORD_BOT_TOKEN") - .map_err(|_| "DISCORD_BOT_TOKEN not set".to_string())?; + let token = resolve("DISCORD_BOT_TOKEN")?; let url = format!("https://discord.com/api/v10/channels/{target_id}/messages"); let resp = client .post(&url) @@ -2983,8 +3022,7 @@ async fn send_channel_test_message(channel_name: &str, target_id: &str) -> Resul } } "telegram" => { - let token = std::env::var("TELEGRAM_BOT_TOKEN") - .map_err(|_| "TELEGRAM_BOT_TOKEN not set".to_string())?; + let token = resolve("TELEGRAM_BOT_TOKEN")?; let url = format!("https://api.telegram.org/bot{token}/sendMessage"); let resp = client .post(&url) @@ -2998,8 +3036,7 @@ async fn send_channel_test_message(channel_name: &str, target_id: &str) -> Resul } } "slack" => { - let token = std::env::var("SLACK_BOT_TOKEN") - .map_err(|_| "SLACK_BOT_TOKEN not set".to_string())?; + let token = resolve("SLACK_BOT_TOKEN")?; let url = "https://slack.com/api/chat.postMessage"; let resp = client .post(url) @@ -6170,7 +6207,7 @@ pub async fn security_status(State(state): State>) -> impl IntoRes "fuel_metering": true, "epoch_interruption": true, "default_timeout_secs": 30, - "default_fuel_limit": 1_000_000u64 + "default_fuel_limit": openfang_types::agent::ResourceQuota::default().max_cpu_time_ms * 100_000 }, "auth": { "mode": auth_mode, @@ -7705,16 +7742,24 @@ pub async fn set_provider_key( ); } - // Set env var in current process so detect_auth picks it up - std::env::set_var(&env_var, &key); - - // Refresh auth detection + // Store in runtime key map so detect_auth_with_keys and presence checks + // reflect the new key without calling set_var (unsound in async context). state - .kernel - .model_catalog + .api_keys .write() .unwrap_or_else(|e| e.into_inner()) - .detect_auth(); + .insert(env_var.clone(), Zeroizing::new(key.clone())); + + // Refresh auth detection + { + let keys = state.api_keys.read().unwrap_or_else(|e| e.into_inner()); + state + .kernel + .model_catalog + .write() + .unwrap_or_else(|e| e.into_inner()) + .detect_auth_with_keys(&keys); + } // Auto-switch default provider if current default has no working key. // This fixes the common case where a user adds e.g. a Gemini key via dashboard @@ -7740,10 +7785,11 @@ pub async fn set_provider_key( let current_has_key = if current_key_env.is_empty() { false } else { - std::env::var(¤t_key_env) - .ok() - .filter(|v| !v.is_empty()) - .is_some() + state + .api_keys + .read() + .unwrap_or_else(|e| e.into_inner()) + .contains_key(¤t_key_env) }; let switched = if !current_has_key && current_provider != name { // Find a default model for the newly-keyed provider @@ -7877,16 +7923,23 @@ pub async fn delete_provider_key( ); } - // Remove from process environment - std::env::remove_var(&env_var); - - // Refresh auth detection + // Remove from runtime key map state - .kernel - .model_catalog + .api_keys .write() .unwrap_or_else(|e| e.into_inner()) - .detect_auth(); + .remove(&env_var); + + // Refresh auth detection + { + let keys = state.api_keys.read().unwrap_or_else(|e| e.into_inner()); + state + .kernel + .model_catalog + .write() + .unwrap_or_else(|e| e.into_inner()) + .detect_auth_with_keys(&keys); + } ( StatusCode::OK, @@ -7927,7 +7980,13 @@ pub async fn test_provider( } }; - let api_key = std::env::var(&env_var).ok(); + let api_key = state + .api_keys + .read() + .unwrap_or_else(|e| e.into_inner()) + .get(&env_var) + .cloned() + .or_else(|| state.kernel.resolve_credential(&env_var)); // Only require API key for providers that need one (skip local providers like ollama/vllm/lmstudio) if key_required && api_key.is_none() && !env_var.is_empty() { return ( @@ -7940,7 +7999,7 @@ pub async fn test_provider( let start = std::time::Instant::now(); let driver_config = openfang_runtime::llm_driver::DriverConfig { provider: name.clone(), - api_key, + api_key: api_key.map(|z| z.to_string()), base_url: if base_url.is_empty() { None } else { @@ -12061,16 +12120,26 @@ pub async fn copilot_oauth_poll( ); } - // Set in current process - std::env::set_var("GITHUB_TOKEN", access_token.as_str()); - - // Refresh auth detection + // Store in runtime key map (avoids set_var which is unsound in async context) state - .kernel - .model_catalog + .api_keys .write() .unwrap_or_else(|e| e.into_inner()) - .detect_auth(); + .insert( + "GITHUB_TOKEN".to_string(), + Zeroizing::new(access_token.to_string()), + ); + + // Refresh auth detection + { + let keys = state.api_keys.read().unwrap_or_else(|e| e.into_inner()); + state + .kernel + .model_catalog + .write() + .unwrap_or_else(|e| e.into_inner()) + .detect_auth_with_keys(&keys); + } // Clean up flow state COPILOT_FLOWS.remove(&poll_id); diff --git a/crates/openfang-api/src/server.rs b/crates/openfang-api/src/server.rs index a1a2bc9c06..6dfb4a2c8d 100644 --- a/crates/openfang-api/src/server.rs +++ b/crates/openfang-api/src/server.rs @@ -8,6 +8,7 @@ use crate::webchat; use crate::ws; use axum::Router; use openfang_kernel::OpenFangKernel; +use std::collections::HashMap; use std::net::SocketAddr; use std::path::Path; use std::sync::Arc; @@ -41,6 +42,24 @@ pub async fn build_router( // Start channel bridges (Telegram, etc.) let bridge = channel_bridge::start_channel_bridge(kernel.clone()).await; + // Pre-populate the runtime API key map from the credential resolver so keys + // stored in vault or secrets.env are visible at startup without set_var. + let api_keys = { + let mut map = HashMap::new(); + let catalog = kernel + .model_catalog + .read() + .unwrap_or_else(|e| e.into_inner()); + for provider in catalog.list_providers() { + if !provider.api_key_env.is_empty() { + if let Some(val) = kernel.resolve_credential(&provider.api_key_env) { + map.insert(provider.api_key_env.clone(), val); + } + } + } + Arc::new(std::sync::RwLock::new(map)) + }; + let channels_config = kernel.config.channels.clone(); let state = Arc::new(AppState { kernel: kernel.clone(), @@ -52,6 +71,7 @@ pub async fn build_router( clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(kernel.config.budget.clone())), + api_keys, }); // Start WS cron broadcaster — subscribes to kernel event bus and pushes diff --git a/crates/openfang-api/tests/api_integration_test.rs b/crates/openfang-api/tests/api_integration_test.rs index d31a2ef651..77b49f94a4 100644 --- a/crates/openfang-api/tests/api_integration_test.rs +++ b/crates/openfang-api/tests/api_integration_test.rs @@ -13,6 +13,7 @@ use openfang_api::routes::{self, AppState}; use openfang_api::ws; use openfang_kernel::OpenFangKernel; use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use std::collections::HashMap; use std::sync::Arc; use std::time::Instant; use tower_http::cors::CorsLayer; @@ -80,6 +81,7 @@ async fn start_test_server_with_provider( clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), + api_keys: Arc::new(std::sync::RwLock::new(HashMap::new())), }); let app = Router::new() @@ -900,7 +902,7 @@ async fn start_test_server_with_auth(api_key: &str) -> TestServer { let config = KernelConfig { home_dir: tmp.path().to_path_buf(), data_dir: tmp.path().join("data"), - api_key: api_key.to_string(), + api_key: zeroize::Zeroizing::new(api_key.to_string()), default_model: DefaultModelConfig { provider: "ollama".to_string(), model: "test-model".to_string(), @@ -925,6 +927,7 @@ async fn start_test_server_with_auth(api_key: &str) -> TestServer { clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), + api_keys: Arc::new(std::sync::RwLock::new(HashMap::new())), }); let api_key = state.kernel.config.api_key.trim().to_string(); diff --git a/crates/openfang-api/tests/daemon_lifecycle_test.rs b/crates/openfang-api/tests/daemon_lifecycle_test.rs index c62cba9b7a..744440442c 100644 --- a/crates/openfang-api/tests/daemon_lifecycle_test.rs +++ b/crates/openfang-api/tests/daemon_lifecycle_test.rs @@ -9,6 +9,7 @@ use openfang_api::routes::{self, AppState}; use openfang_api::server::{read_daemon_info, DaemonInfo}; use openfang_kernel::OpenFangKernel; use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use std::collections::HashMap; use std::sync::Arc; use std::time::Instant; use tower_http::cors::CorsLayer; @@ -117,6 +118,7 @@ async fn test_full_daemon_lifecycle() { clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), + api_keys: Arc::new(std::sync::RwLock::new(HashMap::new())), }); let app = Router::new() @@ -244,6 +246,7 @@ async fn test_server_immediate_responsiveness() { clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), + api_keys: Arc::new(std::sync::RwLock::new(HashMap::new())), }); let app = Router::new() diff --git a/crates/openfang-api/tests/load_test.rs b/crates/openfang-api/tests/load_test.rs index c0bfc9ae59..3005278788 100644 --- a/crates/openfang-api/tests/load_test.rs +++ b/crates/openfang-api/tests/load_test.rs @@ -10,6 +10,7 @@ use openfang_api::middleware; use openfang_api::routes::{self, AppState}; use openfang_kernel::OpenFangKernel; use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, Instant}; use tower_http::cors::CorsLayer; @@ -61,6 +62,7 @@ async fn start_test_server() -> TestServer { clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), + api_keys: Arc::new(std::sync::RwLock::new(HashMap::new())), }); let app = Router::new() diff --git a/crates/openfang-api/tests/skill_config_api_test.rs b/crates/openfang-api/tests/skill_config_api_test.rs index e8a1e60a72..9e0a72a6f5 100644 --- a/crates/openfang-api/tests/skill_config_api_test.rs +++ b/crates/openfang-api/tests/skill_config_api_test.rs @@ -13,6 +13,7 @@ use openfang_api::middleware; use openfang_api::routes::{self, AppState}; use openfang_kernel::OpenFangKernel; use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use std::collections::HashMap; use std::sync::Arc; use std::time::Instant; use tower_http::cors::CorsLayer; @@ -98,6 +99,7 @@ async fn start_test_server() -> TestServer { clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), + api_keys: Arc::new(std::sync::RwLock::new(HashMap::new())), }); let app = Router::new() diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 8f59414c97..defbf7b3c0 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -609,7 +609,7 @@ impl OpenFangKernel { let key = key.trim().to_string(); if !key.is_empty() { info!("Using API key from OPENFANG_API_KEY environment variable"); - config.api_key = key; + config.api_key = zeroize::Zeroizing::new(key); } } } @@ -2511,7 +2511,7 @@ impl OpenFangKernel { fuel_limit: entry.manifest.resources.max_cpu_time_ms * 100_000, max_memory_bytes: entry.manifest.resources.max_memory_bytes as usize, capabilities: caps, - timeout_secs: Some(30), + timeout_secs: Some((entry.manifest.resources.max_cpu_time_ms / 1000).max(30)), ssrf_allowed_hosts: self.config.web.fetch.ssrf_allowed_hosts.clone(), }; @@ -5238,6 +5238,11 @@ impl OpenFangKernel { // Best-effort kill — don't block shutdown on failure #[cfg(unix)] { + // SAFETY: `pid` was stored when this process spawned the WhatsApp + // gateway child; the Mutex guard is held so no concurrent mutation + // of the PID can occur. If the child has already exited the kernel + // will have reaped it (or it became a zombie), in which case + // `kill(pid, SIGTERM)` returns ESRCH and is a harmless no-op here. unsafe { libc::kill(pid as i32, libc::SIGTERM); } @@ -5284,12 +5289,11 @@ impl OpenFangKernel { /// the boot-time snapshot). This helper checks both sources so that custom /// providers work immediately without a daemon restart. /// Resolve a credential by env var name using the vault → dotenv → env var chain. - pub fn resolve_credential(&self, key: &str) -> Option { + pub fn resolve_credential(&self, key: &str) -> Option> { self.credential_resolver .lock() .unwrap_or_else(|e| e.into_inner()) .resolve(key) - .map(|z| z.to_string()) } /// Store a credential in the vault (best-effort — falls through silently if no vault). @@ -5649,7 +5653,7 @@ impl OpenFangKernel { let driver_config = DriverConfig { provider: agent_provider.clone(), - api_key, + api_key: api_key.map(|z| z.to_string()), base_url, skip_permissions: true, subprocess_timeout_secs: primary_timeout, @@ -5726,7 +5730,7 @@ impl OpenFangKernel { let resolved_to_default = fb.provider.is_empty() || fb.provider == "default"; let config = DriverConfig { provider: fb_provider.clone(), - api_key: fb_api_key, + api_key: fb_api_key.map(|z| z.to_string()), base_url: fb .base_url .clone() @@ -5766,7 +5770,7 @@ impl OpenFangKernel { }; let fb_config = DriverConfig { provider: fb.provider.clone(), - api_key: fb_api_key, + api_key: fb_api_key.map(|z| z.to_string()), base_url: fb .base_url .clone() @@ -5824,7 +5828,7 @@ impl OpenFangKernel { for var_name in &server_config.env { if std::env::var(var_name).is_err() { if let Some(val) = self.resolve_credential(var_name) { - std::env::set_var(var_name, &val); + std::env::set_var(var_name, val.as_str()); } } } diff --git a/crates/openfang-runtime/src/host_functions.rs b/crates/openfang-runtime/src/host_functions.rs index a3c3c29fb4..327cb42f1a 100644 --- a/crates/openfang-runtime/src/host_functions.rs +++ b/crates/openfang-runtime/src/host_functions.rs @@ -238,7 +238,7 @@ fn host_net_fetch(state: &GuestState, params: &serde_json::Value) -> serde_json: } state.tokio_handle.block_on(async { - let client = reqwest::Client::new(); + let client = &state.http_client; let request = match method.to_uppercase().as_str() { "POST" => client.post(url).body(body.to_string()), "PUT" => client.put(url).body(body.to_string()), @@ -433,6 +433,8 @@ mod tests { agent_id: "test-agent".to_string(), tokio_handle: tokio::runtime::Handle::current(), ssrf_allowed_hosts: Vec::new(), + http_client: reqwest::Client::new(), + memory_limiter: crate::sandbox::MemoryLimiter::new(0), } } diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index 4bf6fc7ec8..fa4f8a705f 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -15,6 +15,7 @@ use openfang_types::model_catalog::{ ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL, }; use std::collections::HashMap; +use zeroize::Zeroizing; /// The model catalog — registry of all known models and providers. pub struct ModelCatalog { @@ -116,6 +117,69 @@ impl ModelCatalog { } } + /// Detect which providers have API keys configured, also checking a runtime key map. + /// + /// Checks the provided `runtime_keys` map first, then falls back to `std::env::var()`. + /// This avoids calling `std::env::set_var` (unsound in multithreaded contexts) while + /// still reflecting keys that were added at runtime via the API. + pub fn detect_auth_with_keys(&mut self, runtime_keys: &HashMap>) { + for provider in &mut self.providers { + if provider.id == "claude-code" { + provider.auth_status = if crate::drivers::claude_code::claude_code_available() { + AuthStatus::Configured + } else { + AuthStatus::Missing + }; + continue; + } + if provider.id == "qwen-code" { + provider.auth_status = if crate::drivers::qwen_code::qwen_code_available() { + AuthStatus::Configured + } else { + AuthStatus::Missing + }; + continue; + } + if provider.id == "github-copilot" || provider.id == "copilot" { + let openfang_dir = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .map(|h| std::path::PathBuf::from(h).join(".openfang")) + .unwrap_or_else(|_| std::path::PathBuf::from(".openfang")); + provider.auth_status = if runtime_keys.contains_key("GITHUB_TOKEN") + || crate::drivers::copilot::copilot_auth_available(&openfang_dir) + { + AuthStatus::Configured + } else { + AuthStatus::Missing + }; + continue; + } + if !provider.key_required { + provider.auth_status = AuthStatus::NotRequired; + continue; + } + let has_key = runtime_keys.contains_key(&provider.api_key_env) + || std::env::var(&provider.api_key_env).is_ok(); + let has_fallback = match provider.id.as_str() { + "gemini" => { + runtime_keys.contains_key("GOOGLE_API_KEY") + || std::env::var("GOOGLE_API_KEY").is_ok() + } + "codex" => { + runtime_keys.contains_key("OPENAI_API_KEY") + || std::env::var("OPENAI_API_KEY").is_ok() + || read_codex_credential().is_some() + } + _ => false, + }; + provider.auth_status = if has_key || has_fallback { + AuthStatus::Configured + } else { + AuthStatus::Missing + }; + } + } + /// List all models in the catalog. pub fn list_models(&self) -> &[ModelCatalogEntry] { &self.models diff --git a/crates/openfang-runtime/src/sandbox.rs b/crates/openfang-runtime/src/sandbox.rs index d8aced6359..62e3b3f90d 100644 --- a/crates/openfang-runtime/src/sandbox.rs +++ b/crates/openfang-runtime/src/sandbox.rs @@ -26,6 +26,7 @@ use crate::host_functions; use crate::kernel_handle::KernelHandle; use openfang_types::capability::Capability; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tracing::debug; use wasmtime::*; @@ -35,7 +36,7 @@ use wasmtime::*; pub struct SandboxConfig { /// Maximum fuel (CPU instruction budget). 0 = unlimited. pub fuel_limit: u64, - /// Maximum WASM linear memory in bytes (reserved for future enforcement). + /// Maximum WASM linear memory in bytes. 0 = unlimited. pub max_memory_bytes: usize, /// Capabilities granted to this sandbox instance. pub capabilities: Vec, @@ -71,6 +72,13 @@ pub struct GuestState { pub tokio_handle: tokio::runtime::Handle, /// Hosts allowed to bypass SSRF private-IP checks (from config). pub ssrf_allowed_hosts: Vec, + /// Shared HTTP client for `host_net_fetch` — reqwest::Client maintains an + /// internal connection pool, so it must be created once and reused rather + /// than per-call. + pub http_client: reqwest::Client, + /// Memory growth limiter — enforces `max_memory_bytes` via `Store::limiter` + /// and records whether a grow was rejected so errors can be classified. + pub(crate) memory_limiter: MemoryLimiter, } /// Result of executing a WASM module. @@ -93,16 +101,71 @@ pub enum SandboxError { Execution(String), #[error("Fuel exhausted: skill exceeded CPU budget")] FuelExhausted, + #[error("Memory limit exceeded: skill exceeded memory budget")] + MemoryExceeded, #[error("Guest ABI violation: {0}")] AbiError(String), } +/// Wasmtime `ResourceLimiter` that caps linear memory growth to `max_bytes`. +/// +/// When `max_bytes` is 0 all grows are allowed (unlimited). When a grow is +/// rejected the `rejected` flag is set so the host can return +/// `SandboxError::MemoryExceeded` rather than a generic trap error. +pub(crate) struct MemoryLimiter { + max_bytes: usize, + /// Set to `true` the first time a memory grow is rejected. + pub(crate) rejected: bool, +} + +impl MemoryLimiter { + pub(crate) fn new(max_bytes: usize) -> Self { + Self { + max_bytes, + rejected: false, + } + } +} + +impl ResourceLimiter for MemoryLimiter { + /// Called whenever WASM linear memory attempts to grow. + /// + /// `desired` is the new total size in bytes after the grow. Returns + /// `Ok(true)` to allow, `Ok(false)` to deny (guest's `memory.grow` + /// instruction returns -1). + fn memory_growing( + &mut self, + _current: usize, + desired: usize, + _maximum: Option, + ) -> Result { + if self.max_bytes > 0 && desired > self.max_bytes { + self.rejected = true; + Ok(false) + } else { + Ok(true) + } + } + + fn table_growing( + &mut self, + _current: usize, + _desired: usize, + _maximum: Option, + ) -> Result { + Ok(true) + } +} + /// The WASM sandbox engine. /// /// Create one per kernel, reuse across skill invocations. The `Engine` /// is expensive to create but can compile/instantiate many modules. pub struct WasmSandbox { engine: Engine, + /// Shared HTTP client handed to every guest's `GuestState`. Created once + /// here so its connection pool is reused across all WASM host calls. + http_client: reqwest::Client, } impl WasmSandbox { @@ -112,7 +175,10 @@ impl WasmSandbox { config.consume_fuel(true); config.epoch_interruption(true); let engine = Engine::new(&config).map_err(|e| SandboxError::Compilation(e.to_string()))?; - Ok(Self { engine }) + Ok(Self { + engine, + http_client: reqwest::Client::new(), + }) } /// Execute a WASM module with the given JSON input. @@ -129,6 +195,7 @@ impl WasmSandbox { agent_id: &str, ) -> Result { let engine = self.engine.clone(); + let http_client = self.http_client.clone(); let wasm_bytes = wasm_bytes.to_vec(); let agent_id = agent_id.to_string(); let handle = tokio::runtime::Handle::current(); @@ -142,6 +209,7 @@ impl WasmSandbox { kernel, &agent_id, handle, + http_client, ) }) .await @@ -149,6 +217,7 @@ impl WasmSandbox { } /// Synchronous inner execution — runs on a blocking thread. + #[allow(clippy::too_many_arguments)] fn execute_sync( engine: &Engine, wasm_bytes: &[u8], @@ -157,6 +226,7 @@ impl WasmSandbox { kernel: Option>, agent_id: &str, tokio_handle: tokio::runtime::Handle, + http_client: reqwest::Client, ) -> Result { // Compile the module (accepts both .wasm binary and .wat text) let module = Module::new(engine, wasm_bytes) @@ -171,6 +241,8 @@ impl WasmSandbox { agent_id: agent_id.to_string(), tokio_handle, ssrf_allowed_hosts: config.ssrf_allowed_hosts.clone(), + http_client, + memory_limiter: MemoryLimiter::new(config.max_memory_bytes), }, ); @@ -185,11 +257,32 @@ impl WasmSandbox { store.set_epoch_deadline(1); let engine_clone = engine.clone(); let timeout = config.timeout_secs.unwrap_or(30); + + // Cancellable watchdog: polls every 100ms so it exits promptly when + // WASM finishes early rather than sleeping the full timeout duration. + // Without cancellation, one 30-second sleeping thread accumulates per + // invocation under load. + let cancel = Arc::new(AtomicBool::new(false)); + let cancel_watchdog = Arc::clone(&cancel); let _watchdog = std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(timeout)); - engine_clone.increment_epoch(); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout); + loop { + if cancel_watchdog.load(Ordering::Acquire) { + return; + } + let remaining = deadline.saturating_duration_since(std::time::Instant::now()); + if remaining.is_zero() { + engine_clone.increment_epoch(); + return; + } + std::thread::sleep(remaining.min(std::time::Duration::from_millis(100))); + } }); + // Enforce the per-sandbox linear memory cap. Must be registered before + // instantiation so the initial memory allocation is also subject to the limit. + store.limiter(|state| &mut state.memory_limiter); + // Build linker with host function imports let mut linker = Linker::new(engine); Self::register_host_functions(&mut linker)?; @@ -234,10 +327,21 @@ impl WasmSandbox { } mem_data[start..end].copy_from_slice(&input_bytes); - // Call guest execute - let packed = match execute_fn.call(&mut store, (input_ptr, input_bytes.len() as i32)) { + // Call guest execute, then immediately signal the watchdog to stop — + // WASM has finished regardless of success, fuel exhaustion, or timeout. + let packed_result = execute_fn.call(&mut store, (input_ptr, input_bytes.len() as i32)); + cancel.store(true, Ordering::Release); + + let packed = match packed_result { Ok(v) => v, Err(e) => { + // Memory rejection must be checked first: the limiter sets + // `rejected` when it denies a grow, after which the guest + // typically traps via `unreachable` or an OOB access — the + // trap code alone doesn't tell us *why* it fired. + if store.data().memory_limiter.rejected { + return Err(SandboxError::MemoryExceeded); + } // Check for fuel exhaustion via trap code if let Some(Trap::OutOfFuel) = e.downcast_ref::() { return Err(SandboxError::FuelExhausted); @@ -611,4 +715,59 @@ mod tests { "Expected unknown method error, got: {err_msg}" ); } + + /// Module that attempts to grow memory by 10 pages (640 KB) during execute. + /// When the limiter denies the grow, `memory.grow` returns -1; the module + /// traps via `unreachable` so the host can detect `SandboxError::MemoryExceeded`. + const MEMORY_GROW_WAT: &str = r#" + (module + (memory (export "memory") 1) + (global $bump (mut i32) (i32.const 1024)) + + (func (export "alloc") (param $size i32) (result i32) + (local $ptr i32) + (local.set $ptr (global.get $bump)) + (global.set $bump (i32.add (global.get $bump) (local.get $size))) + (local.get $ptr) + ) + + (func (export "execute") (param $ptr i32) (param $len i32) (result i64) + ;; Attempt to grow by 10 pages (640 KB). + ;; If the limiter rejects it, memory.grow returns -1; trap explicitly. + (memory.grow (i32.const 10)) + (i32.const -1) + (i32.eq) + (if (then unreachable)) + (i64.const 0) + ) + ) + "#; + + #[tokio::test] + async fn test_memory_limit_exceeded() { + let sandbox = WasmSandbox::new().unwrap(); + let input = serde_json::json!({}); + // Allow exactly 1 page (64 KB) — the module's initial allocation. + // Growing by 10 more pages would bring the total to 704 KB, well over the cap. + let config = SandboxConfig { + max_memory_bytes: 64 * 1024, + ..Default::default() + }; + + let err = sandbox + .execute( + MEMORY_GROW_WAT.as_bytes(), + input, + config, + None, + "test-agent", + ) + .await + .unwrap_err(); + + assert!( + matches!(err, SandboxError::MemoryExceeded), + "Expected MemoryExceeded, got: {err}" + ); + } } diff --git a/crates/openfang-runtime/src/tool_runner.rs b/crates/openfang-runtime/src/tool_runner.rs index 16695616ee..e2216e590c 100644 --- a/crates/openfang-runtime/src/tool_runner.rs +++ b/crates/openfang-runtime/src/tool_runner.rs @@ -328,8 +328,8 @@ pub async fn execute_tool( "image_analyze" => tool_image_analyze(input).await, // Media understanding tools - "media_describe" => tool_media_describe(input, media_engine).await, - "media_transcribe" => tool_media_transcribe(input, media_engine).await, + "media_describe" => tool_media_describe(input, media_engine, workspace_root).await, + "media_transcribe" => tool_media_transcribe(input, media_engine, workspace_root).await, // Image generation tool "image_generate" => tool_image_generate(input, workspace_root, media_engine).await, @@ -1371,7 +1371,7 @@ fn resolve_file_path(raw_path: &str, workspace_root: Option<&Path>) -> Result, ) -> Result { // Reject `..` components regardless of workspace. - let _ = validate_path(raw_path)?; + validate_path(raw_path)?; let Some(root) = workspace_root else { return Ok(PathBuf::from(raw_path)); @@ -2993,19 +2993,20 @@ fn tool_system_time() -> String { async fn tool_media_describe( input: &serde_json::Value, media_engine: Option<&crate::media_understanding::MediaEngine>, + workspace_root: Option<&Path>, ) -> Result { use base64::Engine; let engine = media_engine.ok_or("Media engine not available. Check media configuration.")?; let path = input["path"].as_str().ok_or("Missing 'path' parameter")?; - let _ = validate_path(path)?; + let resolved = resolve_file_path(path, workspace_root)?; // Read image file - let data = tokio::fs::read(path) + let data = tokio::fs::read(&resolved) .await .map_err(|e| format!("Failed to read image file: {e}"))?; // Detect MIME type from extension - let ext = std::path::Path::new(path) + let ext = resolved .extension() .and_then(|e| e.to_str()) .unwrap_or("") @@ -3038,19 +3039,20 @@ async fn tool_media_describe( async fn tool_media_transcribe( input: &serde_json::Value, media_engine: Option<&crate::media_understanding::MediaEngine>, + workspace_root: Option<&Path>, ) -> Result { use base64::Engine; let engine = media_engine.ok_or("Media engine not available. Check media configuration.")?; let path = input["path"].as_str().ok_or("Missing 'path' parameter")?; - let _ = validate_path(path)?; + let resolved = resolve_file_path(path, workspace_root)?; // Read audio file - let data = tokio::fs::read(path) + let data = tokio::fs::read(&resolved) .await .map_err(|e| format!("Failed to read audio file: {e}"))?; // Detect MIME type from extension - let ext = std::path::Path::new(path) + let ext = resolved .extension() .and_then(|e| e.to_str()) .unwrap_or("") diff --git a/crates/openfang-skills/src/clawhub.rs b/crates/openfang-skills/src/clawhub.rs index b844b07cf6..21d1f10a74 100644 --- a/crates/openfang-skills/src/clawhub.rs +++ b/crates/openfang-skills/src/clawhub.rs @@ -557,7 +557,7 @@ impl ClawHubClient { ".staging-{}-{}-{}", slug, std::process::id(), - chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0) + uuid::Uuid::new_v4() )); // Pre-clean any leftover staging dir from a prior crashed install. let _ = std::fs::remove_dir_all(&staging_dir); @@ -682,19 +682,34 @@ impl ClawHubClient { // Step 9: Atomic rename staging -> final. Closes the TOCTOU window // between extraction and enforcement (Codex Finding 1). If the final - // location already exists from a prior install, replace it - // atomically (where the filesystem allows) or fall back to - // remove-then-rename. - if final_dir.exists() { - if let Err(e) = std::fs::remove_dir_all(&final_dir) { + // location already exists from a prior install, move it aside to a + // backup name first, then rename staging into place, then delete the + // backup — this way the slot is never empty between removal and + // rename (remove-then-rename leaves a window where the slot doesn't + // exist, which is unsafe on platforms like Windows). + let backup_dir = if final_dir.exists() { + let backup = target_dir.join(format!( + ".backup-{}-{}-{}", + slug, + std::process::id(), + uuid::Uuid::new_v4() + )); + if let Err(e) = std::fs::rename(&final_dir, &backup) { let _ = std::fs::remove_dir_all(&skill_dir); return Err(SkillError::SecurityBlocked(format!( - "install: failed to clear existing {} before atomic rename: {e}", + "install: failed to move aside existing {} before atomic rename: {e}", final_dir.display() ))); } - } + Some(backup) + } else { + None + }; if let Err(e) = std::fs::rename(&skill_dir, &final_dir) { + // Restore the previous install so the slot isn't left empty. + if let Some(backup) = &backup_dir { + let _ = std::fs::rename(backup, &final_dir); + } let _ = std::fs::remove_dir_all(&skill_dir); return Err(SkillError::SecurityBlocked(format!( "install: atomic rename {} -> {} failed: {e}", @@ -702,6 +717,10 @@ impl ClawHubClient { final_dir.display() ))); } + // New install is in place; clean up the old version's backup. + if let Some(backup) = backup_dir { + let _ = std::fs::remove_dir_all(&backup); + } // From this point on the skill lives at final_dir, not staging. // (Variable kept for clarity even though unread; future tracing // additions reference it.) diff --git a/crates/openfang-types/src/agent.rs b/crates/openfang-types/src/agent.rs index 5589b1e35e..d07a8c57d3 100644 --- a/crates/openfang-types/src/agent.rs +++ b/crates/openfang-types/src/agent.rs @@ -230,11 +230,18 @@ pub enum ScheduleMode { #[default] Reactive, /// Agent wakes up on a cron schedule. - Periodic { cron: String }, + Periodic { + /// Cron expression controlling wake-up times. + cron: String, + }, /// Agent monitors conditions and acts when thresholds are met. - Proactive { conditions: Vec }, + Proactive { + /// Conditions to monitor; the agent acts when one is met. + conditions: Vec, + }, /// Agent runs in a persistent loop. Continuous { + /// Seconds between consecutive loop iterations. #[serde(default = "default_check_interval")] check_interval_secs: u64, }, @@ -299,13 +306,20 @@ pub enum Priority { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ToolProfile { + /// File read/list only. Minimal, + /// File read/write/list, shell exec, and web fetch. Coding, + /// Web fetch/search plus file read/write. Research, + /// Inter-agent messaging and memory tools. Messaging, + /// Combines coding, research, and messaging tools. Automation, + /// All tools (`*`). #[default] Full, + /// User-defined tool list — no implied tools. Custom, } @@ -405,10 +419,14 @@ impl Default for ModelConfig { /// A fallback model entry in a chain. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FallbackModel { + /// LLM provider name. pub provider: String, + /// Model identifier. pub model: String, + /// Optional API key environment variable name. #[serde(default)] pub api_key_env: Option, + /// Optional base URL override for the provider. #[serde(default)] pub base_url: Option, } diff --git a/crates/openfang-types/src/approval.rs b/crates/openfang-types/src/approval.rs index 157efed270..df0047c703 100644 --- a/crates/openfang-types/src/approval.rs +++ b/crates/openfang-types/src/approval.rs @@ -36,9 +36,13 @@ const MAX_TIMEOUT_SECS: u64 = 300; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RiskLevel { + /// Routine operation; informational only. Low, + /// Operation that warrants operator awareness. Medium, + /// Potentially destructive or irreversible operation. High, + /// Severe operation that could cause major damage if misused. Critical, } @@ -62,8 +66,11 @@ impl RiskLevel { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ApprovalDecision { + /// The operator approved the request. Approved, + /// The operator denied the request. Denied, + /// No decision was made before the timeout elapsed; treated as a denial. TimedOut, } @@ -74,13 +81,19 @@ pub enum ApprovalDecision { /// An approval request for a dangerous agent operation. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApprovalRequest { + /// Unique identifier for this request. pub id: Uuid, + /// Identifier of the agent that triggered the request. pub agent_id: String, + /// Name of the tool requiring approval. pub tool_name: String, + /// Human-readable description of the requested operation. pub description: String, /// The specific action being requested (sanitized for display). pub action_summary: String, + /// Assessed risk level of the operation. pub risk_level: RiskLevel, + /// When the request was created. pub requested_at: DateTime, /// Auto-deny timeout in seconds. pub timeout_secs: u64, @@ -152,9 +165,13 @@ impl ApprovalRequest { /// Response to an approval request. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApprovalResponse { + /// Identifier of the [`ApprovalRequest`] this response answers. pub request_id: Uuid, + /// The decision reached. pub decision: ApprovalDecision, + /// When the decision was made. pub decided_at: DateTime, + /// Identifier of the operator who made the decision, if known. pub decided_by: Option, } diff --git a/crates/openfang-types/src/comms.rs b/crates/openfang-types/src/comms.rs index a6a694f9ca..33dec30edd 100644 --- a/crates/openfang-types/src/comms.rs +++ b/crates/openfang-types/src/comms.rs @@ -43,7 +43,9 @@ pub enum EdgeKind { /// The full agent topology: nodes + edges. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Topology { + /// Agents in the topology. pub nodes: Vec, + /// Relationships between agents. pub edges: Vec, } @@ -89,16 +91,22 @@ pub enum CommsEventKind { /// Request body for POST /api/comms/send. #[derive(Debug, Clone, Deserialize)] pub struct CommsSendRequest { + /// ID of the sending agent. pub from_agent_id: String, + /// ID of the receiving agent. pub to_agent_id: String, + /// Message content. pub message: String, } /// Request body for POST /api/comms/task. #[derive(Debug, Clone, Deserialize)] pub struct CommsTaskRequest { + /// Short task title. pub title: String, + /// Full task description. pub description: String, + /// ID of the agent the task is assigned to, if any. #[serde(default)] pub assigned_to: Option, } diff --git a/crates/openfang-types/src/media.rs b/crates/openfang-types/src/media.rs index c8f767c091..4c1cc59018 100644 --- a/crates/openfang-types/src/media.rs +++ b/crates/openfang-types/src/media.rs @@ -6,8 +6,11 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MediaType { + /// A still image. Image, + /// An audio clip. Audio, + /// A video clip. Video, } @@ -26,11 +29,22 @@ impl std::fmt::Display for MediaType { #[serde(rename_all = "snake_case", tag = "type")] pub enum MediaSource { /// Path to a local file. - FilePath { path: String }, + FilePath { + /// Filesystem path to the media file. + path: String, + }, /// URL to fetch the media from (SSRF-checked). - Url { url: String }, + Url { + /// URL to fetch the media from. + url: String, + }, /// Base64-encoded data. - Base64 { data: String, mime_type: String }, + Base64 { + /// Base64-encoded media bytes. + data: String, + /// MIME type of the decoded data. + mime_type: String, + }, } /// A media attachment to be analyzed. @@ -245,9 +259,12 @@ impl MediaAttachment { #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum ImageGenModel { + /// OpenAI DALL-E 3. #[default] DallE3, + /// OpenAI DALL-E 2. DallE2, + /// OpenAI GPT Image 1. #[serde(rename = "gpt-image-1")] GptImage1, } @@ -293,9 +310,11 @@ fn default_image_count() -> u8 { 1 } -/// Allowed sizes per model. +/// Allowed sizes for DALL-E 3. pub const DALLE3_SIZES: &[&str] = &["1024x1024", "1792x1024", "1024x1792"]; +/// Allowed sizes for DALL-E 2. pub const DALLE2_SIZES: &[&str] = &["256x256", "512x512", "1024x1024"]; +/// Allowed sizes for GPT Image 1. pub const GPT_IMAGE1_SIZES: &[&str] = &["1024x1024", "1536x1024", "1024x1536"]; impl ImageGenRequest { diff --git a/packages/whatsapp-gateway/index.js b/packages/whatsapp-gateway/index.js index 973d6b3f0b..cc18494edf 100644 --- a/packages/whatsapp-gateway/index.js +++ b/packages/whatsapp-gateway/index.js @@ -1,7 +1,7 @@ #!/usr/bin/env node import http from 'node:http'; -import { randomUUID } from 'node:crypto'; +import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -19,11 +19,52 @@ const PORT = parseInt(process.env.WHATSAPP_GATEWAY_PORT || '3009', 10); const OPENFANG_URL = (process.env.OPENFANG_URL || 'http://127.0.0.1:4200').replace(/\/+$/, ''); const DEFAULT_AGENT = process.env.OPENFANG_DEFAULT_AGENT || 'assistant'; +// --------------------------------------------------------------------------- +// Auth — shared-secret bearer token +// --------------------------------------------------------------------------- +// Generated fresh on every startup and printed once so the Rust kernel (which +// spawned this process) can capture it and attach it to every request. This +// is a localhost service-to-service API, not a browser-facing one — there is +// no session/cookie auth, just a static bearer token for the process lifetime. +const GATEWAY_TOKEN = randomBytes(32).toString('hex'); +console.log(`[gateway] Auth token: ${GATEWAY_TOKEN}`); + +// --------------------------------------------------------------------------- +// Log sanitization helpers — never write raw PII or attacker-controlled +// strings (push names come from the sender's WhatsApp profile) to stdout. +// --------------------------------------------------------------------------- +function redactPhone(phone) { + const digits = String(phone || '').replace(/\D/g, ''); + if (digits.length <= 4) return '***' + digits; + return '***' + digits.slice(-4); +} + +function sanitizeForLog(text) { + // Strip C0/C1 control characters (including ANSI escapes) so log output + // can't be forged or split by attacker-controlled push names. + // eslint-disable-next-line no-control-regex + return String(text || '').replace(/[\x00-\x1f\x7f-\x9f]/g, ''); +} + +function isAuthorized(req) { + const header = req.headers['authorization'] || ''; + const prefix = 'Bearer '; + if (!header.startsWith(prefix)) return false; + + const provided = Buffer.from(header.slice(prefix.length)); + const expected = Buffer.from(GATEWAY_TOKEN); + // timingSafeEqual requires equal-length buffers — mismatched length means + // the token is wrong, but we still need a constant-time comparison for the + // case where lengths happen to match. + if (provided.length !== expected.length) return false; + return timingSafeEqual(provided, expected); +} + // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- let sock = null; // Baileys socket -let sessionId = ''; // current session identifier +let connectionId = ''; // current connection identifier (informational only — not a credential) let qrDataUrl = ''; // latest QR code as data:image/png;base64,... let connStatus = 'disconnected'; // disconnected | qr_ready | connected let qrExpired = false; @@ -39,9 +80,21 @@ async function startConnection() { const authDir = path.join(__dirname, 'auth_store'); const { state, saveCreds } = await useMultiFileAuthState(authDir); + + // SECURITY: auth_store contains the WhatsApp session's private keys in + // plaintext JSON. Restrict it to the owning user (POSIX only — Windows + // ACLs default to the owning user's profile and chmod is a no-op there). + if (process.platform !== 'win32') { + try { + fs.chmodSync(authDir, 0o700); + } catch (err) { + console.error('[gateway] Failed to restrict auth_store permissions:', err.message); + } + } + const { version } = await fetchLatestBaileysVersion(); - sessionId = randomUUID(); + connectionId = randomUUID(); qrDataUrl = ''; qrExpired = false; connStatus = 'disconnected'; @@ -51,7 +104,6 @@ async function startConnection() { version, auth: state, logger, - printQRInTerminal: true, browser: ['OpenFang', 'Desktop', '1.0.0'], }); @@ -153,12 +205,14 @@ async function startConnection() { sender: phone, sender_name: pushName, }; + const logName = sanitizeForLog(pushName); + const logPhone = redactPhone(phone); if (isGroup) { metadata.group_jid = remoteJid; metadata.is_group = true; - console.log(`[gateway] Group msg from ${pushName} (${phone}) in ${remoteJid}: ${text.substring(0, 80)}`); + console.log(`[gateway] Group msg from ${logName} (${logPhone}) in ${remoteJid}: ${text.substring(0, 80)}`); } else { - console.log(`[gateway] Incoming from ${pushName} (${phone}): ${text.substring(0, 80)}`); + console.log(`[gateway] Incoming from ${logName} (${logPhone}): ${text.substring(0, 80)}`); } // Forward to OpenFang agent @@ -168,7 +222,7 @@ async function startConnection() { // Reply in the same context: group → group, DM → DM const replyJid = isGroup ? remoteJid : senderJid.replace(/@.*$/, '') + '@s.whatsapp.net'; await sock.sendMessage(replyJid, { text: response }); - console.log(`[gateway] Replied to ${pushName}${isGroup ? ' in group ' + remoteJid : ''}`); + console.log(`[gateway] Replied to ${logName}${isGroup ? ' in group ' + remoteJid : ''}`); } } catch (err) { console.error(`[gateway] Forward/reply failed:`, err.message); @@ -267,20 +321,17 @@ function jsonResponse(res, status, data) { res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), - 'Access-Control-Allow-Origin': '*', }); res.end(body); } const server = http.createServer(async (req, res) => { - // CORS preflight - if (req.method === 'OPTIONS') { - res.writeHead(204, { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - }); - return res.end(); + // SECURITY: this is a localhost service-to-service API consumed only by the + // Rust kernel that spawned us — not a browser API. No CORS headers are sent + // (a browser page must never be able to call these endpoints), and every + // request must present the shared-secret bearer token printed at startup. + if (!isAuthorized(req)) { + return jsonResponse(res, 401, { error: 'Unauthorized' }); } const url = new URL(req.url, `http://localhost:${PORT}`); @@ -293,7 +344,7 @@ const server = http.createServer(async (req, res) => { if (connStatus === 'connected') { return jsonResponse(res, 200, { qr_data_url: '', - session_id: sessionId, + connection_id: connectionId, message: 'Already connected to WhatsApp', connected: true, }); @@ -311,7 +362,7 @@ const server = http.createServer(async (req, res) => { return jsonResponse(res, 200, { qr_data_url: qrDataUrl, - session_id: sessionId, + connection_id: connectionId, message: statusMessage, connected: connStatus === 'connected', }); @@ -344,15 +395,15 @@ const server = http.createServer(async (req, res) => { return jsonResponse(res, 200, { status: 'ok', connected: connStatus === 'connected', - session_id: sessionId || null, + connection_id: connectionId || null, }); } // 404 jsonResponse(res, 404, { error: 'Not found' }); } catch (err) { - console.error(`[gateway] ${req.method} ${pathname} error:`, err.message); - jsonResponse(res, 500, { error: err.message }); + console.error(`[gateway] ${req.method} ${pathname} error:`, err); + jsonResponse(res, 500, { error: 'internal error' }); } }); diff --git a/packages/whatsapp-gateway/package-lock.json b/packages/whatsapp-gateway/package-lock.json new file mode 100644 index 0000000000..a641ff9727 --- /dev/null +++ b/packages/whatsapp-gateway/package-lock.json @@ -0,0 +1,1857 @@ +{ + "name": "@openfang/whatsapp-gateway", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openfang/whatsapp-gateway", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@whiskeysockets/baileys": "^6", + "pino": "^9", + "qrcode": "^1.5" + }, + "bin": { + "openfang-whatsapp-gateway": "index.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz", + "integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==", + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.4.1", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/node-cache": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz", + "integrity": "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==", + "license": "MIT", + "dependencies": { + "cacheable": "^2.3.1", + "hookified": "^1.14.0", + "keyv": "^5.5.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", + "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@hapi/boom": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", + "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "9.x.x" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@whiskeysockets/baileys": { + "version": "6.7.23", + "resolved": "https://registry.npmjs.org/@whiskeysockets/baileys/-/baileys-6.7.23.tgz", + "integrity": "sha512-UDbysXJpFKHC2S27qy4oABQc2uZekhUcCNi+Nvzev7wZkOhC72wqezzM2cfYB3PHJdy3Jadc2VkVv8eQeywIUw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@cacheable/node-cache": "^1.4.0", + "@hapi/boom": "^9.1.3", + "async-mutex": "^0.5.0", + "axios": "^1.6.0", + "libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git", + "music-metadata": "^11.7.0", + "pino": "^9.6", + "protobufjs": "^7.2.4", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "audio-decode": "^2.1.3", + "jimp": "^1.6.0", + "link-preview-js": "^3.0.0", + "sharp": "*" + }, + "peerDependenciesMeta": { + "audio-decode": { + "optional": true + }, + "jimp": { + "optional": true + }, + "link-preview-js": { + "optional": true + } + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/axios": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/cacheable": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz", + "integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==", + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.0.8", + "@cacheable/utils": "^2.4.1", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/curve25519-js": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz", + "integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/libsignal": { + "version": "6.0.0", + "resolved": "git+ssh://git@github.com/whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7", + "license": "GPL-3.0", + "dependencies": { + "curve25519-js": "^0.0.4", + "protobufjs": "^7.5.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/music-metadata": { + "version": "11.12.3", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.12.3.tgz", + "integrity": "sha512-n6hSTZkuD59qWgHh6IP5dtDlDZQXoxk/bcA85Jywg8Z1iFrlNgl2+GTFgjZyn52W5UgQpV42V4XqrQZZAMbZTQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.2", + "@tokenizer/token": "^0.3.0", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "file-type": "^21.3.1", + "media-typer": "^1.1.0", + "strtok3": "^10.3.4", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0", + "win-guid": "^0.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.2.tgz", + "integrity": "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "license": "MIT" + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/win-guid": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz", + "integrity": "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +}