(chore): update codebase

This commit is contained in:
Alois 2026-05-01 19:57:03 +02:00
commit 4e8d46b20e
106 changed files with 29936 additions and 1023 deletions

View file

@ -41,11 +41,11 @@
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@tensamin/ui": "*", "@tensamin/ui": "*",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",
"lucide-react": "^1.7.0", "lucide-react": "^1.14.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0" "react-dom": "^19.2.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2" "@tauri-apps/cli": "^2.11.0"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -15,42 +15,47 @@ name = "mobile_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2.6", features = [] }
[dependencies] [dependencies]
base64 = "0.22" base64 = "0.22"
image = { version = "0.25", default-features = false, features = ["jpeg"] } image = { version = "0.25", default-features = false, features = ["jpeg"] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2.5.3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tauri-plugin-deep-link = "2" tauri-plugin-deep-link = "2.4.7"
[target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))'.dependencies] [target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))'.dependencies]
xcap = "0.4.1" xcap = "0.4.1"
[target.'cfg(target_os = "android")'.dependencies.tauri] [target.'cfg(target_os = "android")'.dependencies.tauri]
version = "2" version = "2.11"
features = [] features = []
default-features = true default-features = true
[target.'cfg(target_os = "windows")'.dependencies.tauri] [target.'cfg(target_os = "windows")'.dependencies.tauri]
version = "2" version = "2.11"
features = ["compression", "common-controls-v6", "dynamic-acl"] features = ["compression", "common-controls-v6", "dynamic-acl"]
default-features = true default-features = true
[target.'cfg(target_os = "linux")'.dependencies.tauri] [target.'cfg(target_os = "linux")'.dependencies.tauri]
version = "2" version = "2.11"
features = ["common-controls-v6", "cef", "compression", "dynamic-acl", "x11"] features = ["common-controls-v6", "cef", "compression", "dynamic-acl", "x11"]
default-features = false default-features = false
[target.'cfg(target_os = "macos")'.dependencies.tauri] [target.'cfg(target_os = "macos")'.dependencies.tauri]
version = "2" version = "2.11"
features = ["x11", "common-controls-v6", "cef", "compression", "dynamic-acl"] features = ["x11", "common-controls-v6", "cef", "compression", "dynamic-acl"]
default-features = false default-features = false
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
tauri-plugin-barcode-scanner = "2" tauri-plugin-barcode-scanner = "2.4.4"
[patch.crates-io.tauri] [patch.crates-io.tauri]
git = "https://github.com/tauri-apps/tauri" git = "https://github.com/tauri-apps/tauri"
branch = "feat/cef" branch = "feat/cef"
[patch."https://github.com/tauri-apps/tauri"]
tauri-runtime = { path = "vendor/tauri-runtime" }
tauri-runtime-cef = { path = "vendor/tauri-runtime-cef" }
tauri-utils = { path = "vendor/tauri-utils" }

View file

@ -25,8 +25,6 @@
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. --> <!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
<intent-filter > <intent-filter >
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<!-- ChromeOS ARC++ uses a different action for deep links -->
<action android:name="org.chromium.arc.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tensamin" /> <data android:scheme="tensamin" />

View file

@ -1,3 +1,6 @@
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.LibraryExtension
buildscript { buildscript {
repositories { repositories {
google() google()
@ -5,7 +8,7 @@ buildscript {
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:8.11.0") classpath("com.android.tools.build:gradle:8.11.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21")
} }
} }
@ -16,7 +19,24 @@ allprojects {
} }
} }
subprojects {
plugins.withId("com.android.application") {
extensions.configure<ApplicationExtension>("android") {
lint {
checkReleaseBuilds = false
}
}
}
plugins.withId("com.android.library") {
extensions.configure<LibraryExtension>("android") {
lint {
checkReleaseBuilds = false
}
}
}
}
tasks.register("clean").configure { tasks.register("clean").configure {
delete("build") delete("build")
} }

View file

@ -1,10 +1,13 @@
import java.io.File import java.io.File
import javax.inject.Inject
import org.apache.tools.ant.taskdefs.condition.Os import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.GradleException import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
open class BuildTask : DefaultTask() { open class BuildTask : DefaultTask() {
@Input @Input
@ -14,6 +17,13 @@ open class BuildTask : DefaultTask() {
@Input @Input
var release: Boolean? = null var release: Boolean? = null
@Internal
var baseProjectDir: File? = null
@get:Inject
protected open val execOperations: ExecOperations
get() = throw UnsupportedOperationException("Gradle injects ExecOperations")
@TaskAction @TaskAction
fun assemble() { fun assemble() {
val executable = """bun"""; val executable = """bun""";
@ -48,15 +58,16 @@ open class BuildTask : DefaultTask() {
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null") val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
val target = target ?: throw GradleException("target cannot be null") val target = target ?: throw GradleException("target cannot be null")
val release = release ?: throw GradleException("release cannot be null") val release = release ?: throw GradleException("release cannot be null")
val baseProjectDir = baseProjectDir ?: throw GradleException("baseProjectDir cannot be null")
val args = listOf("tauri", "android", "android-studio-script"); val args = listOf("tauri", "android", "android-studio-script");
project.exec { execOperations.exec {
workingDir(File(project.projectDir, rootDirRel)) workingDir(File(baseProjectDir, rootDirRel))
executable(executable) executable(executable)
args(args) args(args)
if (project.logger.isEnabled(LogLevel.DEBUG)) { if (logger.isEnabled(LogLevel.DEBUG)) {
args("-vv") args("-vv")
} else if (project.logger.isEnabled(LogLevel.INFO)) { } else if (logger.isEnabled(LogLevel.INFO)) {
args("-v") args("-v")
} }
if (release) { if (release) {
@ -65,4 +76,4 @@ open class BuildTask : DefaultTask() {
args(listOf("--target", target)) args(listOf("--target", target))
}.assertNormalExitValue() }.assertNormalExitValue()
} }
} }

View file

@ -72,6 +72,7 @@ open class RustPlugin : Plugin<Project> {
rootDirRel = config.rootDirRel rootDirRel = config.rootDirRel
target = targetName target = targetName
release = profile == "release" release = profile == "release"
baseProjectDir = project.projectDir
} }
buildTask.dependsOn(targetBuildTask) buildTask.dependsOn(targetBuildTask)
@ -82,4 +83,4 @@ open class RustPlugin : Plugin<Project> {
} }
} }
} }
} }

View file

@ -183,8 +183,31 @@ fn get_screen_share_capabilities() -> ScreenShareCapabilities {
} }
} }
fn setup_app<R: tauri::Runtime>(
_app: &mut tauri::App<R>,
) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(any(target_os = "linux", windows))]
{
use tauri_plugin_deep_link::DeepLinkExt;
if let Err(error) = _app.deep_link().register_all() {
if cfg!(debug_assertions) {
eprintln!("Skipping deep link registration during dev: {error}");
} else {
return Err(Box::new(error));
}
}
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
#[cfg(any(target_os = "linux", target_os = "macos"))]
let builder = tauri::Builder::<tauri::Cef>::default();
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
let builder = tauri::Builder::default(); let builder = tauri::Builder::default();
#[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg(any(target_os = "linux", target_os = "macos"))]
@ -207,21 +230,7 @@ pub fn run() {
let builder = builder.plugin(tauri_plugin_barcode_scanner::init()); let builder = builder.plugin(tauri_plugin_barcode_scanner::init());
builder builder
.setup(|_app| { .setup(setup_app)
#[cfg(any(target_os = "linux", windows))]
{
use tauri_plugin_deep_link::DeepLinkExt;
if let Err(error) = _app.deep_link().register_all() {
if cfg!(debug_assertions) {
eprintln!("Skipping deep link registration during dev: {error}");
} else {
return Err(error.into());
}
}
}
Ok(())
})
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
list_screen_share_sources, list_screen_share_sources,
list_audio_outputs, list_audio_outputs,

12
apps/tauri/src-tauri/vendor/Cargo.toml vendored Normal file
View file

@ -0,0 +1,12 @@
[workspace]
members = ["tauri-runtime", "tauri-runtime-cef", "tauri-utils"]
resolver = "2"
[workspace.package]
authors = ["Tauri Programme within The Commons Conservancy"]
homepage = "https://tauri.app/"
repository = "https://github.com/tauri-apps/tauri"
categories = ["gui", "web-programming"]
license = "Apache-2.0 OR MIT"
edition = "2024"
rust-version = "1.88"

View file

@ -0,0 +1,55 @@
[package]
name = "tauri-runtime-cef"
version = "0.1.0"
authors.workspace = true
homepage.workspace = true
repository.workspace = true
categories.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
[dependencies]
dioxus-debug-cell = "0.1"
tauri-runtime = { version = "2.9.2", path = "../tauri-runtime" }
tauri-utils = { version = "2.8.0", path = "../tauri-utils", features = [
"html-manipulation",
] }
html5ever = "0.29"
raw-window-handle = "0.6"
url = "2"
http = "1"
cef = { version = "=146.4.1", default-features = false }
# Not actually used directly, just locking it.
cef-dll-sys = "=146.4.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
kuchiki = { package = "kuchikiki", version = "0.8.8-speedreader" }
sha2 = "0.10"
base64 = "0.22"
dirs = "6"
[target."cfg(windows)".dependencies]
windows = { version = "0.61", features = [
"Win32_Graphics",
"Win32_Graphics_Gdi",
"Win32_UI_HiDpi",
"Win32_UI_Input",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_System_LibraryLoader",
] }
[target."cfg(target_os = \"macos\")".dependencies]
objc2 = "0.6"
objc2-app-kit = { version = "0.3", features = [] }
objc2-foundation = { version = "0.3", features = ["NSNotification"] }
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
gtk = { version = "0.18", features = ["v3_24"] }
x11-dl = "2.21"
[features]
default = ["sandbox"]
devtools = []
macos-private-api = ["tauri-runtime/macos-private-api"]
sandbox = ["cef/sandbox"]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,82 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use cef::{rc::*, *};
use std::sync::{Arc, Mutex, mpsc::Sender};
cef::wrap_cookie_visitor! {
pub struct CollectUrlCookiesVisitor {
pub tx: Sender<tauri_runtime::Result<Vec<tauri_runtime::Cookie<'static>>>>,
pub collected: Arc<Mutex<Vec<tauri_runtime::Cookie<'static>>>>,
}
impl CookieVisitor {
fn visit(
&self,
cookie: Option<&cef::Cookie>,
count: ::std::os::raw::c_int,
total: ::std::os::raw::c_int,
_delete_cookie: Option<&mut ::std::os::raw::c_int>,
) -> ::std::os::raw::c_int {
if let Some(c) = cookie {
let name = c.name.to_string();
let value = c.value.to_string();
let domain = c.domain.to_string();
let path = c.path.to_string();
let mut builder = tauri_runtime::Cookie::build((name, value));
if !domain.is_empty() { builder = builder.domain(domain); }
if !path.is_empty() { builder = builder.path(path); }
if c.secure == 1 { builder = builder.secure(true); }
if c.httponly == 1 { builder = builder.http_only(true); }
let ck = builder.build();
self.collected.lock().unwrap().push(ck.into_owned());
}
if (count + 1) >= total {
let _ = self.tx.send(Ok(self.collected.lock().unwrap().clone()));
}
1
}
}
}
cef::wrap_cookie_visitor! {
pub struct CollectAllCookiesVisitor {
pub tx: Sender<tauri_runtime::Result<Vec<tauri_runtime::Cookie<'static>>>>,
pub collected: Arc<Mutex<Vec<tauri_runtime::Cookie<'static>>>>,
}
impl CookieVisitor {
fn visit(
&self,
cookie: Option<&cef::Cookie>,
count: ::std::os::raw::c_int,
total: ::std::os::raw::c_int,
_delete_cookie: Option<&mut ::std::os::raw::c_int>,
) -> ::std::os::raw::c_int {
if let Some(c) = cookie {
let name = c.name.to_string();
let value = c.value.to_string();
let domain = c.domain.to_string();
let path = c.path.to_string();
let mut builder = tauri_runtime::Cookie::build((name, value));
if !domain.is_empty() { builder = builder.domain(domain); }
if !path.is_empty() { builder = builder.path(path); }
if c.secure == 1 { builder = builder.secure(true); }
if c.httponly == 1 { builder = builder.http_only(true); }
let ck = builder.build();
self.collected.lock().unwrap().push(ck.into_owned());
}
if (count + 1) >= total {
let _ = self.tx.send(Ok(self.collected.lock().unwrap().clone()));
}
1
}
}
}

View file

@ -0,0 +1,72 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#[cfg(windows)]
pub mod windows {
use cef::*;
use windows::Win32::Foundation::*;
use windows::Win32::UI::WindowsAndMessaging::*;
use windows::core::{PCWSTR, w};
/// Same as [WNDPROC] but without the Option wrapper.
type WindowProc = unsafe extern "system" fn(HWND, u32, WPARAM, LPARAM) -> LRESULT;
const ORIGINAL_WND_PROP: PCWSTR = w!("TAURI_CEF_ORIGINAL_WND_PROC");
/// Subclasses the given window to handle draggable regions
/// by replacing its window procedure with `root_window_proc`
/// and storing the original procedure as a property to be called later.
pub fn subclass_window_for_dragging(window: &mut cef::Window) {
let hwnd = window.window_handle();
let hwnd = HWND(hwnd.0 as _);
subclass_window(hwnd, root_window_proc);
}
/// Subclasses a window by replacing its window procedure with the given `proc`
/// and storing the original procedure as a property for later use.
fn subclass_window(hwnd: HWND, proc: WindowProc) {
// If already subclassed, return early
let orginial_wnd_proc = unsafe { GetPropW(hwnd, ORIGINAL_WND_PROP) };
if !orginial_wnd_proc.is_invalid() {
return;
}
// Reset last error
unsafe { SetLastError(ERROR_SUCCESS) };
// Set the new window procedure and get the orginal one
let original_wnd_proc = unsafe { SetWindowLongPtrW(hwnd, GWLP_WNDPROC, proc as isize) };
if original_wnd_proc == 0 && unsafe { GetLastError() } != ERROR_SUCCESS {
return;
}
unsafe {
// Store the original window proc as a property for later use
let _ = SetPropW(
hwnd,
ORIGINAL_WND_PROP,
Some(HANDLE(original_wnd_proc as _)),
);
}
}
/// The root window procedure to handle WM_NCLBUTTONDOWN
/// by calling DefWindowProcW directly to allow dragging
/// and forwarding other messages to the original CEF window procedure.
unsafe extern "system" fn root_window_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
if msg == WM_NCLBUTTONDOWN {
return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
}
// For other messages, call the original CEF window procedure
let original_wnd_proc = GetPropW(hwnd, ORIGINAL_WND_PROP);
let original_wnd_proc = std::mem::transmute::<_, WindowProc>(original_wnd_proc.0);
CallWindowProcW(Some(original_wnd_proc), hwnd, msg, wparam, lparam)
}
}

View file

@ -0,0 +1,432 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
borrow::Cow,
io::{Cursor, Read},
sync::Arc,
};
use cef::{rc::*, *};
use dioxus_debug_cell::RefCell;
use html5ever::{LocalName, interface::QualName, namespace_url, ns};
use http::{
HeaderMap, HeaderName, HeaderValue,
header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE},
};
use kuchiki::NodeRef;
use tauri_runtime::webview::UriSchemeProtocolHandler;
use tauri_utils::{
config::{Csp, CspDirectiveSources},
html::{parse as parse_html, serialize_node},
};
use url::Url;
use super::CefInitScript;
type HttpResponse = Arc<RefCell<Option<http::Response<Cursor<Vec<u8>>>>>>;
fn csp_inject_initialization_scripts_hashes(
existing_csp: String,
initialization_scripts: &[CefInitScript],
) -> String {
if initialization_scripts.is_empty() {
return existing_csp;
}
// For custom schemes, include ALL script hashes (we inject all scripts into HTML)
// This matches the HTML injection behavior in inject_scripts_into_html_body
let script_hashes: Vec<String> = initialization_scripts
.iter()
.map(|s| s.hash.clone())
.collect();
if script_hashes.is_empty() {
return existing_csp;
}
// Parse CSP using tauri-utils
let mut csp_map: std::collections::HashMap<String, CspDirectiveSources> =
Csp::Policy(existing_csp.to_string()).into();
// Update or create script-src directive with script hashes
let script_src = csp_map
.entry("script-src".to_string())
.or_insert_with(|| CspDirectiveSources::List(vec!["'self'".to_string()]));
// Extend with script hashes
script_src.extend(script_hashes);
// Convert back to CSP string
Csp::DirectiveMap(csp_map).to_string()
}
/// Helper function to inject initialization scripts into HTML body
fn inject_scripts_into_html_body(
body: &[u8],
initialization_scripts: &[CefInitScript],
) -> Option<Vec<u8>> {
// Check if body is valid UTF-8 HTML
let Ok(body_str) = std::str::from_utf8(body) else {
return None;
};
// Parse HTML and inject scripts
let document = parse_html(body_str.to_string());
let head = if let Ok(ref head_node) = document.select_first("head") {
head_node.as_node().clone()
} else {
let head_node = NodeRef::new_element(
QualName::new(None, ns!(html), LocalName::from("head")),
None,
);
document.prepend(head_node.clone());
head_node
};
// Inject initialization scripts (for custom schemes, inject all scripts)
for init_script in initialization_scripts.iter().rev() {
let script_el = NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None);
script_el.append(NodeRef::new_text(init_script.script.script.as_str()));
head.prepend(script_el);
}
// Serialize the modified HTML
Some(serialize_node(&document))
}
wrap_resource_request_handler! {
pub struct WebResourceRequestHandler {
initialization_scripts: Arc<Vec<CefInitScript>>,
}
impl ResourceRequestHandler {
fn on_before_resource_load(
&self,
_browser: Option<&mut Browser>,
_frame: Option<&mut Frame>,
_request: Option<&mut Request>,
_callback: Option<&mut Callback>,
) -> ReturnValue {
sys::cef_return_value_t::RV_CONTINUE.into()
}
}
}
wrap_request_handler! {
pub struct WebRequestHandler {
initialization_scripts: Arc<Vec<CefInitScript>>,
navigation_handler: Option<Arc<tauri_runtime::webview::NavigationHandler>>,
}
impl RequestHandler {
fn on_before_browse(
&self,
_browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
request: Option<&mut Request>,
_user_gesture: ::std::os::raw::c_int,
_is_redirect: ::std::os::raw::c_int,
) -> ::std::os::raw::c_int {
let Some(frame) = frame else {
return 0;
};
// we only fire main frame navigation events to match the behavior of the wry runtime
if frame.is_main() == 0 {
return 0;
}
let Some(handler) = &self.navigation_handler else {
return 0;
};
let Some(request) = request else {
return 0;
};
let url_str = CefString::from(&request.url()).to_string();
let Ok(url) = url::Url::parse(&url_str) else {
return 0;
};
let should_navigate = handler(&url);
if should_navigate {
0
} else {
1
}
}
fn resource_request_handler(
&self,
_browser: Option<&mut Browser>,
_frame: Option<&mut Frame>,
_request: Option<&mut Request>,
_is_navigation: ::std::os::raw::c_int,
_is_download: ::std::os::raw::c_int,
_request_initiator: Option<&CefString>,
_disable_default_handling: Option<&mut ::std::os::raw::c_int>,
) -> Option<ResourceRequestHandler> {
Some(WebResourceRequestHandler::new(
self.initialization_scripts.clone(),
))
}
}
}
wrap_resource_handler! {
pub struct WebResourceHandler {
webview_label: String,
handler: Arc<Box<UriSchemeProtocolHandler>>,
initialization_scripts: Arc<Vec<CefInitScript>>,
// we clone response to send it to the handler thread
response: HttpResponse,
}
impl ResourceHandler {
fn process_request(
&self,
request: Option<&mut Request>,
callback: Option<&mut Callback>,
) -> ::std::os::raw::c_int {
let Some(request) = request else { return 0 };
let Some(callback) = callback else { return 0 };
let url = CefString::from(&request.url()).to_string();
let url = Url::parse(&url).ok();
if let Some(url) = url {
let callback = ThreadSafe(callback.clone());
let response_store = ThreadSafe(self.response.clone());
let initialization_scripts = self.initialization_scripts.clone();
let responder = Box::new(move |response: http::Response<Cow<'static, [u8]>>| {
// Check if this is an HTML response that needs script injection
let content_type = response.headers().get(CONTENT_TYPE);
let is_html = content_type
.and_then(|ct| ct.to_str().ok())
.map(|ct| ct.to_lowercase().starts_with("text/html"))
.unwrap_or(false);
let (parts, body) = response.into_parts();
let body_bytes = body.into_owned();
let modified_body = if is_html {
inject_scripts_into_html_body(&body_bytes, &initialization_scripts)
.unwrap_or(body_bytes)
} else {
body_bytes
};
let mut response = http::Response::from_parts(parts, Cursor::new(modified_body));
let csp = response
.headers_mut()
.get_mut(CONTENT_SECURITY_POLICY);
if let Some(csp) = csp {
let csp_string = csp.to_str().unwrap().to_string();
let new_csp = csp_inject_initialization_scripts_hashes(
csp_string,
&initialization_scripts,
);
*csp = HeaderValue::from_str(&new_csp).unwrap();
}
response_store.into_owned().borrow_mut().replace(response);
let callback = callback.into_owned();
callback.cont();
});
let label = self.webview_label.clone();
let handler = self.handler.clone();
let data = read_request_body(request);
let headers = get_request_headers(request);
let method_str = CefString::from(&request.method()).to_string();
let method = http::Method::from_bytes(method_str.as_bytes())
.unwrap_or(http::Method::GET);
std::thread::spawn(move || {
let mut http_request = http::Request::builder().method(method).uri(url.as_str()).body(data).unwrap();
*http_request.headers_mut() = headers;
// handler is Arc<Box<UriSchemeProtocol>>, so we need to dereference to call it
(**handler)(&label, http_request, responder);
});
1
} else {
0
}
}
fn read(
&self,
data_out: *mut u8,
bytes_to_read: ::std::os::raw::c_int,
bytes_read: Option<&mut ::std::os::raw::c_int>,
_callback: Option<&mut ResourceReadCallback>,
) -> ::std::os::raw::c_int {
let Ok(bytes_to_read) = usize::try_from(bytes_to_read) else {
return 0;
};
let data_out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read) };
let count = self.response.borrow_mut().as_mut().and_then(|response| response.body_mut().read(data_out).ok()).unwrap_or(0);
if let Some(bytes_read) = bytes_read {
let Ok(count) = count.try_into() else {
return 0;
};
*bytes_read = count;
if count > 0 {
return 1;
}
}
0
}
fn response_headers(
&self,
response: Option<&mut Response>,
response_length: Option<&mut i64>,
redirect_url: Option<&mut CefString>,
) {
let (Some(response), Some(response_data)) = (response, &*self.response.borrow()) else { return };
response.set_status(response_data.status().as_u16() as i32);
let mut content_type = None;
// First pass: collect CSP header and set other headers
for (name, value) in response_data.headers() {
let Ok(value) = value.to_str() else { continue; };
response.set_header_by_name(Some(&name.as_str().into()), Some(&value.into()), 0);
if name == CONTENT_TYPE {
content_type.replace(value.to_string());
}
}
response.set_header_by_name(
Some(&"Cache-Control".into()),
Some(&"no-store".into()),
1,
);
let mime_type = content_type
.as_ref()
.and_then(|t| t.split(';').next())
.map(str::trim)
.unwrap_or("text/plain");
response.set_mime_type(Some(&mime_type.into()));
if let Some(length) = response_length { *length = -1; }
if let Some(redirect_url) = redirect_url {
let _ = std::mem::take(redirect_url);
}
}
}
}
wrap_scheme_handler_factory! {
pub struct UriSchemeHandlerFactory {
registry: super::SchemeHandlerRegistry,
scheme: String,
}
impl SchemeHandlerFactory {
fn create(
&self,
browser: Option<&mut Browser>,
_frame: Option<&mut Frame>,
_scheme_name: Option<&CefString>,
_request: Option<&mut Request>,
) -> Option<ResourceHandler> {
let browser = browser?;
let id = browser.identifier();
// get handler from our regsitry based on browser ID and scheme
let (webview_label, handler, initialization_scripts) = self
.registry
.lock()
.unwrap()
.get(&(id, self.scheme.clone()))
.cloned()?;
Some(WebResourceHandler::new(webview_label, handler, initialization_scripts, Arc::new(RefCell::new(None))))
}
}
}
struct ThreadSafe<T>(T);
impl<T> ThreadSafe<T> {
fn into_owned(self) -> T {
self.0
}
}
unsafe impl<T> Send for ThreadSafe<T> {}
unsafe impl<T> Sync for ThreadSafe<T> {}
fn read_request_body(request: &mut Request) -> Vec<u8> {
let mut body = Vec::new();
if let Some(post_data) = request.post_data() {
let mut elements = vec![None; post_data.element_count()];
post_data.elements(Some(&mut elements));
for element in elements.into_iter().flatten() {
match element.get_type().as_ref() {
sys::cef_postdataelement_type_t::PDE_TYPE_BYTES => {
let size = element.bytes_count();
if size > 0 {
let mut buf = vec![0u8; size];
// Copy bytes into our buffer
let copied = element.bytes(size, buf.as_mut_ptr());
// Safety: CEF promises it wrote `copied` bytes into buf
unsafe {
buf.set_len(copied);
}
body.extend(buf);
}
}
sys::cef_postdataelement_type_t::PDE_TYPE_FILE => {
// Read file from disk
let file_path = CefString::from(&element.file()).to_string();
if let Ok(mut file) = std::fs::File::open(&file_path) {
use std::io::Read;
let mut buf = Vec::new();
if file.read_to_end(&mut buf).is_ok() {
body.extend(buf);
}
}
}
_ => {}
}
}
}
body
}
fn get_request_headers(request: &mut Request) -> HeaderMap {
let mut headers = HeaderMap::new();
let mut map = CefStringMultimap::new();
request.header_map(Some(&mut map));
// Iterate through all entries
for (name, value) in map {
for v in value {
headers.append(
HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(&v).unwrap(),
);
}
}
headers
}

View file

@ -0,0 +1,110 @@
use cef::*;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(windows)]
mod windows;
#[cfg(target_os = "linux")]
mod linux;
#[derive(Clone)]
pub enum CefWebview {
BrowserView(cef::BrowserView),
Browser(cef::Browser),
}
impl CefWebview {
pub fn is_browser(&self) -> bool {
matches!(self, CefWebview::Browser(_))
}
pub fn browser(&self) -> Option<cef::Browser> {
match self {
CefWebview::BrowserView(view) => view.browser(),
CefWebview::Browser(browser) => Some(browser.clone()),
}
}
pub fn browser_id(&self) -> i32 {
match self {
CefWebview::BrowserView(view) => view.browser().map_or(-1, |b| b.identifier()),
CefWebview::Browser(browser) => browser.identifier(),
}
}
pub fn set_background_color(&self, color: Option<u32>) {
if let CefWebview::BrowserView(view) = self {
let window = view.window();
let color = color.or_else(|| {
window.map(|w| w.theme_color(ColorId::COLOR_PRIMARY_BACKGROUND.get_raw() as _))
});
if let Some(color) = color {
view.set_background_color(color);
}
}
}
pub fn bounds(&self) -> cef::Rect {
match self {
CefWebview::BrowserView(view) => view.bounds(),
CefWebview::Browser(browser) => browser.bounds(),
}
}
pub fn set_bounds(&self, rect: Option<&cef::Rect>) {
match self {
CefWebview::BrowserView(view) => view.set_bounds(rect),
CefWebview::Browser(browser) => browser.set_bounds(rect),
}
}
pub fn scale_factor(&self) -> f64 {
match self {
CefWebview::BrowserView(view) => view
.window()
.and_then(|w| w.display())
.map_or(1.0, |d| d.device_scale_factor() as f64),
CefWebview::Browser(browser) => browser.scale_factor(),
}
}
pub fn set_visible(&self, visible: i32) {
match self {
CefWebview::BrowserView(view) => view.set_visible(visible),
CefWebview::Browser(browser) => browser.set_visible(visible),
}
}
pub fn close(&self) {
match self {
CefWebview::BrowserView(_) => {}
CefWebview::Browser(browser) => browser.close(),
}
}
pub fn set_parent(&self, parent: &cef::Window) {
match self {
CefWebview::BrowserView(_) => {}
CefWebview::Browser(browser) => browser.set_parent(parent),
}
}
}
trait CefBrowserExt {
fn bounds(&self) -> cef::Rect;
fn set_bounds(&self, rect: Option<&cef::Rect>);
fn scale_factor(&self) -> f64;
fn set_visible(&self, visible: i32);
fn close(&self);
fn set_parent(&self, parent: &cef::Window);
#[cfg(target_os = "macos")]
fn nsview(&self) -> Option<objc2::rc::Retained<objc2_app_kit::NSView>>;
#[cfg(windows)]
fn hwnd(&self) -> Option<::windows::Win32::Foundation::HWND>;
#[cfg(target_os = "linux")]
fn xid(&self) -> Option<u64>;
}

View file

@ -0,0 +1,208 @@
use cef::*;
use std::sync::LazyLock;
use x11_dl::xlib;
use crate::cef_webview::CefBrowserExt;
static X11: LazyLock<Option<xlib::Xlib>> = LazyLock::new(|| xlib::Xlib::open().ok());
impl CefBrowserExt for cef::Browser {
fn xid(&self) -> Option<u64> {
let host = self.host()?;
let xid = host.window_handle();
Some(xid)
}
fn bounds(&self) -> cef::Rect {
let Some(xid) = self.xid() else {
return cef::Rect::default();
};
let Some(xlib) = X11.as_ref() else {
return cef::Rect::default();
};
unsafe {
let display = (xlib.XOpenDisplay)(std::ptr::null());
if display.is_null() {
return cef::Rect::default();
}
let mut root: xlib::Window = 0;
let mut x: i32 = 0;
let mut y: i32 = 0;
let mut width: u32 = 0;
let mut height: u32 = 0;
let mut border_width: u32 = 0;
let mut depth: u32 = 0;
let status = (xlib.XGetGeometry)(
display,
xid as xlib::Window,
&mut root,
&mut x,
&mut y,
&mut width,
&mut height,
&mut border_width,
&mut depth,
);
(xlib.XCloseDisplay)(display);
if status == 0 {
return cef::Rect::default();
}
// XGetGeometry returns position relative to parent, which is what we need
cef::Rect {
x,
y,
width: width as i32,
height: height as i32,
}
}
}
fn set_bounds(&self, rect: Option<&cef::Rect>) {
let Some(rect) = rect else {
return;
};
let Some(xid) = self.xid() else {
return;
};
let Some(xlib) = X11.as_ref() else {
return;
};
unsafe {
let display = (xlib.XOpenDisplay)(std::ptr::null());
if display.is_null() {
return;
}
(xlib.XMoveResizeWindow)(
display,
xid as xlib::Window,
rect.x,
rect.y,
rect.width as u32,
rect.height as u32,
);
// Ensure window is mapped and raised after setting bounds
(xlib.XMapRaised)(display, xid as xlib::Window);
(xlib.XFlush)(display);
(xlib.XCloseDisplay)(display);
}
}
fn scale_factor(&self) -> f64 {
// Get scale factor from primary display
// CEF on Linux doesn't provide direct access to the window's display,
// so we use the primary display as a reasonable default
cef::display_get_primary()
.map(|d| d.device_scale_factor() as f64)
.unwrap_or(1.0)
}
fn set_visible(&self, visible: i32) {
let Some(xid) = self.xid() else {
return;
};
let Some(xlib) = X11.as_ref() else {
return;
};
unsafe {
let display = (xlib.XOpenDisplay)(std::ptr::null());
if display.is_null() {
return;
}
if visible != 0 {
(xlib.XMapWindow)(display, xid as xlib::Window);
} else {
(xlib.XUnmapWindow)(display, xid as xlib::Window);
}
(xlib.XFlush)(display);
(xlib.XCloseDisplay)(display);
}
}
fn close(&self) {
let Some(xid) = self.xid() else {
return;
};
let Some(xlib) = X11.as_ref() else {
return;
};
unsafe {
let display = (xlib.XOpenDisplay)(std::ptr::null());
if display.is_null() {
return;
}
(xlib.XDestroyWindow)(display, xid as xlib::Window);
(xlib.XFlush)(display);
(xlib.XCloseDisplay)(display);
}
}
fn set_parent(&self, parent: &cef::Window) {
let Some(xid) = self.xid() else {
return;
};
let parent_xid = parent.window_handle();
if parent_xid == 0 {
return;
}
let Some(xlib) = X11.as_ref() else {
return;
};
unsafe {
let display = (xlib.XOpenDisplay)(std::ptr::null());
if display.is_null() {
return;
}
// Check if window exists before reparenting
let mut root: xlib::Window = 0;
let mut parent_window: xlib::Window = 0;
let mut children: *mut xlib::Window = std::ptr::null_mut();
let mut nchildren: u32 = 0;
let status = (xlib.XQueryTree)(
display,
xid as xlib::Window,
&mut root,
&mut parent_window,
&mut children,
&mut nchildren,
);
if status != 0 && !children.is_null() {
(xlib.XFree)(children as *mut std::ffi::c_void);
}
(xlib.XReparentWindow)(
display,
xid as xlib::Window,
parent_xid as xlib::Window,
0,
0,
);
// Ensure window is mapped and raised after reparenting
(xlib.XMapRaised)(display, xid as xlib::Window);
(xlib.XFlush)(display);
(xlib.XCloseDisplay)(display);
}
}
}

View file

@ -0,0 +1,99 @@
use crate::cef_webview::CefBrowserExt;
use cef::*;
use objc2::rc::Retained;
use objc2_app_kit::NSView;
use objc2_foundation::{NSPoint, NSRect, NSSize};
impl CefBrowserExt for cef::Browser {
fn nsview(&self) -> Option<objc2::rc::Retained<objc2_app_kit::NSView>> {
let host = self.host()?;
let nsview = host.window_handle() as *mut NSView;
unsafe { Retained::<NSView>::retain(nsview) }
}
fn bounds(&self) -> cef::Rect {
let Some(nsview) = self.nsview() else {
return cef::Rect::default();
};
let parent = unsafe { nsview.superview().unwrap() };
let parent_frame = parent.frame();
let webview_frame = nsview.frame();
cef::Rect {
x: webview_frame.origin.x as i32,
y: (parent_frame.size.height - webview_frame.origin.y - webview_frame.size.height) as i32,
width: webview_frame.size.width as i32,
height: webview_frame.size.height as i32,
}
}
fn set_bounds(&self, rect: Option<&cef::Rect>) {
let Some(rect) = rect else {
return;
};
let Some(nsview) = self.nsview() else {
return;
};
let parent = unsafe { nsview.superview().unwrap() };
let parent_frame = parent.frame();
let origin = NSPoint {
x: rect.x as f64,
y: (parent_frame.size.height - (rect.y as f64 + rect.height as f64)),
};
let size = NSSize {
width: rect.width as f64,
height: rect.height as f64,
};
unsafe { nsview.setFrame(NSRect { origin, size }) };
}
fn scale_factor(&self) -> f64 {
let Some(nsview) = self.nsview() else {
return 1.0;
};
let screen = nsview.window().and_then(|w| w.screen());
screen.map(|s| s.backingScaleFactor()).unwrap_or(1.0)
}
fn set_visible(&self, visible: i32) {
let Some(nsview) = self.nsview() else {
return;
};
if visible != 0 {
nsview.setHidden(false);
} else {
nsview.setHidden(true);
}
}
fn close(&self) {
let Some(nsview) = self.nsview() else {
return;
};
unsafe { nsview.removeFromSuperview() };
}
fn set_parent(&self, parent: &cef::Window) {
crate::cef_impl::ensure_valid_content_view(parent.window_handle());
let Some(nsview) = self.nsview() else {
return;
};
let parent_nsview = parent.window_handle();
let Some(parent_nsview) = (unsafe { Retained::<NSView>::retain(parent_nsview as _) }) else {
return;
};
unsafe { parent_nsview.addSubview(&nsview) };
}
}

View file

@ -0,0 +1,210 @@
use cef::*;
use std::sync::LazyLock;
use crate::cef_webview::CefBrowserExt;
use windows::{
Win32::{
Foundation::*,
Graphics::Gdi::*,
System::LibraryLoader::*,
UI::{HiDpi::*, WindowsAndMessaging::*},
},
core::{HRESULT, HSTRING, PCSTR},
};
impl CefBrowserExt for cef::Browser {
fn hwnd(&self) -> Option<HWND> {
let host = self.host()?;
let hwnd = host.window_handle();
Some(HWND(hwnd.0 as _))
}
fn bounds(&self) -> cef::Rect {
let Some(hwnd) = self.hwnd() else {
return cef::Rect::default();
};
let mut rect = RECT::default();
let _ = unsafe { GetClientRect(hwnd, &mut rect) };
let position_point = &mut [POINT {
x: rect.left,
y: rect.top,
}];
unsafe { MapWindowPoints(Some(hwnd), GetParent(hwnd).ok(), position_point) };
cef::Rect {
x: position_point[0].x,
y: position_point[0].y,
width: (rect.right - rect.left) as i32,
height: (rect.bottom - rect.top) as i32,
}
}
fn set_bounds(&self, rect: Option<&cef::Rect>) {
let Some(rect) = rect else {
return;
};
let Some(hwnd) = self.hwnd() else {
return;
};
let _ = unsafe {
SetWindowPos(
hwnd,
None,
rect.x,
rect.y,
rect.width,
rect.height,
SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOZORDER,
)
};
}
fn scale_factor(&self) -> f64 {
let Some(hwnd) = self.hwnd() else {
return 1.0;
};
let dpi = unsafe { hwnd_dpi(hwnd) };
dpi_to_scale_factor(dpi)
}
fn set_visible(&self, visible: i32) {
let Some(hwnd) = self.hwnd() else {
return;
};
if visible != 0 {
let _ = unsafe { ShowWindow(hwnd, SW_SHOW) };
unsafe { ensure_render_target(hwnd) };
} else {
let _ = unsafe { ShowWindow(hwnd, SW_HIDE) };
}
}
fn close(&self) {
let Some(hwnd) = self.hwnd() else {
return;
};
let _ = unsafe { DestroyWindow(hwnd) };
}
fn set_parent(&self, parent: &cef::Window) {
let Some(hwnd) = self.hwnd() else {
return;
};
let parent_hwnd = HWND(parent.window_handle().0 as _);
let _ = unsafe { SetParent(hwnd, Some(parent_hwnd)) };
}
}
/// Toggle visibility on Chrome_WidgetWin_1 children to force CEF to rebind
/// the compositor surface and emit a fresh paint after the child webview has
/// been hidden.
///
/// Required when the child has been hidden, particularly when paired with CDP
/// `Page.setWebLifecycleState: frozen`, which pauses the renderer's compositor.
/// The Chrome_RenderWidgetHostHWND host window survives the freeze, but its
/// compositor surface is stale and won't repaint on its own when the parent is
/// shown again — leaving the widget visible with blank content. The hide+show
/// dance on Chrome_WidgetWin_1 forces Chromium to rebind the surface and
/// schedule a paint.
unsafe fn ensure_render_target(hwnd: HWND) {
use windows::core::PCWSTR;
const CHROME_WIDGET: PCWSTR = windows::core::w!("Chrome_WidgetWin_1");
let mut child = unsafe { FindWindowExW(Some(hwnd), None, CHROME_WIDGET, PCWSTR::null()) };
while let Ok(child_hwnd) = child {
let _ = unsafe { ShowWindow(child_hwnd, SW_HIDE) };
let _ = unsafe { ShowWindow(child_hwnd, SW_SHOW) };
child = unsafe { FindWindowExW(Some(hwnd), Some(child_hwnd), CHROME_WIDGET, PCWSTR::null()) };
}
}
fn get_function_impl(library: &str, function: &str) -> FARPROC {
let library = HSTRING::from(library);
assert_eq!(function.chars().last(), Some('\0'));
// Library names we will use are ASCII so we can use the A version to avoid string conversion.
let module = unsafe { LoadLibraryW(&library) }.unwrap_or_default();
if module.is_invalid() {
return None;
}
unsafe { GetProcAddress(module, PCSTR::from_raw(function.as_ptr())) }
}
macro_rules! get_function {
($lib:expr, $func:ident) => {
get_function_impl($lib, concat!(stringify!($func), '\0'))
.map(|f| unsafe { std::mem::transmute::<_, $func>(f) })
};
}
pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32;
pub type GetDpiForMonitor = unsafe extern "system" fn(
hmonitor: HMONITOR,
dpi_type: MONITOR_DPI_TYPE,
dpi_x: *mut u32,
dpi_y: *mut u32,
) -> HRESULT;
static GET_DPI_FOR_WINDOW: LazyLock<Option<GetDpiForWindow>> =
LazyLock::new(|| get_function!("user32.dll", GetDpiForWindow));
static GET_DPI_FOR_MONITOR: LazyLock<Option<GetDpiForMonitor>> =
LazyLock::new(|| get_function!("shcore.dll", GetDpiForMonitor));
pub const BASE_DPI: u32 = 96;
pub fn dpi_to_scale_factor(dpi: u32) -> f64 {
dpi as f64 / BASE_DPI as f64
}
#[allow(non_snake_case)]
pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 {
if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW {
// We are on Windows 10 Anniversary Update (1607) or later.
match GetDpiForWindow(hwnd) {
0 => BASE_DPI, // 0 is returned if hwnd is invalid
#[allow(clippy::unnecessary_cast)]
dpi => dpi as u32,
}
} else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR {
// We are on Windows 8.1 or later.
let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if monitor.is_invalid() {
return BASE_DPI;
}
let mut dpi_x = 0;
let mut dpi_y = 0;
#[allow(clippy::unnecessary_cast)]
if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == S_OK {
dpi_x as u32
} else {
BASE_DPI
}
} else {
let hdc = GetDC(Some(hwnd));
if hdc.is_invalid() {
return BASE_DPI;
}
// We are on Vista or later.
if IsProcessDPIAware().as_bool() {
// If the process is DPI aware, then scaling must be handled by the application using
// this DPI value.
GetDeviceCaps(Some(hdc), LOGPIXELSX) as u32
} else {
// If the process is DPI unaware, then scaling is performed by the OS; we thus return
// 96 (scale factor 1.0) to prevent the window from being re-scaled by both the
// application and the WM.
BASE_DPI
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
#[cfg(windows)]
pub mod windows {
use tauri_runtime::dpi::PhysicalSize;
use windows::Win32::Foundation::*;
use windows::Win32::UI::WindowsAndMessaging::*;
pub fn inner_size(hwnd: cef::sys::HWND) -> PhysicalSize<u32> {
let hwnd = HWND(hwnd.0 as _);
let mut rect = RECT::default();
let _ = unsafe { GetClientRect(hwnd, &mut rect) };
PhysicalSize::new(
(rect.right - rect.left) as u32,
(rect.bottom - rect.top) as u32,
)
}
/// Adjusts the given size to account for window borders, so that the resulting inner size matches the requested size.
///
/// Expects and returns a size in physical pixels.
pub fn adjust_size(hwnd: cef::sys::HWND, size: cef::Size) -> cef::Size {
let hwnd = HWND(hwnd.0 as _);
let mut client_rect = RECT::default();
let _ = unsafe { GetClientRect(hwnd, &mut client_rect) };
let client_width = client_rect.right - client_rect.left;
let client_height = client_rect.bottom - client_rect.top;
let mut window_rect = RECT::default();
let _ = unsafe { GetWindowRect(hwnd, &mut window_rect) };
let window_width = window_rect.right - window_rect.left;
let window_height = window_rect.bottom - window_rect.top;
let width_diff = window_width - client_width;
let height_diff = window_height - client_height;
cef::Size {
width: size.width + width_diff,
height: size.height + height_diff,
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
[package]
name = "tauri-runtime"
version = "2.11.0"
description = "Runtime for Tauri applications"
exclude = ["CHANGELOG.md", "/target"]
readme = "README.md"
authors.workspace = true
homepage.workspace = true
repository.workspace = true
categories.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
all-features = true
default-target = "x86_64-unknown-linux-gnu"
targets = [
"x86_64-pc-windows-msvc",
"x86_64-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-linux-android",
"x86_64-apple-ios",
]
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
tauri-utils = { version = "2.9.0", path = "../tauri-utils" }
http = "1"
raw-window-handle = "0.6"
url = { version = "2" }
dpi = { version = "0.1", features = ["serde"] }
# WARNING: cookie::Cookie is re-exported so bumping this is a breaking change, documented to be done as a minor bump
cookie = "0.18"
[target."cfg(windows)".dependencies.windows]
version = "0.61"
features = ["Win32_Foundation", "Win32_System_WinRT"]
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
gtk = { version = "0.18", features = ["v3_24"] }
[target."cfg(target_os = \"android\")".dependencies]
jni = "0.21"
[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies]
objc2 = "0.6"
objc2-ui-kit = { version = "0.3.0", default-features = false, features = [
"UIView",
"UIResponder",
"UIScene",
"UISceneOptions",
] }
[target."cfg(target_os = \"macos\")".dependencies]
url = "2"
objc2 = "0.6"
[features]
devtools = []
macos-private-api = []

View file

@ -0,0 +1,44 @@
# tauri-runtime
<img align="right" src="https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" height="128" width="128">
[![status](https://img.shields.io/badge/Status-Beta-green.svg)](https://github.com/tauri-apps/tauri)
[![Chat Server](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/SpmNs4S)
[![test core](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/test-core.yml?label=test%20core&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml)
[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app)
[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation)
[![support](https://img.shields.io/badge/sponsor-Opencollective-blue.svg)](https://opencollective.com/tauri)
| Component | Version |
| ------------- | -------------------------------------------------------------------------------------------------------------- |
| tauri-runtime | [![](https://img.shields.io/crates/v/tauri-runtime?style=flat-square)](https://crates.io/crates/tauri-runtime) |
## About Tauri
Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily.
Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task.
## This module
This is the glue layer between tauri itself and lower level webview libraries.
None of the exposed API of this crate is stable, and it may break semver
compatibility in the future. The major version only signifies the intended Tauri version.
To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document.
## Semver
**tauri** is following [Semantic Versioning 2.0](https://semver.org/).
## Licenses
Code: (c) 2021 - The Tauri Programme within The Commons Conservancy.
MIT or MIT/Apache 2.0 where applicable.
Logo: CC-BY-NC-ND
- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum)

View file

@ -0,0 +1,19 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// creates a cfg alias if `has_feature` is true.
// `alias` must be a snake case string.
fn alias(alias: &str, has_feature: bool) {
println!("cargo:rustc-check-cfg=cfg({alias})");
if has_feature {
println!("cargo:rustc-cfg={alias}");
}
}
fn main() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let mobile = target_os == "ios" || target_os == "android";
alias("desktop", !mobile);
alias("mobile", mobile);
}

View file

@ -0,0 +1,60 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
pub use dpi::*;
use serde::Serialize;
/// A rectangular region.
#[derive(Clone, Copy, Debug, Serialize)]
pub struct Rect {
/// Rect position.
pub position: dpi::Position,
/// Rect size.
pub size: dpi::Size,
}
impl Default for Rect {
fn default() -> Self {
Self {
position: Position::Logical((0, 0).into()),
size: Size::Logical((0, 0).into()),
}
}
}
/// A rectangular region in physical pixels.
#[derive(Clone, Copy, Debug, Serialize)]
pub struct PhysicalRect<P: dpi::Pixel, S: dpi::Pixel> {
/// Rect position.
pub position: dpi::PhysicalPosition<P>,
/// Rect size.
pub size: dpi::PhysicalSize<S>,
}
impl<P: dpi::Pixel, S: dpi::Pixel> Default for PhysicalRect<P, S> {
fn default() -> Self {
Self {
position: (0, 0).into(),
size: (0, 0).into(),
}
}
}
/// A rectangular region in logical pixels.
#[derive(Clone, Copy, Debug, Serialize)]
pub struct LogicalRect<P: dpi::Pixel, S: dpi::Pixel> {
/// Rect position.
pub position: dpi::LogicalPosition<P>,
/// Rect size.
pub size: dpi::LogicalSize<S>,
}
impl<P: dpi::Pixel, S: dpi::Pixel> Default for LogicalRect<P, S> {
fn default() -> Self {
Self {
position: (0, 0).into(),
size: (0, 0).into(),
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::dpi::{PhysicalPosition, PhysicalRect, PhysicalSize};
/// Monitor descriptor.
#[derive(Debug, Clone)]
pub struct Monitor {
/// A human-readable name of the monitor.
/// `None` if the monitor doesn't exist anymore.
pub name: Option<String>,
/// The monitor's resolution.
pub size: PhysicalSize<u32>,
/// The top-left corner position of the monitor relative to the larger full screen area.
pub position: PhysicalPosition<i32>,
/// The monitor's work_area.
pub work_area: PhysicalRect<i32, u32>,
/// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa.
pub scale_factor: f64,
}

View file

@ -0,0 +1,820 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! A layer between raw [`Runtime`] webviews and Tauri.
//!
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::window::WindowId;
use crate::{Rect, Runtime, UserEvent, window::is_label_valid};
use http::Request;
use tauri_utils::config::{
BackgroundThrottlingPolicy, Color, ScrollBarStyle as ConfigScrollBarStyle, WebviewUrl,
WindowConfig, WindowEffectsConfig,
};
use url::Url;
use std::{
borrow::Cow,
collections::HashMap,
hash::{Hash, Hasher},
path::PathBuf,
sync::Arc,
};
pub type UriSchemeProtocolHandler = dyn Fn(&str, http::Request<Vec<u8>>, Box<dyn FnOnce(http::Response<Cow<'static, [u8]>>) + Send>)
+ Send
+ Sync
+ 'static;
pub type WebResourceRequestHandler =
dyn Fn(http::Request<Vec<u8>>, &mut http::Response<Cow<'static, [u8]>>) + Send + Sync;
pub type NavigationHandler = dyn Fn(&Url) -> bool + Send;
pub type NewWindowHandler<T, R> =
dyn Fn(Url, NewWindowFeatures<T, R>) -> NewWindowResponse + Send + Sync;
pub type OnPageLoadHandler = dyn Fn(Url, PageLoadEvent) + Send;
pub type DocumentTitleChangedHandler = dyn Fn(String) + Send + 'static;
pub type AddressChangedHandler = dyn Fn(&Url) + Send + Sync + 'static;
pub type DownloadHandler = dyn Fn(DownloadEvent) -> bool + Send + Sync;
#[cfg(any(target_os = "macos", target_os = "ios"))]
type OnWebContentProcessTerminateHandler = dyn Fn() + Send;
#[cfg(target_os = "ios")]
type InputAccessoryViewBuilderFn = dyn Fn(&objc2_ui_kit::UIView) -> Option<objc2::rc::Retained<objc2_ui_kit::UIView>>
+ Send
+ Sync
+ 'static;
/// Download event.
pub enum DownloadEvent<'a> {
/// Download requested.
Requested {
/// The url being downloaded.
url: Url,
/// Represents where the file will be downloaded to.
/// Can be used to set the download location by assigning a new path to it.
/// The assigned path _must_ be absolute.
destination: &'a mut PathBuf,
},
/// Download finished.
Finished {
/// The URL of the original download request.
url: Url,
/// Potentially representing the filesystem path the file was downloaded to.
path: Option<PathBuf>,
/// Indicates if the download succeeded or not.
success: bool,
},
}
#[cfg(target_os = "android")]
pub struct CreationContext<'a, 'b> {
pub env: &'a mut jni::JNIEnv<'b>,
pub activity: &'a jni::objects::JObject<'b>,
pub webview: &'a jni::objects::JObject<'b>,
}
/// Kind of event for the page load handler.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageLoadEvent {
/// Page started to load.
Started,
/// Page finished loading.
Finished,
}
/// Window features of a window requested to open.
#[derive(Debug)]
pub struct NewWindowFeatures<T: UserEvent, R: Runtime<T>> {
pub(crate) size: Option<crate::dpi::LogicalSize<f64>>,
pub(crate) position: Option<crate::dpi::LogicalPosition<f64>>,
pub(crate) opener: R::WindowOpener,
}
impl<T: UserEvent, R: Runtime<T>> NewWindowFeatures<T, R> {
pub fn new(
size: Option<crate::dpi::LogicalSize<f64>>,
position: Option<crate::dpi::LogicalPosition<f64>>,
opener: R::WindowOpener,
) -> Self {
Self {
size,
position,
opener,
}
}
/// Specifies the size of the content area
/// as defined by the user's operating system where the new window will be generated.
pub fn size(&self) -> Option<crate::dpi::LogicalSize<f64>> {
self.size
}
/// Specifies the position of the window relative to the work area
/// as defined by the user's operating system where the new window will be generated.
pub fn position(&self) -> Option<crate::dpi::LogicalPosition<f64>> {
self.position
}
/// Returns information about the webview that initiated a new window request.
pub fn opener(&self) -> &R::WindowOpener {
&self.opener
}
/// Returns information about the webview that initiated a new window request.
pub fn into_opener(self) -> R::WindowOpener {
self.opener
}
}
/// Response for the new window request handler.
pub enum NewWindowResponse {
/// Allow the window to be opened with the default implementation.
Allow,
/// Allow the window to be opened, with the given window.
///
/// The window must be created referencing the opener window so it can inherit the appropriate attributes.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Create { window_id: WindowId },
/// Deny the window from being opened.
Deny,
}
/// The scrollbar style to use in the webview.
///
/// ## Platform-specific
///
/// - **Windows**: This option must be given the same value for all webviews that target the same data directory.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default)]
pub enum ScrollBarStyle {
#[default]
/// The default scrollbar style for the webview.
Default,
#[cfg(windows)]
/// Fluent UI style overlay scrollbars. **Windows Only**
///
/// Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions,
/// see <https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541>
FluentOverlay,
}
/// A webview that has yet to be built.
pub struct PendingWebview<T: UserEvent, R: Runtime<T>> {
/// The label that the webview will be named.
pub label: String,
/// The [`WebviewAttributes`] that the webview will be created with.
pub webview_attributes: WebviewAttributes,
/// Information about the webview that initiated a new window request.
pub opener: Option<R::WindowOpener>,
/// Runtime specific attributes.
pub platform_specific_attributes: Vec<R::PlatformSpecificWebviewAttribute>,
/// Custom protocols to register on the webview
pub uri_scheme_protocols: HashMap<String, Box<UriSchemeProtocolHandler>>,
/// How to handle IPC calls on the webview.
pub ipc_handler: Option<WebviewIpcHandler<T, R>>,
/// A handler to decide if incoming url is allowed to navigate.
pub navigation_handler: Option<Box<NavigationHandler>>,
pub new_window_handler: Option<Box<NewWindowHandler<T, R>>>,
pub document_title_changed_handler: Option<Box<DocumentTitleChangedHandler>>,
pub address_changed_handler: Option<Box<AddressChangedHandler>>,
/// The resolved URL to load on the webview.
pub url: String,
#[cfg(target_os = "android")]
#[allow(clippy::type_complexity)]
pub on_webview_created:
Option<Box<dyn Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send + Sync>>,
pub web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
pub on_page_load_handler: Option<Box<OnPageLoadHandler>>,
pub download_handler: Option<Arc<DownloadHandler>>,
#[cfg(any(target_os = "macos", target_os = "ios"))]
pub on_web_content_process_terminate_handler: Option<Box<OnWebContentProcessTerminateHandler>>,
}
impl<T: UserEvent, R: Runtime<T>> PendingWebview<T, R> {
/// Create a new [`PendingWebview`] with a label from the given [`WebviewAttributes`].
pub fn new(
webview_attributes: WebviewAttributes,
platform_specific_attributes: Vec<R::PlatformSpecificWebviewAttribute>,
label: impl Into<String>,
) -> crate::Result<Self> {
let label = label.into();
if !is_label_valid(&label) {
Err(crate::Error::InvalidWindowLabel)
} else {
Ok(Self {
webview_attributes,
opener: None,
platform_specific_attributes,
uri_scheme_protocols: Default::default(),
label,
ipc_handler: None,
navigation_handler: None,
new_window_handler: None,
document_title_changed_handler: None,
address_changed_handler: None,
url: "tauri://localhost".to_string(),
#[cfg(target_os = "android")]
on_webview_created: None,
web_resource_request_handler: None,
on_page_load_handler: None,
download_handler: None,
#[cfg(any(target_os = "macos", target_os = "ios"))]
on_web_content_process_terminate_handler: None,
})
}
}
pub fn register_uri_scheme_protocol<
N: Into<String>,
H: Fn(&str, http::Request<Vec<u8>>, Box<dyn FnOnce(http::Response<Cow<'static, [u8]>>) + Send>)
+ Send
+ Sync
+ 'static,
>(
&mut self,
uri_scheme: N,
protocol_handler: H,
) {
let uri_scheme = uri_scheme.into();
self
.uri_scheme_protocols
.insert(uri_scheme, Box::new(protocol_handler));
}
#[cfg(target_os = "android")]
pub fn on_webview_created<
F: Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send + Sync + 'static,
>(
mut self,
f: F,
) -> Self {
self.on_webview_created.replace(Box::new(f));
self
}
}
/// A webview that is not yet managed by Tauri.
#[derive(Debug)]
pub struct DetachedWebview<T: UserEvent, R: Runtime<T>> {
/// Name of the window
pub label: String,
/// The [`crate::WebviewDispatch`] associated with the window.
pub dispatcher: R::WebviewDispatcher,
}
impl<T: UserEvent, R: Runtime<T>> Clone for DetachedWebview<T, R> {
fn clone(&self) -> Self {
Self {
label: self.label.clone(),
dispatcher: self.dispatcher.clone(),
}
}
}
impl<T: UserEvent, R: Runtime<T>> Hash for DetachedWebview<T, R> {
/// Only use the [`DetachedWebview`]'s label to represent its hash.
fn hash<H: Hasher>(&self, state: &mut H) {
self.label.hash(state)
}
}
impl<T: UserEvent, R: Runtime<T>> Eq for DetachedWebview<T, R> {}
impl<T: UserEvent, R: Runtime<T>> PartialEq for DetachedWebview<T, R> {
/// Only use the [`DetachedWebview`]'s label to compare equality.
fn eq(&self, other: &Self) -> bool {
self.label.eq(&other.label)
}
}
/// The attributes used to create an webview.
#[derive(Debug)]
pub struct WebviewAttributes {
pub url: WebviewUrl,
pub user_agent: Option<String>,
/// A list of initialization javascript scripts to run when loading new pages.
/// When webview load a new page, this initialization code will be executed.
/// It is guaranteed that code is executed before `window.onload`.
///
/// ## Platform-specific
///
/// - **Windows:** scripts are always added to subframes.
/// - **Android:** When [addDocumentStartJavaScript] is not supported,
/// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs).
/// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts.
///
/// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)
/// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap)
pub initialization_scripts: Vec<InitializationScript>,
pub data_directory: Option<PathBuf>,
pub drag_drop_handler_enabled: bool,
pub clipboard: bool,
pub accept_first_mouse: bool,
pub additional_browser_args: Option<String>,
pub window_effects: Option<WindowEffectsConfig>,
pub incognito: bool,
pub transparent: bool,
pub focus: bool,
pub bounds: Option<Rect>,
pub auto_resize: bool,
pub proxy_url: Option<Url>,
pub zoom_hotkeys_enabled: bool,
pub browser_extensions_enabled: bool,
pub extensions_path: Option<PathBuf>,
pub data_store_identifier: Option<[u8; 16]>,
pub use_https_scheme: bool,
pub devtools: Option<bool>,
pub background_color: Option<Color>,
pub traffic_light_position: Option<dpi::Position>,
pub background_throttling: Option<BackgroundThrottlingPolicy>,
pub javascript_disabled: bool,
/// on macOS and iOS there is a link preview on long pressing links, this is enabled by default.
/// see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview
pub allow_link_preview: bool,
pub scroll_bar_style: ScrollBarStyle,
/// Controls the WebView's browser-level general autofill behavior.
///
/// **This option does not disable password or credit card autofill.**
///
/// When set to `false`, the WebView will not automatically populate
/// general form fields using previously stored data such as addresses
/// or contact information.
///
/// If not specified, this is `true` by default.
///
/// ## Platform-specific
///
/// - **Windows**: Supported. WebView2's autofill feature (called
/// "Suggestions") may not honor `autocomplete="off"` on input
/// elements in some cases.
/// - **Linux / Android / iOS / macOS**: Unsupported and performs no
/// operation.
pub general_autofill_enabled: bool,
/// Allows overriding the keyboard accessory view on iOS.
/// Returning `None` effectively removes the view.
///
/// The closure parameter is the webview instance.
///
/// The accessory view is the view that appears above the keyboard when a text input element is focused.
/// It usually displays a view with "Done", "Next" buttons.
///
/// # Stability
///
/// This relies on [`objc2_ui_kit`] which does not provide a stable API yet, so it can receive breaking changes in minor releases.
#[cfg(target_os = "ios")]
pub input_accessory_view_builder: Option<InputAccessoryViewBuilder>,
}
unsafe impl Send for WebviewAttributes {}
unsafe impl Sync for WebviewAttributes {}
#[cfg(target_os = "ios")]
#[non_exhaustive]
pub struct InputAccessoryViewBuilder(pub Box<InputAccessoryViewBuilderFn>);
#[cfg(target_os = "ios")]
impl std::fmt::Debug for InputAccessoryViewBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("InputAccessoryViewBuilder").finish()
}
}
#[cfg(target_os = "ios")]
impl InputAccessoryViewBuilder {
pub fn new(builder: Box<InputAccessoryViewBuilderFn>) -> Self {
Self(builder)
}
}
impl From<&WindowConfig> for WebviewAttributes {
fn from(config: &WindowConfig) -> Self {
let mut builder = Self::new(config.url.clone())
.incognito(config.incognito)
.focused(config.focus)
.zoom_hotkeys_enabled(config.zoom_hotkeys_enabled)
.use_https_scheme(config.use_https_scheme)
.browser_extensions_enabled(config.browser_extensions_enabled)
.background_throttling(config.background_throttling.clone())
.devtools(config.devtools)
.scroll_bar_style(match config.scroll_bar_style {
ConfigScrollBarStyle::Default => ScrollBarStyle::Default,
#[cfg(windows)]
ConfigScrollBarStyle::FluentOverlay => ScrollBarStyle::FluentOverlay,
_ => ScrollBarStyle::Default,
})
.general_autofill_enabled(config.general_autofill_enabled);
#[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
{
builder = builder.transparent(config.transparent);
}
#[cfg(target_os = "macos")]
{
if let Some(position) = &config.traffic_light_position {
builder =
builder.traffic_light_position(dpi::LogicalPosition::new(position.x, position.y).into());
}
}
builder = builder.accept_first_mouse(config.accept_first_mouse);
if !config.drag_drop_enabled {
builder = builder.disable_drag_drop_handler();
}
if let Some(user_agent) = &config.user_agent {
builder = builder.user_agent(user_agent);
}
if let Some(additional_browser_args) = &config.additional_browser_args {
builder = builder.additional_browser_args(additional_browser_args);
}
if let Some(effects) = &config.window_effects {
builder = builder.window_effects(effects.clone());
}
if let Some(url) = &config.proxy_url {
builder = builder.proxy_url(url.to_owned());
}
if let Some(color) = config.background_color {
builder = builder.background_color(color);
}
builder.javascript_disabled = config.javascript_disabled;
builder.allow_link_preview = config.allow_link_preview;
#[cfg(target_os = "ios")]
if config.disable_input_accessory_view {
builder
.input_accessory_view_builder
.replace(InputAccessoryViewBuilder::new(Box::new(|_webview| None)));
}
builder
}
}
impl WebviewAttributes {
/// Initializes the default attributes for a webview.
pub fn new(url: WebviewUrl) -> Self {
Self {
url,
user_agent: None,
initialization_scripts: Vec::new(),
data_directory: None,
drag_drop_handler_enabled: true,
clipboard: false,
accept_first_mouse: false,
additional_browser_args: None,
window_effects: None,
incognito: false,
transparent: false,
focus: true,
bounds: None,
auto_resize: false,
proxy_url: None,
zoom_hotkeys_enabled: false,
browser_extensions_enabled: false,
data_store_identifier: None,
extensions_path: None,
use_https_scheme: false,
devtools: None,
background_color: None,
traffic_light_position: None,
background_throttling: None,
javascript_disabled: false,
allow_link_preview: true,
scroll_bar_style: ScrollBarStyle::Default,
general_autofill_enabled: true,
#[cfg(target_os = "ios")]
input_accessory_view_builder: None,
}
}
/// Sets the user agent
#[must_use]
pub fn user_agent(mut self, user_agent: &str) -> Self {
self.user_agent = Some(user_agent.to_string());
self
}
/// Adds an init script for the main frame.
///
/// When webview load a new page, this initialization code will be executed.
/// It is guaranteed that code is executed before `window.onload`.
///
/// This is executed only on the main frame.
/// If you only want to run it in all frames, use [`Self::initialization_script_on_all_frames`] instead.
///
/// ## Platform-specific
///
/// - **Windows:** scripts are always added to subframes.
/// - **Android:** When [addDocumentStartJavaScript] is not supported,
/// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs).
/// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts.
///
/// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)
/// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap)
#[must_use]
pub fn initialization_script(mut self, script: impl Into<String>) -> Self {
self.initialization_scripts.push(InitializationScript {
script: script.into(),
for_main_frame_only: true,
});
self
}
/// Adds an init script for all frames.
///
/// When webview load a new page, this initialization code will be executed.
/// It is guaranteed that code is executed before `window.onload`.
///
/// This is executed on all frames, main frame and also sub frames.
/// If you only want to run it in the main frame, use [`Self::initialization_script`] instead.
///
/// ## Platform-specific
///
/// - **Windows:** scripts are always added to subframes.
/// - **Android:** When [addDocumentStartJavaScript] is not supported,
/// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs).
/// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts.
///
/// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)
/// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap)
#[must_use]
pub fn initialization_script_on_all_frames(mut self, script: impl Into<String>) -> Self {
self.initialization_scripts.push(InitializationScript {
script: script.into(),
for_main_frame_only: false,
});
self
}
/// Data directory for the webview.
#[must_use]
pub fn data_directory(mut self, data_directory: PathBuf) -> Self {
self.data_directory.replace(data_directory);
self
}
/// Disables the drag and drop handler. This is required to use HTML5 drag and drop APIs on the frontend on Windows.
#[must_use]
pub fn disable_drag_drop_handler(mut self) -> Self {
self.drag_drop_handler_enabled = false;
self
}
/// Enables clipboard access for the page rendered on **Linux** and **Windows**.
///
/// **macOS** doesn't provide such method and is always enabled by default,
/// but you still need to add menu item accelerators to use shortcuts.
#[must_use]
pub fn enable_clipboard_access(mut self) -> Self {
self.clipboard = true;
self
}
/// Sets whether clicking an inactive window also clicks through to the webview.
#[must_use]
pub fn accept_first_mouse(mut self, accept: bool) -> Self {
self.accept_first_mouse = accept;
self
}
/// Sets additional browser arguments. **Windows Only**
#[must_use]
pub fn additional_browser_args(mut self, additional_args: &str) -> Self {
self.additional_browser_args = Some(additional_args.to_string());
self
}
/// Sets window effects
#[must_use]
pub fn window_effects(mut self, effects: WindowEffectsConfig) -> Self {
self.window_effects = Some(effects);
self
}
/// Enable or disable incognito mode for the WebView.
#[must_use]
pub fn incognito(mut self, incognito: bool) -> Self {
self.incognito = incognito;
self
}
/// Enable or disable transparency for the WebView.
#[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
#[must_use]
pub fn transparent(mut self, transparent: bool) -> Self {
self.transparent = transparent;
self
}
/// Whether the webview should be focused or not.
#[must_use]
pub fn focused(mut self, focus: bool) -> Self {
self.focus = focus;
self
}
/// Sets the webview to automatically grow and shrink its size and position when the parent window resizes.
#[must_use]
pub fn auto_resize(mut self) -> Self {
self.auto_resize = true;
self
}
/// Enable proxy for the WebView
#[must_use]
pub fn proxy_url(mut self, url: Url) -> Self {
self.proxy_url = Some(url);
self
}
/// Whether page zooming by hotkeys is enabled
///
/// ## Platform-specific:
///
/// - **Windows**: Controls WebView2's [`IsZoomControlEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings?view=webview2-winrt-1.0.2420.47#iszoomcontrolenabled) setting.
/// - **MacOS / Linux**: Injects a polyfill that zooms in and out with `ctrl/command` + `-/=`,
/// 20% in each step, ranging from 20% to 1000%. Requires `webview:allow-set-webview-zoom` permission
///
/// - **Android / iOS**: Unsupported.
#[must_use]
pub fn zoom_hotkeys_enabled(mut self, enabled: bool) -> Self {
self.zoom_hotkeys_enabled = enabled;
self
}
/// Whether browser extensions can be installed for the webview process
///
/// ## Platform-specific:
///
/// - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled)
/// - **MacOS / Linux / iOS / Android** - Unsupported.
#[must_use]
pub fn browser_extensions_enabled(mut self, enabled: bool) -> Self {
self.browser_extensions_enabled = enabled;
self
}
/// Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.
///
/// ## Note
///
/// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `<scheme>://localhost` protocols used on macOS and Linux.
///
/// ## Warning
///
/// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data.
#[must_use]
pub fn use_https_scheme(mut self, enabled: bool) -> Self {
self.use_https_scheme = enabled;
self
}
/// Whether web inspector, which is usually called browser devtools, is enabled or not. Enabled by default.
///
/// This API works in **debug** builds, but requires `devtools` feature flag to enable it in **release** builds.
///
/// ## Platform-specific
///
/// - macOS: This will call private functions on **macOS**.
/// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android.
/// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window.
#[must_use]
pub fn devtools(mut self, enabled: Option<bool>) -> Self {
self.devtools = enabled;
self
}
/// Set the window and webview background color.
/// ## Platform-specific:
///
/// - **Windows**: On Windows 7, alpha channel is ignored for the webview layer.
/// - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored.
#[must_use]
pub fn background_color(mut self, color: Color) -> Self {
self.background_color = Some(color);
self
}
/// Change the position of the window controls. Available on macOS only.
///
/// Requires titleBarStyle: Overlay and decorations: true.
///
/// ## Platform-specific
///
/// - **Linux / Windows / iOS / Android:** Unsupported.
#[must_use]
pub fn traffic_light_position(mut self, position: dpi::Position) -> Self {
self.traffic_light_position = Some(position);
self
}
/// Whether to show a link preview when long pressing on links. Available on macOS and iOS only.
///
/// Default is true.
///
/// See https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview
///
/// ## Platform-specific
///
/// - **Linux / Windows / Android:** Unsupported.
#[must_use]
pub fn allow_link_preview(mut self, allow_link_preview: bool) -> Self {
self.allow_link_preview = allow_link_preview;
self
}
/// Change the default background throttling behavior.
///
/// By default, browsers use a suspend policy that will throttle timers and even unload
/// the whole tab (view) to free resources after roughly 5 minutes when a view became
/// minimized or hidden. This will pause all tasks until the documents visibility state
/// changes back from hidden to visible by bringing the view back to the foreground.
///
/// ## Platform-specific
///
/// - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.
/// - **iOS**: Supported since version 17.0+.
/// - **macOS**: Supported since version 14.0+.
///
/// see <https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578>
#[must_use]
pub fn background_throttling(mut self, policy: Option<BackgroundThrottlingPolicy>) -> Self {
self.background_throttling = policy;
self
}
/// Specifies the native scrollbar style to use with the webview.
/// CSS styles that modify the scrollbar are applied on top of the native appearance configured here.
///
/// Defaults to [`ScrollBarStyle::Default`], which is the browser default.
///
/// ## Platform-specific
///
/// - **Windows**:
/// - [`ScrollBarStyle::FluentOverlay`] requires WebView2 Runtime version 125.0.2535.41 or higher,
/// and does nothing on older versions.
/// - This option must be given the same value for all webviews that target the same data directory. Use
/// [`WebviewAttributes::data_directory`] to change data directories if needed.
/// - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation.
#[must_use]
pub fn scroll_bar_style(mut self, style: ScrollBarStyle) -> Self {
self.scroll_bar_style = style;
self
}
/// Controls the WebView's browser-level general autofill behavior.
///
/// **This option does not disable password or credit card autofill.**
///
/// When set to `false`, the WebView will not automatically populate
/// general form fields using previously stored data such as addresses
/// or contact information.
///
/// By default, this is `true`.
///
/// ## Platform-specific
///
/// - **Windows**: Supported. WebView2's autofill feature (called
/// "Suggestions") may not honor `autocomplete="off"` on input
/// elements in some cases.
/// - **Linux / Android / iOS / macOS**: Unsupported and performs no
/// operation.
#[must_use]
pub fn general_autofill_enabled(mut self, enabled: bool) -> Self {
self.general_autofill_enabled = enabled;
self
}
}
/// IPC handler.
pub type WebviewIpcHandler<T, R> = Box<dyn Fn(DetachedWebview<T, R>, Request<String>) + Send>;
/// An initialization script
#[derive(Debug, Clone)]
pub struct InitializationScript {
/// The script to run
pub script: String,
/// Whether the script should be injected to main frame only
pub for_main_frame_only: bool,
}

View file

@ -0,0 +1,661 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! A layer between raw [`Runtime`] windows and Tauri.
use crate::{
Icon, Runtime, UserEvent, WindowDispatch,
webview::{DetachedWebview, PendingWebview},
};
use dpi::PixelUnit;
use serde::{Deserialize, Deserializer, Serialize};
use tauri_utils::{
Theme,
config::{Color, WindowConfig},
};
#[cfg(windows)]
use windows::Win32::Foundation::HWND;
use std::{
hash::{Hash, Hasher},
marker::PhantomData,
path::PathBuf,
sync::mpsc::Sender,
};
/// An event from a window.
#[derive(Debug, Clone)]
pub enum WindowEvent {
/// The size of the window has changed. Contains the client area's new dimensions.
Resized(dpi::PhysicalSize<u32>),
/// The position of the window has changed. Contains the window's new position.
Moved(dpi::PhysicalPosition<i32>),
/// The window has been requested to close.
CloseRequested {
/// A signal sender. If a `true` value is emitted, the window won't be closed.
signal_tx: Sender<bool>,
},
/// The window has been destroyed.
Destroyed,
/// The window gained or lost focus.
///
/// The parameter is true if the window has gained focus, and false if it has lost focus.
Focused(bool),
/// The window's scale factor has changed.
///
/// The following user actions can cause DPI changes:
///
/// - Changing the display's resolution.
/// - Changing the display's scale factor (e.g. in Control Panel on Windows).
/// - Moving the window to a display with a different scale factor.
ScaleFactorChanged {
/// The new scale factor.
scale_factor: f64,
/// The window inner size.
new_inner_size: dpi::PhysicalSize<u32>,
},
/// An event associated with the drag and drop action.
DragDrop(DragDropEvent),
/// The system window theme has changed.
///
/// Applications might wish to react to this to change the theme of the content of the window when the system changes the window theme.
ThemeChanged(Theme),
/// Emitted when the application has been suspended.
///
/// ## Platform-specific
///
/// - **Android**: This is triggered by `onPause` method of the Activity.
/// - **iOS**: This is triggered by `applicationWillResignActive` method of the UIApplicationDelegate.
/// - **Linux / macOS / Windows**: Unsupported.
#[cfg(mobile)]
Suspended,
/// Emitted when the application has been resumed.
///
/// ## Platform-specific
///
/// - **Android**: This is triggered by `onResume` method of the Activity. The first onResume() is ignored to match the iOS implementation, since that is called on activity creation.
/// - **iOS**: This is triggered by `applicationWillEnterForeground` method of the UIApplicationDelegate.
/// - **Linux / macOS / Windows**: Unsupported.
#[cfg(mobile)]
Resumed,
}
/// An event from a window.
#[derive(Debug, Clone)]
pub enum WebviewEvent {
/// An event associated with the drag and drop action.
DragDrop(DragDropEvent),
}
/// The drag drop event payload.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DragDropEvent {
/// A drag operation has entered the webview.
Enter {
/// List of paths that are being dragged onto the webview.
paths: Vec<PathBuf>,
/// The position of the mouse cursor.
position: dpi::PhysicalPosition<f64>,
},
/// A drag operation is moving over the webview.
Over {
/// The position of the mouse cursor.
position: dpi::PhysicalPosition<f64>,
},
/// The file(s) have been dropped onto the webview.
Drop {
/// List of paths that are being dropped onto the window.
paths: Vec<PathBuf>,
/// The position of the mouse cursor.
position: dpi::PhysicalPosition<f64>,
},
/// The drag operation has been cancelled or left the window.
Leave,
}
/// Describes the appearance of the mouse cursor.
#[non_exhaustive]
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
pub enum CursorIcon {
/// The platform-dependent default cursor.
#[default]
Default,
/// A simple crosshair.
Crosshair,
/// A hand (often used to indicate links in web browsers).
Hand,
/// Self explanatory.
Arrow,
/// Indicates something is to be moved.
Move,
/// Indicates text that may be selected or edited.
Text,
/// Program busy indicator.
Wait,
/// Help indicator (often rendered as a "?")
Help,
/// Progress indicator. Shows that processing is being done. But in contrast
/// with "Wait" the user may still interact with the program. Often rendered
/// as a spinning beach ball, or an arrow with a watch or hourglass.
Progress,
/// Cursor showing that something cannot be done.
NotAllowed,
ContextMenu,
Cell,
VerticalText,
Alias,
Copy,
NoDrop,
/// Indicates something can be grabbed.
Grab,
/// Indicates something is grabbed.
Grabbing,
AllScroll,
ZoomIn,
ZoomOut,
/// Indicate that some edge is to be moved. For example, the 'SeResize' cursor
/// is used when the movement starts from the south-east corner of the box.
EResize,
NResize,
NeResize,
NwResize,
SResize,
SeResize,
SwResize,
WResize,
EwResize,
NsResize,
NeswResize,
NwseResize,
ColResize,
RowResize,
}
impl<'de> Deserialize<'de> for CursorIcon {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(match s.to_lowercase().as_str() {
"default" => CursorIcon::Default,
"crosshair" => CursorIcon::Crosshair,
"hand" => CursorIcon::Hand,
"arrow" => CursorIcon::Arrow,
"move" => CursorIcon::Move,
"text" => CursorIcon::Text,
"wait" => CursorIcon::Wait,
"help" => CursorIcon::Help,
"progress" => CursorIcon::Progress,
"notallowed" => CursorIcon::NotAllowed,
"contextmenu" => CursorIcon::ContextMenu,
"cell" => CursorIcon::Cell,
"verticaltext" => CursorIcon::VerticalText,
"alias" => CursorIcon::Alias,
"copy" => CursorIcon::Copy,
"nodrop" => CursorIcon::NoDrop,
"grab" => CursorIcon::Grab,
"grabbing" => CursorIcon::Grabbing,
"allscroll" => CursorIcon::AllScroll,
"zoomin" => CursorIcon::ZoomIn,
"zoomout" => CursorIcon::ZoomOut,
"eresize" => CursorIcon::EResize,
"nresize" => CursorIcon::NResize,
"neresize" => CursorIcon::NeResize,
"nwresize" => CursorIcon::NwResize,
"sresize" => CursorIcon::SResize,
"seresize" => CursorIcon::SeResize,
"swresize" => CursorIcon::SwResize,
"wresize" => CursorIcon::WResize,
"ewresize" => CursorIcon::EwResize,
"nsresize" => CursorIcon::NsResize,
"neswresize" => CursorIcon::NeswResize,
"nwseresize" => CursorIcon::NwseResize,
"colresize" => CursorIcon::ColResize,
"rowresize" => CursorIcon::RowResize,
_ => CursorIcon::Default,
})
}
}
/// Window size constraints
#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WindowSizeConstraints {
/// The minimum width a window can be, If this is `None`, the window will have no minimum width.
///
/// The default is `None`.
pub min_width: Option<PixelUnit>,
/// The minimum height a window can be, If this is `None`, the window will have no minimum height.
///
/// The default is `None`.
pub min_height: Option<PixelUnit>,
/// The maximum width a window can be, If this is `None`, the window will have no maximum width.
///
/// The default is `None`.
pub max_width: Option<PixelUnit>,
/// The maximum height a window can be, If this is `None`, the window will have no maximum height.
///
/// The default is `None`.
pub max_height: Option<PixelUnit>,
}
/// Do **NOT** implement this trait except for use in a custom [`Runtime`]
///
/// This trait is separate from [`WindowBuilder`] to prevent "accidental" implementation.
pub trait WindowBuilderBase: std::fmt::Debug + Clone + Sized {}
/// A builder for all attributes related to a single window.
///
/// This trait is only meant to be implemented by a custom [`Runtime`]
/// and not by applications.
pub trait WindowBuilder: WindowBuilderBase {
/// Initializes a new window attributes builder.
fn new() -> Self;
/// Initializes a new window builder from a [`WindowConfig`]
fn with_config(config: &WindowConfig) -> Self;
/// Show window in the center of the screen.
#[must_use]
fn center(self) -> Self;
/// The initial position of the window in logical pixels.
#[must_use]
fn position(self, x: f64, y: f64) -> Self;
/// Window size in logical pixels.
#[must_use]
fn inner_size(self, width: f64, height: f64) -> Self;
/// Window min inner size in logical pixels.
#[must_use]
fn min_inner_size(self, min_width: f64, min_height: f64) -> Self;
/// Window max inner size in logical pixels.
#[must_use]
fn max_inner_size(self, max_width: f64, max_height: f64) -> Self;
/// Window inner size constraints.
#[must_use]
fn inner_size_constraints(self, constraints: WindowSizeConstraints) -> Self;
/// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) on creation
///
/// ## Platform-specific
///
/// - **iOS / Android:** Unsupported.
#[must_use]
fn prevent_overflow(self) -> Self;
/// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size)
/// on creation with a margin
///
/// ## Platform-specific
///
/// - **iOS / Android:** Unsupported.
#[must_use]
fn prevent_overflow_with_margin(self, margin: dpi::Size) -> Self;
/// Whether the window is resizable or not.
/// When resizable is set to false, native window's maximize button is automatically disabled.
#[must_use]
fn resizable(self, resizable: bool) -> Self;
/// Whether the window's native maximize button is enabled or not.
/// If resizable is set to false, this setting is ignored.
///
/// ## Platform-specific
///
/// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode.
/// - **Linux / iOS / Android:** Unsupported.
#[must_use]
fn maximizable(self, maximizable: bool) -> Self;
/// Whether the window's native minimize button is enabled or not.
///
/// ## Platform-specific
///
/// - **Linux / iOS / Android:** Unsupported.
#[must_use]
fn minimizable(self, minimizable: bool) -> Self;
/// Whether the window's native close button is enabled or not.
///
/// ## Platform-specific
///
/// - **Linux:** "GTK+ will do its best to convince the window manager not to show a close button.
/// Depending on the system, this function may not have any effect when called on a window that is already visible"
/// - **iOS / Android:** Unsupported.
#[must_use]
fn closable(self, closable: bool) -> Self;
/// The title of the window in the title bar.
#[must_use]
fn title<S: Into<String>>(self, title: S) -> Self;
/// Whether to start the window in fullscreen or not.
#[must_use]
fn fullscreen(self, fullscreen: bool) -> Self;
/// Whether the window will be initially focused or not.
#[must_use]
fn focused(self, focused: bool) -> Self;
/// Whether the window will be focusable or not.
#[must_use]
fn focusable(self, focusable: bool) -> Self;
/// Whether the window should be maximized upon creation.
#[must_use]
fn maximized(self, maximized: bool) -> Self;
/// Whether the window should be immediately visible upon creation.
#[must_use]
fn visible(self, visible: bool) -> Self;
/// Whether the window should be transparent. If this is true, writing colors
/// with alpha values different than `1.0` will produce a transparent window.
#[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
#[cfg_attr(
docsrs,
doc(cfg(any(not(target_os = "macos"), feature = "macos-private-api")))
)]
#[must_use]
fn transparent(self, transparent: bool) -> Self;
/// Whether the window should have borders and bars.
#[must_use]
fn decorations(self, decorations: bool) -> Self;
/// Whether the window should always be below other windows.
#[must_use]
fn always_on_bottom(self, always_on_bottom: bool) -> Self;
/// Whether the window should always be on top of other windows.
#[must_use]
fn always_on_top(self, always_on_top: bool) -> Self;
/// Whether the window should be visible on all workspaces or virtual desktops.
#[must_use]
fn visible_on_all_workspaces(self, visible_on_all_workspaces: bool) -> Self;
/// Prevents the window contents from being captured by other apps.
#[must_use]
fn content_protected(self, protected: bool) -> Self;
/// Sets the window icon.
fn icon(self, icon: Icon) -> crate::Result<Self>;
/// Sets whether or not the window icon should be added to the taskbar.
#[must_use]
fn skip_taskbar(self, skip: bool) -> Self;
/// Set the window background color.
#[must_use]
fn background_color(self, color: Color) -> Self;
/// Sets whether or not the window has shadow.
///
/// ## Platform-specific
///
/// - **Windows:**
/// - `false` has no effect on decorated window, shadows are always ON.
/// - `true` will make undecorated window have a 1px white border,
/// and on Windows 11, it will have a rounded corners.
/// - **Linux:** Unsupported.
#[must_use]
fn shadow(self, enable: bool) -> Self;
/// Set an owner to the window to be created.
///
/// From MSDN:
/// - An owned window is always above its owner in the z-order.
/// - The system automatically destroys an owned window when its owner is destroyed.
/// - An owned window is hidden when its owner is minimized.
///
/// For more information, see <https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#owned-windows>
#[cfg(windows)]
#[must_use]
fn owner(self, owner: HWND) -> Self;
/// Sets a parent to the window to be created.
///
/// A child window has the WS_CHILD style and is confined to the client area of its parent window.
///
/// For more information, see <https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#child-windows>
#[cfg(windows)]
#[must_use]
fn parent(self, parent: HWND) -> Self;
/// Sets a parent to the window to be created.
///
/// See <https://developer.apple.com/documentation/appkit/nswindow/1419152-addchildwindow?language=objc>
#[cfg(target_os = "macos")]
#[must_use]
fn parent(self, parent: *mut std::ffi::c_void) -> Self;
/// Sets the window to be created transient for parent.
///
/// See <https://docs.gtk.org/gtk3/method.Window.set_transient_for.html>
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
fn transient_for(self, parent: &impl gtk::glib::IsA<gtk::Window>) -> Self;
/// Enables or disables drag and drop support.
#[cfg(windows)]
#[must_use]
fn drag_and_drop(self, enabled: bool) -> Self;
/// Hide the titlebar. Titlebar buttons will still be visible.
#[cfg(target_os = "macos")]
#[must_use]
fn title_bar_style(self, style: tauri_utils::TitleBarStyle) -> Self;
/// Change the position of the window controls on macOS.
///
/// Requires titleBarStyle: Overlay and decorations: true.
#[cfg(target_os = "macos")]
#[must_use]
fn traffic_light_position<P: Into<dpi::Position>>(self, position: P) -> Self;
/// Hide the window title.
#[cfg(target_os = "macos")]
#[must_use]
fn hidden_title(self, hidden: bool) -> Self;
/// Defines the window [tabbing identifier] for macOS.
///
/// Windows with matching tabbing identifiers will be grouped together.
/// If the tabbing identifier is not set, automatic tabbing will be disabled.
///
/// [tabbing identifier]: <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
#[cfg(target_os = "macos")]
#[must_use]
fn tabbing_identifier(self, identifier: &str) -> Self;
/// Forces a theme or uses the system settings if None was provided.
fn theme(self, theme: Option<Theme>) -> Self;
/// Whether the icon was set or not.
fn has_icon(&self) -> bool;
fn get_theme(&self) -> Option<Theme>;
/// Sets custom name for Windows' window class. **Windows only**.
#[must_use]
fn window_classname<S: Into<String>>(self, window_classname: S) -> Self;
/// The name of the activity to create for this webview window.
#[cfg(target_os = "android")]
fn activity_name<S: Into<String>>(self, class_name: S) -> Self;
/// Sets the name of the activity that is creating this webview window.
///
/// This is important to determine which stack the activity will belong to.
#[cfg(target_os = "android")]
fn created_by_activity_name<S: Into<String>>(self, class_name: S) -> Self;
/// Sets the identifier of the UIScene that is requesting the creation of this new scene,
/// establishing a relationship between the two scenes.
///
/// By default the system uses the foreground scene.
#[cfg(target_os = "ios")]
fn requested_by_scene_identifier<S: Into<String>>(self, identifier: S) -> Self;
}
/// A window that has yet to be built.
pub struct PendingWindow<T: UserEvent, R: Runtime<T>> {
/// The label that the window will be named.
pub label: String,
/// The [`WindowBuilder`] that the window will be created with.
pub window_builder: <R::WindowDispatcher as WindowDispatch<T>>::WindowBuilder,
/// The webview that gets added to the window. Optional in case you want to use child webviews or other window content instead.
pub webview: Option<PendingWebview<T, R>>,
}
pub fn is_label_valid(label: &str) -> bool {
label
.chars()
.all(|c| char::is_alphanumeric(c) || c == '-' || c == '/' || c == ':' || c == '_')
}
pub fn assert_label_is_valid(label: &str) {
assert!(
is_label_valid(label),
"Window label must include only alphanumeric characters, `-`, `/`, `:` and `_`."
);
}
impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
/// Create a new [`PendingWindow`] with a label from the given [`WindowBuilder`].
pub fn new(
window_builder: <R::WindowDispatcher as WindowDispatch<T>>::WindowBuilder,
label: impl Into<String>,
) -> crate::Result<Self> {
let label = label.into();
if !is_label_valid(&label) {
Err(crate::Error::InvalidWindowLabel)
} else {
Ok(Self {
window_builder,
label,
webview: None,
})
}
}
/// Sets a webview to be created on the window.
pub fn set_webview(&mut self, webview: PendingWebview<T, R>) -> &mut Self {
self.webview.replace(webview);
self
}
}
/// Identifier of a window.
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct WindowId(u32);
impl From<u32> for WindowId {
fn from(value: u32) -> Self {
Self(value)
}
}
/// A window that is not yet managed by Tauri.
#[derive(Debug)]
pub struct DetachedWindow<T: UserEvent, R: Runtime<T>> {
/// The identifier of the window.
pub id: WindowId,
/// Name of the window
pub label: String,
/// The [`WindowDispatch`] associated with the window.
pub dispatcher: R::WindowDispatcher,
/// The webview dispatcher in case this window has an attached webview.
pub webview: Option<DetachedWindowWebview<T, R>>,
}
/// A detached webview associated with a window.
#[derive(Debug)]
pub struct DetachedWindowWebview<T: UserEvent, R: Runtime<T>> {
pub webview: DetachedWebview<T, R>,
pub use_https_scheme: bool,
/// Whether devtools was enabled in [`crate::webview::WebviewAttributes`]. `Some(false)` disables the inspector.
pub devtools: Option<bool>,
}
impl<T: UserEvent, R: Runtime<T>> Clone for DetachedWindowWebview<T, R> {
fn clone(&self) -> Self {
Self {
webview: self.webview.clone(),
use_https_scheme: self.use_https_scheme,
devtools: self.devtools,
}
}
}
impl<T: UserEvent, R: Runtime<T>> Clone for DetachedWindow<T, R> {
fn clone(&self) -> Self {
Self {
id: self.id,
label: self.label.clone(),
dispatcher: self.dispatcher.clone(),
webview: self.webview.clone(),
}
}
}
impl<T: UserEvent, R: Runtime<T>> Hash for DetachedWindow<T, R> {
/// Only use the [`DetachedWindow`]'s label to represent its hash.
fn hash<H: Hasher>(&self, state: &mut H) {
self.label.hash(state)
}
}
impl<T: UserEvent, R: Runtime<T>> Eq for DetachedWindow<T, R> {}
impl<T: UserEvent, R: Runtime<T>> PartialEq for DetachedWindow<T, R> {
/// Only use the [`DetachedWindow`]'s label to compare equality.
fn eq(&self, other: &Self) -> bool {
self.label.eq(&other.label)
}
}
/// A raw window type that contains fields to access
/// the HWND on Windows, gtk::ApplicationWindow on Linux
pub struct RawWindow<'a> {
#[cfg(windows)]
pub hwnd: isize,
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub gtk_window: &'a gtk::ApplicationWindow,
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub default_vbox: Option<&'a gtk::Box>,
pub _marker: &'a PhantomData<()>,
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,94 @@
[package]
name = "tauri-utils"
version = "2.9.0"
description = "Utilities for Tauri"
exclude = ["CHANGELOG.md", "/target"]
readme = "README.md"
authors.workspace = true
homepage.workspace = true
repository.workspace = true
categories.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
thiserror = "2"
phf = { version = "0.11", features = ["macros"] }
brotli = { version = "8", optional = true, default-features = false, features = [
"std",
] }
url = { version = "2", features = ["serde"] }
html5ever = { version = "0.29", optional = true }
kuchiki = { package = "kuchikiki", version = "0.8.8-speedreader", optional = true }
dom_query = { version = "0.27", optional = true, default-features = false }
proc-macro2 = { version = "1", optional = true }
quote = { version = "1", optional = true }
schemars = { version = "1", features = ["url2", "uuid1"], optional = true }
serde_with = "3"
aes-gcm = { version = "0.10", optional = true }
getrandom = { version = "0.3", optional = true, features = ["std"] }
serialize-to-javascript = { version = "0.1.2", optional = true }
ctor = { version = "0.8", default-features = false, features = [
"std",
"proc_macro",
] }
json5 = { version = "0.4", optional = true }
# Part of public api in error type
toml = { version = ">=0.9, <=1", features = ["parse"] }
json-patch = "3.0"
# Our code requires at least 0.3.1
glob = "0.3.1"
urlpattern = "0.3"
regex = "1"
walkdir = { version = "2", optional = true }
memchr = "2"
semver = "1"
infer = "0.19"
dunce = "1"
log = "0.4.21"
cargo_metadata = { version = "0.19", optional = true }
serde-untagged = "0.1"
uuid = { version = "1", features = ["serde"] }
http = "1"
plist = "1"
[target."cfg(target_os = \"macos\")".dependencies]
swift-rs = { version = "1", optional = true, features = ["build"] }
[dev-dependencies]
getrandom = { version = "0.3", features = ["std"] }
serial_test = "3"
tauri = { path = "../tauri" }
tempfile = "3.15.0"
[features]
build = [
"proc-macro2",
"quote",
"cargo_metadata",
"schema",
"swift-rs",
"html-manipulation",
]
# Same as `build` but uses `html-manipulation-2` to avoid the `kuchikiki` dependency.
build-2 = [
"proc-macro2",
"quote",
"cargo_metadata",
"schema",
"swift-rs",
"html-manipulation-2",
]
compression = ["brotli"]
schema = ["schemars"]
isolation = ["aes-gcm", "getrandom", "serialize-to-javascript"]
process-relaunch-dangerous-allow-symlink-macos = []
config-json5 = ["json5"]
config-toml = []
resources = ["walkdir"]
html-manipulation = ["dep:html5ever", "dep:kuchiki"]
html-manipulation-2 = ["dep:dom_query"]

View file

@ -0,0 +1,42 @@
# tauri-utils
<img align="right" src="https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" height="128" width="128">
[![status](https://img.shields.io/badge/status-stable-blue.svg)](https://github.com/tauri-apps/tauri/tree/dev)
[![License](https://img.shields.io/badge/License-MIT%20or%20Apache%202-green.svg)](https://opencollective.com/tauri)
[![test core](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/test-core.yml?label=test%20core&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield)
[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S)
[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app)
[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation)
[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri)
| Component | Version |
| ----------- | ---------------------------------------------------------------------------------------------------------- |
| tauri-utils | [![](https://img.shields.io/crates/v/tauri-utils?style=flat-square)](https://crates.io/crates/tauri-utils) |
## About Tauri
Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily.
Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task.
## This module
This is common code that is reused in many places and offers useful utilities like parsing configuration files, detecting platform triples, injecting the CSP, and managing assets.
To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document.
## Semver
**tauri** is following [Semantic Versioning 2.0](https://semver.org/).
## Licenses
Code: (c) 2021 - The Tauri Programme within The Commons Conservancy.
MIT or MIT/Apache 2.0 where applicable.
Logo: CC-BY-NC-ND
- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum)

View file

@ -0,0 +1,500 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! ACL items that are only useful inside of build script/codegen context.
use std::{
collections::{BTreeMap, HashMap},
env, fs,
path::{Path, PathBuf},
};
use crate::{
acl::{AllowedCommands, Error, has_app_manifest},
config::Config,
write_if_changed,
};
use super::{
ALLOWED_COMMANDS_FILE_NAME, PERMISSION_SCHEMA_FILE_NAME, PERMISSION_SCHEMAS_FOLDER_NAME,
REMOVE_UNUSED_COMMANDS_ENV_VAR,
capability::{Capability, CapabilityFile},
manifest::PermissionFile,
};
/// Known name of the folder containing autogenerated permissions.
pub const AUTOGENERATED_FOLDER_NAME: &str = "autogenerated";
/// Cargo cfg key for permissions file paths
pub const PERMISSION_FILES_PATH_KEY: &str = "PERMISSION_FILES_PATH";
/// Cargo cfg key for global scope schemas
pub const GLOBAL_SCOPE_SCHEMA_PATH_KEY: &str = "GLOBAL_SCOPE_SCHEMA_PATH";
/// Allowed permission file extensions
pub const PERMISSION_FILE_EXTENSIONS: &[&str] = &["json", "toml"];
/// Known filename of the permission documentation file
pub const PERMISSION_DOCS_FILE_NAME: &str = "reference.md";
/// Allowed capability file extensions
const CAPABILITY_FILE_EXTENSIONS: &[&str] = &[
"json",
#[cfg(feature = "config-json5")]
"json5",
"toml",
];
/// Known folder name of the capability schemas
const CAPABILITIES_SCHEMA_FOLDER_NAME: &str = "schemas";
const CORE_PLUGIN_PERMISSIONS_TOKEN: &str = "__CORE_PLUGIN__";
fn parse_permissions(paths: Vec<PathBuf>) -> Result<Vec<PermissionFile>, Error> {
let mut permissions = Vec::new();
for path in paths {
let ext = path.extension().unwrap().to_string_lossy().to_string();
let permission_file = fs::read_to_string(&path).map_err(|e| Error::ReadFile(e, path))?;
let permission: PermissionFile = match ext.as_str() {
"toml" => toml::from_str(&permission_file)?,
"json" => serde_json::from_str(&permission_file)?,
_ => return Err(Error::UnknownPermissionFormat(ext)),
};
permissions.push(permission);
}
Ok(permissions)
}
/// Write the permissions to a temporary directory and pass it to the immediate consuming crate.
pub fn define_permissions<F: Fn(&Path) -> bool>(
pattern: &str,
pkg_name: &str,
out_dir: &Path,
filter_fn: F,
) -> Result<Vec<PermissionFile>, Error> {
let permission_files = glob::glob(pattern)?
.flatten()
.flat_map(|p| p.canonicalize())
// filter extension
.filter(|p| {
p.extension()
.and_then(|e| e.to_str())
.map(|e| PERMISSION_FILE_EXTENSIONS.contains(&e))
.unwrap_or_default()
})
.filter(|p| filter_fn(p))
// filter schemas
.filter(|p| p.parent().unwrap().file_name().unwrap() != PERMISSION_SCHEMAS_FOLDER_NAME)
.collect::<Vec<PathBuf>>();
let pkg_name_valid_path = pkg_name.replace(':', "-");
let permission_files_path = out_dir.join(format!("{pkg_name_valid_path}-permission-files"));
let permission_files_json = serde_json::to_string(&permission_files)?;
write_if_changed(&permission_files_path, permission_files_json)
.map_err(|e| Error::WriteFile(e, permission_files_path.clone()))?;
if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") {
println!(
"cargo:{plugin_name}{CORE_PLUGIN_PERMISSIONS_TOKEN}_{PERMISSION_FILES_PATH_KEY}={}",
permission_files_path.display()
);
} else {
println!(
"cargo:{PERMISSION_FILES_PATH_KEY}={}",
permission_files_path.display()
);
}
parse_permissions(permission_files)
}
/// Read all permissions listed from the defined cargo cfg key value.
pub fn read_permissions() -> Result<HashMap<String, Vec<PermissionFile>>, Error> {
let mut permissions_map = HashMap::new();
for (key, value) in env::vars_os() {
let key = key.to_string_lossy();
if let Some(plugin_crate_name_var) = key
.strip_prefix("DEP_")
.and_then(|v| v.strip_suffix(&format!("_{PERMISSION_FILES_PATH_KEY}")))
.map(|v| {
v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN)
.and_then(|v| v.strip_prefix("TAURI_"))
.unwrap_or(v)
})
{
let permissions_path = PathBuf::from(value);
let permissions_str =
fs::read_to_string(&permissions_path).map_err(|e| Error::ReadFile(e, permissions_path))?;
let permissions: Vec<PathBuf> = serde_json::from_str(&permissions_str)?;
let permissions = parse_permissions(permissions)?;
let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
let plugin_crate_name = plugin_crate_name
.strip_prefix("tauri-plugin-")
.map(ToString::to_string)
.unwrap_or(plugin_crate_name);
permissions_map.insert(plugin_crate_name, permissions);
}
}
Ok(permissions_map)
}
/// Define the global scope schema JSON file path if it exists and pass it to the immediate consuming crate.
pub fn define_global_scope_schema(
schema: schemars::Schema,
pkg_name: &str,
out_dir: &Path,
) -> Result<(), Error> {
let path = out_dir.join("global-scope.json");
write_if_changed(&path, serde_json::to_vec(&schema)?)
.map_err(|e| Error::WriteFile(e, path.clone()))?;
if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") {
println!(
"cargo:{plugin_name}{CORE_PLUGIN_PERMISSIONS_TOKEN}_{GLOBAL_SCOPE_SCHEMA_PATH_KEY}={}",
path.display()
);
} else {
println!("cargo:{GLOBAL_SCOPE_SCHEMA_PATH_KEY}={}", path.display());
}
Ok(())
}
/// Read all global scope schemas listed from the defined cargo cfg key value.
pub fn read_global_scope_schemas() -> Result<HashMap<String, serde_json::Value>, Error> {
let mut schemas_map = HashMap::new();
for (key, value) in env::vars_os() {
let key = key.to_string_lossy();
if let Some(plugin_crate_name_var) = key
.strip_prefix("DEP_")
.and_then(|v| v.strip_suffix(&format!("_{GLOBAL_SCOPE_SCHEMA_PATH_KEY}")))
.map(|v| {
v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN)
.and_then(|v| v.strip_prefix("TAURI_"))
.unwrap_or(v)
})
{
let path = PathBuf::from(value);
let json = fs::read_to_string(&path).map_err(|e| Error::ReadFile(e, path))?;
let schema: serde_json::Value = serde_json::from_str(&json)?;
let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
let plugin_crate_name = plugin_crate_name
.strip_prefix("tauri-plugin-")
.map(ToString::to_string)
.unwrap_or(plugin_crate_name);
schemas_map.insert(plugin_crate_name, schema);
}
}
Ok(schemas_map)
}
/// Parses all capability files with the given glob pattern.
pub fn parse_capabilities(pattern: &str) -> Result<BTreeMap<String, Capability>, Error> {
let mut capabilities_map = BTreeMap::new();
for path in glob::glob(pattern)?
.flatten() // filter extension
.filter(|p| {
p.extension()
.and_then(|e| e.to_str())
.map(|e| CAPABILITY_FILE_EXTENSIONS.contains(&e))
.unwrap_or_default()
})
// filter schema files
// TODO: remove this before stable
.filter(|p| p.parent().unwrap().file_name().unwrap() != CAPABILITIES_SCHEMA_FOLDER_NAME)
{
match CapabilityFile::load(&path)? {
CapabilityFile::Capability(capability) => {
if capabilities_map.contains_key(&capability.identifier) {
return Err(Error::CapabilityAlreadyExists {
identifier: capability.identifier,
});
}
capabilities_map.insert(capability.identifier.clone(), capability);
}
CapabilityFile::List(capabilities) | CapabilityFile::NamedList { capabilities } => {
for capability in capabilities {
if capabilities_map.contains_key(&capability.identifier) {
return Err(Error::CapabilityAlreadyExists {
identifier: capability.identifier,
});
}
capabilities_map.insert(capability.identifier.clone(), capability);
}
}
}
}
Ok(capabilities_map)
}
/// Permissions that are generated from commands using [`autogenerate_command_permissions`].
pub struct AutogeneratedPermissions {
/// The allow permissions generated from commands.
pub allowed: Vec<String>,
/// The deny permissions generated from commands.
pub denied: Vec<String>,
}
/// Autogenerate permission files for a list of commands.
pub fn autogenerate_command_permissions(
path: &Path,
commands: &[&str],
license_header: &str,
schema_ref: bool,
) -> AutogeneratedPermissions {
if !path.exists() {
fs::create_dir_all(path).expect("unable to create autogenerated commands dir");
}
let schema_entry = if schema_ref {
let cwd = env::current_dir().unwrap();
let components_len = path.strip_prefix(&cwd).unwrap_or(path).components().count();
let schema_path = (1..components_len)
.map(|_| "..")
.collect::<PathBuf>()
.join(PERMISSION_SCHEMAS_FOLDER_NAME)
.join(PERMISSION_SCHEMA_FILE_NAME);
format!(
"\n\"$schema\" = \"{}\"\n",
dunce::simplified(&schema_path)
.display()
.to_string()
.replace('\\', "/")
)
} else {
"".to_string()
};
let mut autogenerated = AutogeneratedPermissions {
allowed: Vec::new(),
denied: Vec::new(),
};
for command in commands {
let slugified_command = command.replace('_', "-");
let toml = format!(
r###"{license_header}# Automatically generated - DO NOT EDIT!
{schema_entry}
[[permission]]
identifier = "allow-{slugified_command}"
description = "Enables the {command} command without any pre-configured scope."
commands.allow = ["{command}"]
[[permission]]
identifier = "deny-{slugified_command}"
description = "Denies the {command} command without any pre-configured scope."
commands.deny = ["{command}"]
"###,
);
let out_path = path.join(format!("{command}.toml"));
write_if_changed(&out_path, toml)
.unwrap_or_else(|_| panic!("unable to autogenerate {out_path:?}"));
autogenerated
.allowed
.push(format!("allow-{slugified_command}"));
autogenerated
.denied
.push(format!("deny-{slugified_command}"));
}
autogenerated
}
const PERMISSION_TABLE_HEADER: &str =
"## Permission Table\n\n<table>\n<tr>\n<th>Identifier</th>\n<th>Description</th>\n</tr>\n";
/// Generate a markdown documentation page containing the list of permissions of the plugin.
pub fn generate_docs(
permissions: &[PermissionFile],
out_dir: &Path,
plugin_identifier: &str,
) -> Result<(), Error> {
let mut default_permission = "".to_owned();
let mut permission_table = "".to_string();
fn docs_from(id: &str, description: Option<&str>, plugin_identifier: &str) -> String {
let mut docs = format!("\n<tr>\n<td>\n\n`{plugin_identifier}:{id}`\n\n</td>\n");
if let Some(d) = description {
docs.push_str(&format!("<td>\n\n{d}\n\n</td>"));
}
docs.push_str("\n</tr>");
docs
}
for permission in permissions {
for set in &permission.set {
permission_table.push_str(&docs_from(
&set.identifier,
Some(&set.description),
plugin_identifier,
));
permission_table.push('\n');
}
if let Some(default) = &permission.default {
default_permission.push_str("## Default Permission\n\n");
default_permission.push_str(default.description.as_deref().unwrap_or_default().trim());
default_permission.push('\n');
default_permission.push('\n');
if !default.permissions.is_empty() {
default_permission.push_str("#### This default permission set includes the following:\n\n");
for permission in &default.permissions {
default_permission.push_str(&format!("- `{permission}`\n"));
}
default_permission.push('\n');
}
}
for permission in &permission.permission {
permission_table.push_str(&docs_from(
&permission.identifier,
permission.description.as_deref(),
plugin_identifier,
));
permission_table.push('\n');
}
}
let docs = format!("{default_permission}{PERMISSION_TABLE_HEADER}\n{permission_table}</table>\n");
let reference_path = out_dir.join(PERMISSION_DOCS_FILE_NAME);
write_if_changed(&reference_path, docs).map_err(|e| Error::WriteFile(e, reference_path))?;
Ok(())
}
// TODO: We have way too many duplicated code around getting the config files, e.g.
// - crates/tauri-codegen/src/lib.rs (`get_config`)
// - crates/tauri-build/src/lib.rs (`try_build`)
// - crates/tauri-cli/src/helpers/config.rs (`get_internal`)
/// Generate allowed commands file for the `generate_handler` macro to remove never allowed commands
pub fn generate_allowed_commands(
out_dir: &Path,
capabilities_from_files: Option<BTreeMap<String, Capability>>,
permissions_map: BTreeMap<String, Vec<PermissionFile>>,
) -> Result<(), anyhow::Error> {
println!("cargo:rerun-if-env-changed={REMOVE_UNUSED_COMMANDS_ENV_VAR}");
let allowed_commands_file_path = out_dir.join(ALLOWED_COMMANDS_FILE_NAME);
let remove_unused_commands_env_var = std::env::var(REMOVE_UNUSED_COMMANDS_ENV_VAR);
let should_generate_allowed_commands =
remove_unused_commands_env_var.is_ok() && !permissions_map.is_empty();
if !should_generate_allowed_commands {
let _ = std::fs::remove_file(allowed_commands_file_path);
return Ok(());
}
// It's safe to `unwrap` here since we have checked if the result is ok above
let config_directory = PathBuf::from(remove_unused_commands_env_var.unwrap());
let capabilities_path = config_directory.join("capabilities");
// Cargo re-builds if the variable points to an empty path,
// so we check for exists here
// see https://github.com/rust-lang/cargo/issues/4213
if capabilities_path.exists() {
println!("cargo:rerun-if-changed={}", capabilities_path.display());
}
let target_triple = env::var("TARGET")?;
let target = crate::platform::Target::from_triple(&target_triple);
let (mut config, config_paths) = crate::config::parse::read_from(target, &config_directory)?;
for config_file_path in config_paths {
println!("cargo:rerun-if-changed={}", config_file_path.display());
}
if let Ok(env) = std::env::var("TAURI_CONFIG") {
let merge_config: serde_json::Value = serde_json::from_str(&env)?;
json_patch::merge(&mut config, &merge_config);
}
println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
// Set working directory to where `tauri.config.json` is, so that relative paths in it are parsed correctly.
let old_cwd = std::env::current_dir()?;
std::env::set_current_dir(config_directory)?;
let config: Config = serde_json::from_value(config)?;
// Reset working directory.
std::env::set_current_dir(old_cwd)?;
let acl: BTreeMap<String, crate::acl::manifest::Manifest> = permissions_map
.into_iter()
.map(|(key, permissions)| {
let key = key
.strip_prefix("tauri-plugin-")
.unwrap_or(&key)
.to_string();
let manifest = crate::acl::manifest::Manifest::new(permissions, None);
(key, manifest)
})
.collect();
let capabilities_from_files = if let Some(capabilities) = capabilities_from_files {
capabilities
} else {
crate::acl::build::parse_capabilities(&format!(
"{}/**/*",
glob::Pattern::escape(&capabilities_path.to_string_lossy())
))?
};
let capabilities = crate::acl::get_capabilities(&config, capabilities_from_files, None)?;
let permission_entries = capabilities
.into_values()
.flat_map(|capabilities| capabilities.permissions);
let mut allowed_commands = AllowedCommands {
has_app_acl: has_app_manifest(&acl),
..Default::default()
};
for permission_entry in permission_entries {
let Ok(permissions) =
crate::acl::resolved::get_permissions(permission_entry.identifier(), &acl)
else {
continue;
};
for permission in permissions {
let plugin_name = permission.key;
let allowed_command_names = &permission.permission.commands.allow;
for allowed_command in allowed_command_names {
let command_name = if plugin_name == crate::acl::APP_ACL_KEY {
allowed_command.to_string()
} else if let Some(core_plugin_name) = plugin_name.strip_prefix("core:") {
format!("plugin:{core_plugin_name}|{allowed_command}")
} else {
format!("plugin:{plugin_name}|{allowed_command}")
};
allowed_commands.commands.insert(command_name);
}
}
}
write_if_changed(
allowed_commands_file_path,
serde_json::to_string(&allowed_commands)?,
)?;
Ok(())
}

View file

@ -0,0 +1,452 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! End-user abstraction for selecting permissions a window has access to.
use std::{path::Path, str::FromStr};
use crate::{acl::Identifier, platform::Target};
use serde::{
Deserialize, Deserializer, Serialize,
de::{Error, IntoDeserializer},
};
use serde_untagged::UntaggedEnumVisitor;
use super::Scopes;
/// An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`]
/// or an object that references a permission and extends its scope.
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum PermissionEntry {
/// Reference a permission or permission set by identifier.
PermissionRef(Identifier),
/// Reference a permission or permission set by identifier and extends its scope.
ExtendedPermission {
/// Identifier of the permission or permission set.
identifier: Identifier,
/// Scope to append to the existing permission scope.
#[serde(default, flatten)]
scope: Scopes,
},
}
impl PermissionEntry {
/// The identifier of the permission referenced in this entry.
pub fn identifier(&self) -> &Identifier {
match self {
Self::PermissionRef(identifier) => identifier,
Self::ExtendedPermission {
identifier,
scope: _,
} => identifier,
}
}
}
impl<'de> Deserialize<'de> for PermissionEntry {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct ExtendedPermissionStruct {
identifier: Identifier,
#[serde(default, flatten)]
scope: Scopes,
}
UntaggedEnumVisitor::new()
.string(|string| {
let de = string.into_deserializer();
Identifier::deserialize(de).map(Self::PermissionRef)
})
.map(|map| {
let ext_perm = map.deserialize::<ExtendedPermissionStruct>()?;
Ok(Self::ExtendedPermission {
identifier: ext_perm.identifier,
scope: ext_perm.scope,
})
})
.deserialize(deserializer)
}
}
/// A grouping and boundary mechanism developers can use to isolate access to the IPC layer.
///
/// It controls application windows' and webviews' fine grained access
/// to the Tauri core, application, or plugin commands.
/// If a webview or its window is not matching any capability then it has no access to the IPC layer at all.
///
/// This can be done to create groups of windows, based on their required system access, which can reduce
/// impact of frontend vulnerabilities in less privileged windows.
/// Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`.
/// A Window can have none, one, or multiple associated capabilities.
///
/// ## Example
///
/// ```json
/// {
/// "identifier": "main-user-files-write",
/// "description": "This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.",
/// "windows": [
/// "main"
/// ],
/// "permissions": [
/// "core:default",
/// "dialog:open",
/// {
/// "identifier": "fs:allow-write-text-file",
/// "allow": [{ "path": "$HOME/test.txt" }]
/// },
/// ],
/// "platforms": ["macOS","windows"]
/// }
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Capability {
/// Identifier of the capability.
///
/// ## Example
///
/// `main-user-files-write`
///
pub identifier: String,
/// Description of what the capability is intended to allow on associated windows.
///
/// It should contain a description of what the grouped permissions should allow.
///
/// ## Example
///
/// This capability allows the `main` window access to `filesystem` write related
/// commands and `dialog` commands to enable programmatic access to files selected by the user.
#[serde(default)]
pub description: String,
/// Configure remote URLs that can use the capability permissions.
///
/// This setting is optional and defaults to not being set, as our
/// default use case is that the content is served from our local application.
///
/// :::caution
/// Make sure you understand the security implications of providing remote
/// sources with local system access.
/// :::
///
/// ## Example
///
/// ```json
/// {
/// "urls": ["https://*.mydomain.dev"]
/// }
/// ```
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote: Option<CapabilityRemote>,
/// Whether this capability is enabled for local app URLs or not. Defaults to `true`.
#[serde(default = "default_capability_local")]
pub local: bool,
/// List of windows that are affected by this capability. Can be a glob pattern.
///
/// If a window label matches any of the patterns in this list,
/// the capability will be enabled on all the webviews of that window,
/// regardless of the value of [`Self::webviews`].
///
/// On multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`]
/// for a fine grained access control.
///
/// ## Example
///
/// `["main"]`
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub windows: Vec<String>,
/// List of webviews that are affected by this capability. Can be a glob pattern.
///
/// The capability will be enabled on all the webviews
/// whose label matches any of the patterns in this list,
/// regardless of whether the webview's window label matches a pattern in [`Self::windows`].
///
/// ## Example
///
/// `["sub-webview-one", "sub-webview-two"]`
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub webviews: Vec<String>,
/// List of permissions attached to this capability.
///
/// Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.
/// For commands directly implemented in the application itself only `${permission-name}`
/// is required.
///
/// ## Example
///
/// ```json
/// [
/// "core:default",
/// "shell:allow-open",
/// "dialog:open",
/// {
/// "identifier": "fs:allow-write-text-file",
/// "allow": [{ "path": "$HOME/test.txt" }]
/// }
/// ]
/// ```
#[cfg_attr(feature = "schema", schemars(schema_with = "unique_permission"))]
pub permissions: Vec<PermissionEntry>,
/// Limit which target platforms this capability applies to.
///
/// By default all platforms are targeted.
///
/// ## Example
///
/// `["macOS","windows"]`
#[serde(skip_serializing_if = "Option::is_none")]
pub platforms: Option<Vec<Target>>,
}
impl Capability {
/// Whether this capability should be active based on the platform target or not.
pub fn is_active(&self, target: &Target) -> bool {
self
.platforms
.as_ref()
.map(|platforms| platforms.contains(target))
.unwrap_or(true)
}
}
#[cfg(feature = "schema")]
fn unique_permission(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let items = serde_json::Value::from(generator.subschema_for::<PermissionEntry>());
schemars::json_schema!({
"type": "array",
"uniqueItems": true,
"items": items
})
}
fn default_capability_local() -> bool {
true
}
/// Configuration for remote URLs that are associated with the capability.
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct CapabilityRemote {
/// Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).
///
/// ## Examples
///
/// - "https://*.mydomain.dev": allows subdomains of mydomain.dev
/// - "https://mydomain.dev/api/*": allows any subpath of mydomain.dev/api
pub urls: Vec<String>,
}
/// Capability formats accepted in a capability file.
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schema", schemars(untagged))]
#[cfg_attr(test, derive(Debug, PartialEq))]
pub enum CapabilityFile {
/// A single capability.
Capability(Capability),
/// A list of capabilities.
List(Vec<Capability>),
/// A list of capabilities.
NamedList {
/// The list of capabilities.
capabilities: Vec<Capability>,
},
}
impl CapabilityFile {
/// Load the given capability file.
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, super::Error> {
let path = path.as_ref();
let capability_file =
std::fs::read_to_string(path).map_err(|e| super::Error::ReadFile(e, path.into()))?;
let ext = path.extension().unwrap().to_string_lossy().to_string();
let file: Self = match ext.as_str() {
"toml" => toml::from_str(&capability_file)?,
"json" => serde_json::from_str(&capability_file)?,
#[cfg(feature = "config-json5")]
"json5" => json5::from_str(&capability_file)?,
_ => return Err(super::Error::UnknownCapabilityFormat(ext)),
};
Ok(file)
}
}
impl<'de> Deserialize<'de> for CapabilityFile {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
UntaggedEnumVisitor::new()
.seq(|seq| seq.deserialize::<Vec<Capability>>().map(Self::List))
.map(|map| {
#[derive(Deserialize)]
struct CapabilityNamedList {
capabilities: Vec<Capability>,
}
let value: serde_json::Map<String, serde_json::Value> = map.deserialize()?;
if value.contains_key("capabilities") {
serde_json::from_value::<CapabilityNamedList>(value.into())
.map(|named| Self::NamedList {
capabilities: named.capabilities,
})
.map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
} else {
serde_json::from_value::<Capability>(value.into())
.map(Self::Capability)
.map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
}
})
.deserialize(deserializer)
}
}
impl FromStr for CapabilityFile {
type Err = super::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
.or_else(|_| toml::from_str(s))
.map_err(Into::into)
}
}
#[cfg(any(feature = "build", feature = "build-2"))]
mod build {
use std::convert::identity;
use proc_macro2::TokenStream;
use quote::{ToTokens, TokenStreamExt, quote};
use super::*;
use crate::{literal_struct, tokens::*};
impl ToTokens for CapabilityRemote {
fn to_tokens(&self, tokens: &mut TokenStream) {
let urls = vec_lit(&self.urls, str_lit);
literal_struct!(
tokens,
::tauri::utils::acl::capability::CapabilityRemote,
urls
);
}
}
impl ToTokens for PermissionEntry {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::acl::capability::PermissionEntry };
tokens.append_all(match self {
Self::PermissionRef(id) => {
quote! { #prefix::PermissionRef(#id) }
}
Self::ExtendedPermission { identifier, scope } => {
quote! { #prefix::ExtendedPermission {
identifier: #identifier,
scope: #scope
} }
}
});
}
}
impl ToTokens for Capability {
fn to_tokens(&self, tokens: &mut TokenStream) {
let identifier = str_lit(&self.identifier);
let description = str_lit(&self.description);
let remote = opt_lit(self.remote.as_ref());
let local = self.local;
let windows = vec_lit(&self.windows, str_lit);
let webviews = vec_lit(&self.webviews, str_lit);
let permissions = vec_lit(&self.permissions, identity);
let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
literal_struct!(
tokens,
::tauri::utils::acl::capability::Capability,
identifier,
description,
remote,
local,
windows,
webviews,
permissions,
platforms
);
}
}
}
#[cfg(test)]
mod tests {
use crate::acl::{Identifier, Scopes};
use super::{Capability, CapabilityFile, PermissionEntry};
#[test]
fn permission_entry_de() {
let identifier = Identifier::try_from("plugin:perm".to_string()).unwrap();
let identifier_json = serde_json::to_string(&identifier).unwrap();
assert_eq!(
serde_json::from_str::<PermissionEntry>(&identifier_json).unwrap(),
PermissionEntry::PermissionRef(identifier.clone())
);
assert_eq!(
serde_json::from_value::<PermissionEntry>(serde_json::json!({
"identifier": identifier,
"allow": [],
"deny": null
}))
.unwrap(),
PermissionEntry::ExtendedPermission {
identifier,
scope: Scopes {
allow: Some(vec![]),
deny: None
}
}
);
}
#[test]
fn capability_file_de() {
let capability = Capability {
identifier: "test".into(),
description: "".into(),
remote: None,
local: true,
windows: vec![],
webviews: vec![],
permissions: vec![],
platforms: None,
};
let capability_json = serde_json::to_string(&capability).unwrap();
assert_eq!(
serde_json::from_str::<CapabilityFile>(&capability_json).unwrap(),
CapabilityFile::Capability(capability.clone())
);
assert_eq!(
serde_json::from_str::<CapabilityFile>(&format!("[{capability_json}]")).unwrap(),
CapabilityFile::List(vec![capability.clone()])
);
assert_eq!(
serde_json::from_str::<CapabilityFile>(&format!(
"{{ \"capabilities\": [{capability_json}] }}"
))
.unwrap(),
CapabilityFile::NamedList {
capabilities: vec![capability]
}
);
}
}

View file

@ -0,0 +1,301 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Identifier for plugins.
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::num::NonZeroU8;
use thiserror::Error;
const IDENTIFIER_SEPARATOR: u8 = b':';
const PLUGIN_PREFIX: &str = "tauri-plugin-";
const CORE_PLUGIN_IDENTIFIER_PREFIX: &str = "core:";
// <https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field>
const MAX_LEN_PREFIX: usize = 64 - PLUGIN_PREFIX.len();
const MAX_LEN_BASE: usize = 64;
const MAX_LEN_IDENTIFIER: usize = MAX_LEN_PREFIX + 1 + MAX_LEN_BASE;
/// Plugin identifier.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Identifier {
inner: String,
separator: Option<NonZeroU8>,
}
#[cfg(feature = "schema")]
impl schemars::JsonSchema for Identifier {
fn schema_name() -> std::borrow::Cow<'static, str> {
"Identifier".into()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(module_path!(), "::Identifier"))
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
String::json_schema(generator)
}
}
impl AsRef<str> for Identifier {
#[inline(always)]
fn as_ref(&self) -> &str {
&self.inner
}
}
impl Identifier {
/// Get the identifier str.
#[inline(always)]
pub fn get(&self) -> &str {
self.as_ref()
}
/// Get the identifier without prefix.
pub fn get_base(&self) -> &str {
match self.separator_index() {
None => self.get(),
Some(i) => &self.inner[i + 1..],
}
}
/// Get the prefix of the identifier.
pub fn get_prefix(&self) -> Option<&str> {
self.separator_index().map(|i| &self.inner[0..i])
}
/// Set the identifier prefix.
pub fn set_prefix(&mut self) -> Result<(), ParseIdentifierError> {
todo!()
}
/// Get the identifier string and its separator.
pub fn into_inner(self) -> (String, Option<NonZeroU8>) {
(self.inner, self.separator)
}
fn separator_index(&self) -> Option<usize> {
self.separator.map(|i| i.get() as usize)
}
}
#[derive(Debug)]
enum ValidByte {
Separator,
Byte(u8),
}
impl ValidByte {
fn alpha_numeric(byte: u8) -> Option<Self> {
byte.is_ascii_alphanumeric().then_some(Self::Byte(byte))
}
fn alpha_numeric_hyphen(byte: u8) -> Option<Self> {
(byte.is_ascii_alphanumeric() || byte == b'-').then_some(Self::Byte(byte))
}
fn next(&self, next: u8) -> Option<ValidByte> {
match (self, next) {
(ValidByte::Byte(b'-'), IDENTIFIER_SEPARATOR) => None,
(ValidByte::Separator, b'-') => None,
(_, IDENTIFIER_SEPARATOR) => Some(ValidByte::Separator),
(ValidByte::Separator, next) => ValidByte::alpha_numeric(next),
(ValidByte::Byte(b'-'), next) => ValidByte::alpha_numeric_hyphen(next),
(ValidByte::Byte(b'_'), next) => ValidByte::alpha_numeric_hyphen(next),
(ValidByte::Byte(_), next) => ValidByte::alpha_numeric_hyphen(next),
}
}
}
/// Errors that can happen when parsing an identifier.
#[derive(Debug, Error)]
pub enum ParseIdentifierError {
/// Identifier start with the plugin prefix.
#[error("identifiers cannot start with {}", PLUGIN_PREFIX)]
StartsWithTauriPlugin,
/// Identifier empty.
#[error("identifiers cannot be empty")]
Empty,
/// Identifier is too long.
#[error("identifiers cannot be longer than {len}, found {0}", len = MAX_LEN_IDENTIFIER)]
Humongous(usize),
/// Identifier is not in a valid format.
#[error(
"identifiers can only include lowercase ASCII, hyphens which are not leading or trailing, and a single colon if using a prefix"
)]
InvalidFormat,
/// Identifier has multiple separators.
#[error(
"identifiers can only include a single separator '{}'",
IDENTIFIER_SEPARATOR
)]
MultipleSeparators,
/// Identifier has a trailing hyphen.
#[error("identifiers cannot have a trailing hyphen")]
TrailingHyphen,
/// Identifier has a prefix without a base.
#[error("identifiers cannot have a prefix without a base")]
PrefixWithoutBase,
}
impl TryFrom<String> for Identifier {
type Error = ParseIdentifierError;
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.starts_with(PLUGIN_PREFIX) {
return Err(Self::Error::StartsWithTauriPlugin);
}
if value.is_empty() {
return Err(Self::Error::Empty);
}
if value.len() > MAX_LEN_IDENTIFIER {
return Err(Self::Error::Humongous(value.len()));
}
let is_core_identifier = value.starts_with(CORE_PLUGIN_IDENTIFIER_PREFIX);
let mut bytes = value.bytes();
// grab the first byte only before parsing the rest
let mut prev = bytes
.next()
.and_then(ValidByte::alpha_numeric)
.ok_or(Self::Error::InvalidFormat)?;
let mut idx = 0;
let mut separator = None;
for byte in bytes {
idx += 1; // we already consumed first item
match prev.next(byte) {
None => return Err(Self::Error::InvalidFormat),
Some(next @ ValidByte::Byte(_)) => prev = next,
Some(ValidByte::Separator) => {
if separator.is_none() || is_core_identifier {
// safe to unwrap because idx starts at 1 and cannot go over MAX_IDENTIFIER_LEN
separator = Some(idx.try_into().unwrap());
prev = ValidByte::Separator
} else {
return Err(Self::Error::MultipleSeparators);
}
}
}
}
match prev {
// empty base
ValidByte::Separator => return Err(Self::Error::PrefixWithoutBase),
// trailing hyphen
ValidByte::Byte(b'-') => return Err(Self::Error::TrailingHyphen),
_ => (),
}
Ok(Self {
inner: value,
separator,
})
}
}
impl<'de> Deserialize<'de> for Identifier {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::try_from(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
}
}
impl Serialize for Identifier {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.get())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ident(s: impl Into<String>) -> Result<Identifier, ParseIdentifierError> {
Identifier::try_from(s.into())
}
#[test]
fn max_len_fits_in_u8() {
assert!(MAX_LEN_IDENTIFIER < u8::MAX as usize)
}
#[test]
fn format() {
assert!(ident("prefix:base").is_ok());
assert!(ident("prefix3:base").is_ok());
assert!(ident("preFix:base").is_ok());
// bad
assert!(ident("tauri-plugin-prefix:base").is_err());
assert!(ident("-prefix-:-base-").is_err());
assert!(ident("-prefix:base").is_err());
assert!(ident("prefix-:base").is_err());
assert!(ident("prefix:-base").is_err());
assert!(ident("prefix:base-").is_err());
assert!(ident("pre--fix:base--sep").is_ok());
assert!(ident("prefix:base--sep").is_ok());
assert!(ident("pre--fix:base").is_ok());
assert!(ident("prefix::base").is_err());
assert!(ident(":base").is_err());
assert!(ident("prefix:").is_err());
assert!(ident(":prefix:base:").is_err());
assert!(ident("base:").is_err());
assert!(ident("").is_err());
assert!(ident("💩").is_err());
assert!(ident("a".repeat(MAX_LEN_IDENTIFIER + 1)).is_err());
}
#[test]
fn base() {
assert_eq!(ident("prefix:base").unwrap().get_base(), "base");
assert_eq!(ident("base").unwrap().get_base(), "base");
}
#[test]
fn prefix() {
assert_eq!(ident("prefix:base").unwrap().get_prefix(), Some("prefix"));
assert_eq!(ident("base").unwrap().get_prefix(), None);
}
}
#[cfg(any(feature = "build", feature = "build-2"))]
mod build {
use proc_macro2::TokenStream;
use quote::{ToTokens, TokenStreamExt, quote};
use super::*;
impl ToTokens for Identifier {
fn to_tokens(&self, tokens: &mut TokenStream) {
let s = self.get();
tokens
.append_all(quote! { ::tauri::utils::acl::Identifier::try_from(#s.to_string()).unwrap() })
}
}
}

View file

@ -0,0 +1,196 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Plugin ACL types.
use std::{collections::BTreeMap, num::NonZeroU64};
use super::{Permission, PermissionSet};
use serde::{Deserialize, Serialize};
/// The default permission set of the plugin.
///
/// Works similarly to a permission with the "default" identifier.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DefaultPermission {
/// The version of the permission.
pub version: Option<NonZeroU64>,
/// Human-readable description of what the permission does.
/// Tauri convention is to use `<h4>` headings in markdown content
/// for Tauri documentation generation purposes.
pub description: Option<String>,
/// All permissions this set contains.
pub permissions: Vec<String>,
}
/// Permission file that can define a default permission, a set of permissions or a list of inlined permissions.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PermissionFile {
/// The default permission set for the plugin
pub default: Option<DefaultPermission>,
/// A list of permissions sets defined
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub set: Vec<PermissionSet>,
/// A list of inlined permissions
#[serde(default)]
pub permission: Vec<Permission>,
}
/// Plugin manifest.
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Manifest {
/// Default permission.
pub default_permission: Option<PermissionSet>,
/// Plugin permissions.
pub permissions: BTreeMap<String, Permission>,
/// Plugin permission sets.
pub permission_sets: BTreeMap<String, PermissionSet>,
/// The global scope schema.
pub global_scope_schema: Option<serde_json::Value>,
}
impl Manifest {
/// Creates a new manifest from the given plugin permission files and global scope schema.
pub fn new(
permission_files: Vec<PermissionFile>,
global_scope_schema: Option<serde_json::Value>,
) -> Self {
let mut manifest = Self {
default_permission: None,
permissions: BTreeMap::new(),
permission_sets: BTreeMap::new(),
global_scope_schema,
};
for permission_file in permission_files {
if let Some(default) = permission_file.default {
manifest.default_permission.replace(PermissionSet {
identifier: "default".into(),
description: default
.description
.unwrap_or_else(|| "Default plugin permissions.".to_string()),
permissions: default.permissions,
});
}
for permission in permission_file.permission {
let key = permission.identifier.clone();
manifest.permissions.insert(key, permission);
}
for set in permission_file.set {
let key = set.identifier.clone();
manifest.permission_sets.insert(key, set);
}
}
manifest
}
}
#[cfg(feature = "schema")]
type ScopeSchema = (schemars::Schema, serde_json::Map<String, serde_json::Value>);
#[cfg(feature = "schema")]
impl Manifest {
/// Return scope schema and extra schema definitions for this plugin manifest.
pub fn global_scope_schema(&self) -> Result<Option<ScopeSchema>, super::Error> {
self
.global_scope_schema
.as_ref()
.map(|s| {
serde_json::from_value::<schemars::Schema>(s.clone()).map(|mut root| {
// Extract definitions from the schema
let definitions = root
.remove("$defs")
.or_else(|| root.remove("definitions"))
.and_then(|v| match v {
serde_json::Value::Object(m) => Some(m),
_ => None,
})
.unwrap_or_default();
// Wrap in an array schema
let items = serde_json::Value::from(root);
let scope_schema = schemars::json_schema!({
"type": "array",
"items": items
});
(scope_schema, definitions)
})
})
.transpose()
.map_err(Into::into)
}
}
#[cfg(any(feature = "build", feature = "build-2"))]
mod build {
use proc_macro2::TokenStream;
use quote::{ToTokens, TokenStreamExt, quote};
use std::convert::identity;
use super::*;
use crate::{literal_struct, tokens::*};
impl ToTokens for DefaultPermission {
fn to_tokens(&self, tokens: &mut TokenStream) {
let version = opt_lit_owned(self.version.as_ref().map(|v| {
let v = v.get();
quote!(::core::num::NonZeroU64::new(#v).unwrap())
}));
// Only used in build script and macros, so don't include them in runtime
let description = quote! { ::core::option::Option::None };
let permissions = vec_lit(&self.permissions, str_lit);
literal_struct!(
tokens,
::tauri::utils::acl::plugin::DefaultPermission,
version,
description,
permissions
)
}
}
impl ToTokens for Manifest {
fn to_tokens(&self, tokens: &mut TokenStream) {
let default_permission = opt_lit(self.default_permission.as_ref());
let permissions = map_lit(
quote! { ::std::collections::BTreeMap },
&self.permissions,
str_lit,
identity,
);
let permission_sets = map_lit(
quote! { ::std::collections::BTreeMap },
&self.permission_sets,
str_lit,
identity,
);
// Only used in build script and macros, so don't include them in runtime
// let global_scope_schema =
// opt_lit_owned(self.global_scope_schema.as_ref().map(json_value_lit));
let global_scope_schema = quote! { ::core::option::Option::None };
literal_struct!(
tokens,
::tauri::utils::acl::manifest::Manifest,
default_permission,
permissions,
permission_sets,
global_scope_schema
)
}
}
}

View file

@ -0,0 +1,550 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Access Control List types.
//!
//! # Stability
//!
//! This is a core functionality that is not considered part of the stable API.
//! If you use it, note that it may include breaking changes in the future.
//!
//! These items are intended to be non-breaking from a de/serialization standpoint only.
//! Using and modifying existing config values will try to avoid breaking changes, but they are
//! free to add fields in the future - causing breaking changes for creating and full destructuring.
//!
//! To avoid this, [ignore unknown fields when destructuring] with the `{my, config, ..}` pattern.
//! If you need to create the Rust config directly without deserializing, then create the struct
//! the [Struct Update Syntax] with `..Default::default()`, which may need a
//! `#[allow(clippy::needless_update)]` attribute if you are declaring all fields.
//!
//! [ignore unknown fields when destructuring]: https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#ignoring-remaining-parts-of-a-value-with-
//! [Struct Update Syntax]: https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax
use anyhow::Context;
use capability::{Capability, CapabilityFile};
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, HashSet},
fs,
num::NonZeroU64,
path::PathBuf,
str::FromStr,
sync::Arc,
};
use thiserror::Error;
use url::Url;
use crate::{
config::{CapabilityEntry, Config},
platform::Target,
};
pub use self::{identifier::*, value::*};
/// Known foldername of the permission schema files
pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas";
/// Known filename of the permission schema JSON file
pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";
/// Known ACL key for the app permissions.
pub const APP_ACL_KEY: &str = "__app-acl__";
/// Known acl manifests file
pub const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
/// Known capabilities file
pub const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
/// Allowed commands file name
pub const ALLOWED_COMMANDS_FILE_NAME: &str = "allowed-commands.json";
/// Set by the CLI with when `build > removeUnusedCommands` is set for dead code elimination,
/// the value is set to the config's directory
pub const REMOVE_UNUSED_COMMANDS_ENV_VAR: &str = "REMOVE_UNUSED_COMMANDS";
#[cfg(any(feature = "build", feature = "build-2"))]
pub mod build;
pub mod capability;
pub mod identifier;
pub mod manifest;
pub mod resolved;
#[cfg(feature = "schema")]
pub mod schema;
pub mod value;
/// Possible errors while processing ACL files.
#[derive(Debug, Error)]
pub enum Error {
/// Could not find an environmental variable that is set inside of build scripts.
///
/// Whatever generated this should be called inside of a build script.
#[error(
"expected build script env var {0}, but it was not found - ensure this is called in a build script"
)]
BuildVar(&'static str),
/// The links field in the manifest **MUST** be set and match the name of the crate.
#[error(
"package.links field in the Cargo manifest is not set, it should be set to the same as package.name"
)]
LinksMissing,
/// The links field in the manifest **MUST** match the name of the crate.
#[error(
"package.links field in the Cargo manifest MUST be set to the same value as package.name"
)]
LinksName,
/// IO error while reading a file
#[error("failed to read file '{}': {}", _1.display(), _0)]
ReadFile(std::io::Error, PathBuf),
/// IO error while writing a file
#[error("failed to write file '{}': {}", _1.display(), _0)]
WriteFile(std::io::Error, PathBuf),
/// IO error while creating a file
#[error("failed to create file '{}': {}", _1.display(), _0)]
CreateFile(std::io::Error, PathBuf),
/// IO error while creating a dir
#[error("failed to create dir '{}': {}", _1.display(), _0)]
CreateDir(std::io::Error, PathBuf),
/// [`cargo_metadata`] was not able to complete successfully
#[cfg(any(feature = "build", feature = "build-2"))]
#[error("failed to execute: {0}")]
Metadata(#[from] ::cargo_metadata::Error),
/// Invalid glob
#[error("failed to run glob: {0}")]
Glob(#[from] glob::PatternError),
/// Invalid TOML encountered
#[error("failed to parse TOML: {0}")]
Toml(#[from] toml::de::Error),
/// Invalid JSON encountered
#[error("failed to parse JSON: {0}")]
Json(#[from] serde_json::Error),
/// Invalid JSON5 encountered
#[cfg(feature = "config-json5")]
#[error("failed to parse JSON5: {0}")]
Json5(#[from] json5::Error),
/// Invalid permissions file format
#[error("unknown permission format {0}")]
UnknownPermissionFormat(String),
/// Invalid capabilities file format
#[error("unknown capability format {0}")]
UnknownCapabilityFormat(String),
/// Permission referenced in set not found.
#[error("permission {permission} not found from set {set}")]
SetPermissionNotFound {
/// Permission identifier.
permission: String,
/// Set identifier.
set: String,
},
/// Unknown ACL manifest.
#[error("unknown ACL for {key}, expected one of {available}")]
UnknownManifest {
/// Manifest key.
key: String,
/// Available manifest keys.
available: String,
},
/// Unknown permission.
#[error("unknown permission {permission} for {key}")]
UnknownPermission {
/// Manifest key.
key: String,
/// Permission identifier.
permission: String,
},
/// Capability with the given identifier already exists.
#[error("capability with identifier `{identifier}` already exists")]
CapabilityAlreadyExists {
/// Capability identifier.
identifier: String,
},
}
/// Allowed and denied commands inside a permission.
///
/// If two commands clash inside of `allow` and `deny`, it should be denied by default.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Commands {
/// Allowed command.
#[serde(default)]
pub allow: Vec<String>,
/// Denied command, which takes priority.
#[serde(default)]
pub deny: Vec<String>,
}
/// An argument for fine grained behavior control of Tauri commands.
///
/// It can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.
/// The configured scope is passed to the command and will be enforced by the command implementation.
///
/// ## Example
///
/// ```json
/// {
/// "allow": [{ "path": "$HOME/**" }],
/// "deny": [{ "path": "$HOME/secret.txt" }]
/// }
/// ```
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Scopes {
/// Data that defines what is allowed by the scope.
#[serde(skip_serializing_if = "Option::is_none")]
pub allow: Option<Vec<Value>>,
/// Data that defines what is denied by the scope. This should be prioritized by validation logic.
#[serde(skip_serializing_if = "Option::is_none")]
pub deny: Option<Vec<Value>>,
}
impl Scopes {
fn is_empty(&self) -> bool {
self.allow.is_none() && self.deny.is_none()
}
}
/// Descriptions of explicit privileges of commands.
///
/// It can enable commands to be accessible in the frontend of the application.
///
/// If the scope is defined it can be used to fine grain control the access of individual or multiple commands.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Permission {
/// The version of the permission.
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<NonZeroU64>,
/// A unique identifier for the permission.
pub identifier: String,
/// Human-readable description of what the permission does.
/// Tauri internal convention is to use `<h4>` headings in markdown content
/// for Tauri documentation generation purposes.
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Allowed or denied commands when using this permission.
#[serde(default)]
pub commands: Commands,
/// Allowed or denied scoped when using this permission.
#[serde(default, skip_serializing_if = "Scopes::is_empty")]
pub scope: Scopes,
/// Target platforms this permission applies. By default all platforms are affected by this permission.
#[serde(skip_serializing_if = "Option::is_none")]
pub platforms: Option<Vec<Target>>,
}
impl Permission {
/// Whether this permission should be active based on the platform target or not.
pub fn is_active(&self, target: &Target) -> bool {
self
.platforms
.as_ref()
.map(|platforms| platforms.contains(target))
.unwrap_or(true)
}
}
/// A set of direct permissions grouped together under a new name.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PermissionSet {
/// A unique identifier for the permission.
pub identifier: String,
/// Human-readable description of what the permission does.
pub description: String,
/// All permissions this set contains.
pub permissions: Vec<String>,
}
/// UrlPattern for [`ExecutionContext::Remote`].
#[derive(Debug, Clone)]
pub struct RemoteUrlPattern(Arc<urlpattern::UrlPattern>, String);
impl FromStr for RemoteUrlPattern {
type Err = urlpattern::quirks::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut init = urlpattern::UrlPatternInit::parse_constructor_string::<regex::Regex>(s, None)?;
if init.search.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
init.search.replace("*".to_string());
}
if init.hash.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
init.hash.replace("*".to_string());
}
if init
.pathname
.as_ref()
.map(|p| p.is_empty() || p == "/")
.unwrap_or(true)
{
init.pathname.replace("*".to_string());
}
let pattern = urlpattern::UrlPattern::parse(init, Default::default())?;
Ok(Self(Arc::new(pattern), s.to_string()))
}
}
impl RemoteUrlPattern {
#[doc(hidden)]
pub fn as_str(&self) -> &str {
&self.1
}
/// Test if a given URL matches the pattern.
pub fn test(&self, url: &Url) -> bool {
self
.0
.test(urlpattern::UrlPatternMatchInput::Url(url.clone()))
.unwrap_or_default()
}
}
impl PartialEq for RemoteUrlPattern {
fn eq(&self, other: &Self) -> bool {
self.0.protocol() == other.0.protocol()
&& self.0.username() == other.0.username()
&& self.0.password() == other.0.password()
&& self.0.hostname() == other.0.hostname()
&& self.0.port() == other.0.port()
&& self.0.pathname() == other.0.pathname()
&& self.0.search() == other.0.search()
&& self.0.hash() == other.0.hash()
}
}
impl Eq for RemoteUrlPattern {}
/// Execution context of an IPC call.
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub enum ExecutionContext {
/// A local URL is used (the Tauri app URL).
#[default]
Local,
/// Remote URL is trying to use the IPC.
Remote {
/// The URL trying to access the IPC (URL pattern).
url: RemoteUrlPattern,
},
}
/// Test if the app has an application manifest from the ACL
pub fn has_app_manifest(acl: &BTreeMap<String, crate::acl::manifest::Manifest>) -> bool {
acl.contains_key(APP_ACL_KEY)
}
/// Get the capabilities from the config file
pub fn get_capabilities(
config: &Config,
mut capabilities_from_files: BTreeMap<String, Capability>,
additional_capability_files: Option<&[PathBuf]>,
) -> anyhow::Result<BTreeMap<String, Capability>> {
let mut capabilities = if config.app.security.capabilities.is_empty() {
capabilities_from_files
} else {
let mut capabilities = BTreeMap::new();
for capability_entry in &config.app.security.capabilities {
match capability_entry {
CapabilityEntry::Inlined(capability) => {
capabilities.insert(capability.identifier.clone(), capability.clone());
}
CapabilityEntry::Reference(id) => {
let capability = capabilities_from_files
.remove(id)
.with_context(|| format!("capability with identifier {id} not found"))?;
capabilities.insert(id.clone(), capability);
}
}
}
capabilities
};
if let Some(paths) = additional_capability_files {
for path in paths {
let capability = CapabilityFile::load(path)
.with_context(|| format!("failed to read capability {}", path.display()))?;
match capability {
CapabilityFile::Capability(c) => {
capabilities.insert(c.identifier.clone(), c);
}
CapabilityFile::List(capabilities_list)
| CapabilityFile::NamedList {
capabilities: capabilities_list,
} => {
capabilities.extend(
capabilities_list
.into_iter()
.map(|c| (c.identifier.clone(), c)),
);
}
}
}
}
Ok(capabilities)
}
/// Allowed commands used to communicate between `generate_handle` and `generate_allowed_commands` through json files
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AllowedCommands {
/// The commands allowed
pub commands: HashSet<String>,
/// Has application ACL or not
pub has_app_acl: bool,
}
/// Try to reads allowed commands from the out dir made by our build script
pub fn read_allowed_commands() -> Option<AllowedCommands> {
let out_file = std::env::var("OUT_DIR")
.map(PathBuf::from)
.ok()?
.join(ALLOWED_COMMANDS_FILE_NAME);
let file = fs::read_to_string(&out_file).ok()?;
let json = serde_json::from_str(&file).ok()?;
Some(json)
}
#[cfg(test)]
mod tests {
use crate::acl::RemoteUrlPattern;
#[test]
fn url_pattern_domain_wildcard() {
let pattern: RemoteUrlPattern = "http://*".parse().unwrap();
assert!(pattern.test(&"http://tauri.app/path".parse().unwrap()));
assert!(pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
let pattern: RemoteUrlPattern = "http://*.tauri.app".parse().unwrap();
assert!(!pattern.test(&"http://tauri.app/path".parse().unwrap()));
assert!(!pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
assert!(pattern.test(&"http://api.tauri.app/path".parse().unwrap()));
assert!(pattern.test(&"http://api.tauri.app/path?q=1".parse().unwrap()));
assert!(!pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(!pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
}
#[test]
fn url_pattern_path_wildcard() {
let pattern: RemoteUrlPattern = "http://localhost/*".parse().unwrap();
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
}
#[test]
fn url_pattern_scheme_wildcard() {
let pattern: RemoteUrlPattern = "*://localhost".parse().unwrap();
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(pattern.test(&"https://localhost/path?q=1".parse().unwrap()));
assert!(pattern.test(&"custom://localhost/path".parse().unwrap()));
}
}
#[cfg(any(feature = "build", feature = "build-2"))]
mod build_ {
use std::convert::identity;
use crate::{literal_struct, tokens::*};
use super::*;
use proc_macro2::TokenStream;
use quote::{ToTokens, TokenStreamExt, quote};
impl ToTokens for ExecutionContext {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::acl::ExecutionContext };
tokens.append_all(match self {
Self::Local => {
quote! { #prefix::Local }
}
Self::Remote { url } => {
let url = url.as_str();
quote! { #prefix::Remote { url: #url.parse().unwrap() } }
}
});
}
}
impl ToTokens for Commands {
fn to_tokens(&self, tokens: &mut TokenStream) {
let allow = vec_lit(&self.allow, str_lit);
let deny = vec_lit(&self.deny, str_lit);
literal_struct!(tokens, ::tauri::utils::acl::Commands, allow, deny)
}
}
impl ToTokens for Scopes {
fn to_tokens(&self, tokens: &mut TokenStream) {
let allow = opt_vec_lit(self.allow.as_ref(), identity);
let deny = opt_vec_lit(self.deny.as_ref(), identity);
literal_struct!(tokens, ::tauri::utils::acl::Scopes, allow, deny)
}
}
impl ToTokens for Permission {
fn to_tokens(&self, tokens: &mut TokenStream) {
let version = opt_lit_owned(self.version.as_ref().map(|v| {
let v = v.get();
quote!(::core::num::NonZeroU64::new(#v).unwrap())
}));
let identifier = str_lit(&self.identifier);
// Only used in build script and macros, so don't include them in runtime
let description = quote! { ::core::option::Option::None };
let commands = &self.commands;
let scope = &self.scope;
let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
literal_struct!(
tokens,
::tauri::utils::acl::Permission,
version,
identifier,
description,
commands,
scope,
platforms
)
}
}
impl ToTokens for PermissionSet {
fn to_tokens(&self, tokens: &mut TokenStream) {
let identifier = str_lit(&self.identifier);
// Only used in build script and macros, so don't include them in runtime
let description = quote! { "".to_string() };
let permissions = vec_lit(&self.permissions, str_lit);
literal_struct!(
tokens,
::tauri::utils::acl::PermissionSet,
identifier,
description,
permissions
)
}
}
}

View file

@ -0,0 +1,683 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Resolved ACL for runtime usage.
use std::{collections::BTreeMap, fmt};
use crate::platform::Target;
use super::{
APP_ACL_KEY, Commands, Error, ExecutionContext, Identifier, Permission, PermissionSet, Scopes,
Value,
capability::{Capability, PermissionEntry},
has_app_manifest,
manifest::Manifest,
};
/// A key for a scope, used to link a [`ResolvedCommand#structfield.scope`] to the store [`Resolved#structfield.scopes`].
pub type ScopeKey = u64;
/// Metadata for what referenced a [`ResolvedCommand`].
#[cfg(debug_assertions)]
#[derive(Default, Clone, PartialEq, Eq)]
pub struct ResolvedCommandReference {
/// Identifier of the capability.
pub capability: String,
/// Identifier of the permission.
pub permission: String,
}
/// A resolved command permission.
#[derive(Default, Clone, PartialEq, Eq)]
pub struct ResolvedCommand {
/// The execution context of this command.
pub context: ExecutionContext,
/// The capability/permission that referenced this command.
#[cfg(debug_assertions)]
pub referenced_by: ResolvedCommandReference,
/// The list of window label patterns that was resolved for this command.
pub windows: Vec<glob::Pattern>,
/// The list of webview label patterns that was resolved for this command.
pub webviews: Vec<glob::Pattern>,
/// The reference of the scope that is associated with this command. See [`Resolved#structfield.command_scopes`].
pub scope_id: Option<ScopeKey>,
}
impl fmt::Debug for ResolvedCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ResolvedCommand")
.field("context", &self.context)
.field("windows", &self.windows)
.field("webviews", &self.webviews)
.field("scope_id", &self.scope_id)
.finish()
}
}
/// A resolved scope. Merges all scopes defined for a single command.
#[derive(Debug, Default, Clone)]
pub struct ResolvedScope {
/// Allows something on the command.
pub allow: Vec<Value>,
/// Denies something on the command.
pub deny: Vec<Value>,
}
/// Resolved access control list.
#[derive(Debug, Default)]
pub struct Resolved {
/// If we should check the ACL for the app commands
pub has_app_acl: bool,
/// The commands that are allowed. Map each command with its context to a [`ResolvedCommand`].
pub allowed_commands: BTreeMap<String, Vec<ResolvedCommand>>,
/// The commands that are denied. Map each command with its context to a [`ResolvedCommand`].
pub denied_commands: BTreeMap<String, Vec<ResolvedCommand>>,
/// The store of scopes referenced by a [`ResolvedCommand`].
pub command_scope: BTreeMap<ScopeKey, ResolvedScope>,
/// The global scope.
pub global_scope: BTreeMap<String, ResolvedScope>,
}
impl Resolved {
/// Resolves the ACL for the given plugin permissions and app capabilities.
pub fn resolve(
acl: &BTreeMap<String, Manifest>,
mut capabilities: BTreeMap<String, Capability>,
target: Target,
) -> Result<Self, Error> {
let mut allowed_commands = BTreeMap::new();
let mut denied_commands = BTreeMap::new();
let mut current_scope_id = 0;
let mut command_scope = BTreeMap::new();
let mut global_scope: BTreeMap<String, Vec<Scopes>> = BTreeMap::new();
// resolve commands
for capability in capabilities.values_mut().filter(|c| c.is_active(&target)) {
with_resolved_permissions(
capability,
acl,
target,
|ResolvedPermission {
key,
commands,
scope,
#[cfg_attr(not(debug_assertions), allow(unused))]
permission_name,
}| {
if commands.allow.is_empty() && commands.deny.is_empty() {
// global scope
global_scope.entry(key.to_string()).or_default().push(scope);
} else {
let scope_id = if scope.allow.is_some() || scope.deny.is_some() {
current_scope_id += 1;
command_scope.insert(
current_scope_id,
ResolvedScope {
allow: scope.allow.unwrap_or_default(),
deny: scope.deny.unwrap_or_default(),
},
);
Some(current_scope_id)
} else {
None
};
for allowed_command in &commands.allow {
resolve_command(
&mut allowed_commands,
if key == APP_ACL_KEY {
allowed_command.to_string()
} else if let Some(core_plugin_name) = key.strip_prefix("core:") {
format!("plugin:{core_plugin_name}|{allowed_command}")
} else {
format!("plugin:{key}|{allowed_command}")
},
capability,
scope_id,
#[cfg(debug_assertions)]
permission_name.to_string(),
)?;
}
for denied_command in &commands.deny {
resolve_command(
&mut denied_commands,
if key == APP_ACL_KEY {
denied_command.to_string()
} else if let Some(core_plugin_name) = key.strip_prefix("core:") {
format!("plugin:{core_plugin_name}|{denied_command}")
} else {
format!("plugin:{key}|{denied_command}")
},
capability,
scope_id,
#[cfg(debug_assertions)]
permission_name.to_string(),
)?;
}
}
Ok(())
},
)?;
}
let global_scope = global_scope
.into_iter()
.map(|(key, scopes)| {
let mut resolved_scope = ResolvedScope {
allow: Vec::new(),
deny: Vec::new(),
};
for scope in scopes {
if let Some(allow) = scope.allow {
resolved_scope.allow.extend(allow);
}
if let Some(deny) = scope.deny {
resolved_scope.deny.extend(deny);
}
}
(key, resolved_scope)
})
.collect();
let resolved = Self {
has_app_acl: has_app_manifest(acl),
allowed_commands,
denied_commands,
command_scope,
global_scope,
};
Ok(resolved)
}
}
fn parse_glob_patterns(mut raw: Vec<String>) -> Result<Vec<glob::Pattern>, Error> {
raw.sort();
let mut patterns = Vec::new();
for pattern in raw {
patterns.push(glob::Pattern::new(&pattern)?);
}
Ok(patterns)
}
fn resolve_command(
commands: &mut BTreeMap<String, Vec<ResolvedCommand>>,
command: String,
capability: &Capability,
scope_id: Option<ScopeKey>,
#[cfg(debug_assertions)] referenced_by_permission_identifier: String,
) -> Result<(), Error> {
let mut contexts = Vec::new();
if capability.local {
contexts.push(ExecutionContext::Local);
}
if let Some(remote) = &capability.remote {
contexts.extend(remote.urls.iter().map(|url| {
ExecutionContext::Remote {
url: url
.parse()
.unwrap_or_else(|e| panic!("invalid URL pattern for remote URL {url}: {e}")),
}
}));
}
for context in contexts {
let resolved_list = commands.entry(command.clone()).or_default();
resolved_list.push(ResolvedCommand {
context,
#[cfg(debug_assertions)]
referenced_by: ResolvedCommandReference {
capability: capability.identifier.clone(),
permission: referenced_by_permission_identifier.clone(),
},
windows: parse_glob_patterns(capability.windows.clone())?,
webviews: parse_glob_patterns(capability.webviews.clone())?,
scope_id,
});
}
Ok(())
}
struct ResolvedPermission<'a> {
key: &'a str,
permission_name: &'a str,
commands: Commands,
scope: Scopes,
}
/// Iterate over permissions in a capability, resolving permission sets if necessary
/// to produce a [`ResolvedPermission`] and calling the provided callback with it.
fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>) -> Result<(), Error>>(
capability: &Capability,
acl: &BTreeMap<String, Manifest>,
target: Target,
mut f: F,
) -> Result<(), Error> {
for permission_entry in &capability.permissions {
let permission_id = permission_entry.identifier();
let permissions = get_permissions(permission_id, acl)?
.into_iter()
.filter(|p| p.permission.is_active(&target));
for TraversedPermission {
key,
permission_name,
permission,
} in permissions
{
let mut resolved_scope = Scopes::default();
let mut commands = Commands::default();
if let PermissionEntry::ExtendedPermission {
identifier: _,
scope,
} = permission_entry
{
if let Some(allow) = scope.allow.clone() {
resolved_scope
.allow
.get_or_insert_with(Default::default)
.extend(allow);
}
if let Some(deny) = scope.deny.clone() {
resolved_scope
.deny
.get_or_insert_with(Default::default)
.extend(deny);
}
}
if let Some(allow) = permission.scope.allow.clone() {
resolved_scope
.allow
.get_or_insert_with(Default::default)
.extend(allow);
}
if let Some(deny) = permission.scope.deny.clone() {
resolved_scope
.deny
.get_or_insert_with(Default::default)
.extend(deny);
}
commands.allow.extend(permission.commands.allow.clone());
commands.deny.extend(permission.commands.deny.clone());
f(ResolvedPermission {
key: &key,
permission_name: &permission_name,
commands,
scope: resolved_scope,
})?;
}
}
Ok(())
}
/// Traversed permission
#[derive(Debug)]
pub struct TraversedPermission<'a> {
/// Plugin name without the tauri-plugin- prefix
pub key: String,
/// Permission's name
pub permission_name: String,
/// Permission details
pub permission: &'a Permission,
}
/// Expand a permissions id based on the ACL to get the associated permissions (e.g. expand some-plugin:default)
pub fn get_permissions<'a>(
permission_id: &Identifier,
acl: &'a BTreeMap<String, Manifest>,
) -> Result<Vec<TraversedPermission<'a>>, Error> {
let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
let permission_name = permission_id.get_base();
let manifest = acl.get(key).ok_or_else(|| Error::UnknownManifest {
key: display_perm_key(key).to_string(),
available: acl.keys().cloned().collect::<Vec<_>>().join(", "),
})?;
if permission_name == "default" {
manifest
.default_permission
.as_ref()
.map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
.unwrap_or_else(|| Ok(Default::default()))
} else if let Some(set) = manifest.permission_sets.get(permission_name) {
get_permission_set_permissions(permission_id, acl, manifest, set)
} else if let Some(permission) = manifest.permissions.get(permission_name) {
Ok(vec![TraversedPermission {
key: key.to_string(),
permission_name: permission_name.to_string(),
permission,
}])
} else {
Err(Error::UnknownPermission {
key: display_perm_key(key).to_string(),
permission: permission_name.to_string(),
})
}
}
// get the permissions from a permission set
fn get_permission_set_permissions<'a>(
permission_id: &Identifier,
acl: &'a BTreeMap<String, Manifest>,
manifest: &'a Manifest,
set: &'a PermissionSet,
) -> Result<Vec<TraversedPermission<'a>>, Error> {
let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
let mut permissions = Vec::new();
for perm in &set.permissions {
// a set could include permissions from other plugins
// for example `dialog:default`, could include `fs:default`
// in this case `perm = "fs:default"` which is not a permission
// in the dialog manifest so we check if `perm` still have a prefix (i.e `fs:`)
// and if so, we resolve this prefix from `acl` first before proceeding
let id = Identifier::try_from(perm.clone()).expect("invalid identifier in permission set?");
let (manifest, permission_id, key, permission_name) =
if let Some((new_key, manifest)) = id.get_prefix().and_then(|k| acl.get(k).map(|m| (k, m))) {
(manifest, &id, new_key, id.get_base())
} else {
(manifest, permission_id, key, perm.as_str())
};
if permission_name == "default" {
permissions.extend(
manifest
.default_permission
.as_ref()
.map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
.transpose()?
.unwrap_or_default(),
);
} else if let Some(permission) = manifest.permissions.get(permission_name) {
permissions.push(TraversedPermission {
key: key.to_string(),
permission_name: permission_name.to_string(),
permission,
});
} else if let Some(permission_set) = manifest.permission_sets.get(permission_name) {
permissions.extend(get_permission_set_permissions(
permission_id,
acl,
manifest,
permission_set,
)?);
} else {
return Err(Error::SetPermissionNotFound {
permission: permission_name.to_string(),
set: set.identifier.clone(),
});
}
}
Ok(permissions)
}
#[inline]
fn display_perm_key(prefix: &str) -> &str {
if prefix == APP_ACL_KEY {
"app manifest"
} else {
prefix
}
}
#[cfg(any(feature = "build", feature = "build-2"))]
mod build {
use proc_macro2::TokenStream;
use quote::{ToTokens, TokenStreamExt, quote};
use std::convert::identity;
use super::*;
use crate::{literal_struct, tokens::*};
#[cfg(debug_assertions)]
impl ToTokens for ResolvedCommandReference {
fn to_tokens(&self, tokens: &mut TokenStream) {
let capability = str_lit(&self.capability);
let permission = str_lit(&self.permission);
literal_struct!(
tokens,
::tauri::utils::acl::resolved::ResolvedCommandReference,
capability,
permission
)
}
}
impl ToTokens for ResolvedCommand {
fn to_tokens(&self, tokens: &mut TokenStream) {
#[cfg(debug_assertions)]
let referenced_by = &self.referenced_by;
let context = &self.context;
let windows = vec_lit(&self.windows, |window| {
let w = window.as_str();
quote!(#w.parse().unwrap())
});
let webviews = vec_lit(&self.webviews, |window| {
let w = window.as_str();
quote!(#w.parse().unwrap())
});
let scope_id = opt_lit(self.scope_id.as_ref());
#[cfg(debug_assertions)]
{
literal_struct!(
tokens,
::tauri::utils::acl::resolved::ResolvedCommand,
context,
referenced_by,
windows,
webviews,
scope_id
)
}
#[cfg(not(debug_assertions))]
literal_struct!(
tokens,
::tauri::utils::acl::resolved::ResolvedCommand,
context,
windows,
webviews,
scope_id
)
}
}
impl ToTokens for ResolvedScope {
fn to_tokens(&self, tokens: &mut TokenStream) {
let allow = vec_lit(&self.allow, identity);
let deny = vec_lit(&self.deny, identity);
literal_struct!(
tokens,
::tauri::utils::acl::resolved::ResolvedScope,
allow,
deny
)
}
}
impl ToTokens for Resolved {
fn to_tokens(&self, tokens: &mut TokenStream) {
let has_app_acl = self.has_app_acl;
let allowed_commands = map_lit(
quote! { ::std::collections::BTreeMap },
&self.allowed_commands,
str_lit,
|v| vec_lit(v, identity),
);
let denied_commands = map_lit(
quote! { ::std::collections::BTreeMap },
&self.denied_commands,
str_lit,
|v| vec_lit(v, identity),
);
let command_scope = map_lit(
quote! { ::std::collections::BTreeMap },
&self.command_scope,
identity,
identity,
);
let global_scope = map_lit(
quote! { ::std::collections::BTreeMap },
&self.global_scope,
str_lit,
identity,
);
literal_struct!(
tokens,
::tauri::utils::acl::resolved::Resolved,
has_app_acl,
allowed_commands,
denied_commands,
command_scope,
global_scope
)
}
}
}
#[cfg(test)]
mod tests {
use super::{Identifier, Manifest, Permission, PermissionSet, get_permissions};
fn manifest<const P: usize, const S: usize>(
name: &str,
permissions: [&str; P],
default_set: Option<&[&str]>,
sets: [(&str, &[&str]); S],
) -> (String, Manifest) {
(
name.to_string(),
Manifest {
default_permission: default_set.map(|perms| PermissionSet {
identifier: "default".to_string(),
description: "default set".to_string(),
permissions: perms.iter().map(|s| s.to_string()).collect(),
}),
permissions: permissions
.iter()
.map(|p| {
(
p.to_string(),
Permission {
identifier: p.to_string(),
..Default::default()
},
)
})
.collect(),
permission_sets: sets
.iter()
.map(|(s, perms)| {
(
s.to_string(),
PermissionSet {
identifier: s.to_string(),
description: format!("{s} set"),
permissions: perms.iter().map(|s| s.to_string()).collect(),
},
)
})
.collect(),
..Default::default()
},
)
}
fn id(id: &str) -> Identifier {
Identifier::try_from(id.to_string()).unwrap()
}
#[test]
fn resolves_permissions_from_other_plugins() {
let acl = [
manifest(
"fs",
["read", "write", "rm", "exist"],
Some(&["read", "exist"]),
[],
),
manifest(
"http",
["fetch", "fetch-cancel"],
None,
[("fetch-with-cancel", &["fetch", "fetch-cancel"])],
),
manifest(
"dialog",
["open", "save"],
None,
[(
"extra",
&[
"save",
"fs:default",
"fs:write",
"http:default",
"http:fetch-with-cancel",
],
)],
),
]
.into();
let permissions = get_permissions(&id("fs:default"), &acl).unwrap();
assert_eq!(permissions.len(), 2);
assert_eq!(permissions[0].key, "fs");
assert_eq!(permissions[0].permission_name, "read");
assert_eq!(permissions[1].key, "fs");
assert_eq!(permissions[1].permission_name, "exist");
let permissions = get_permissions(&id("fs:rm"), &acl).unwrap();
assert_eq!(permissions.len(), 1);
assert_eq!(permissions[0].key, "fs");
assert_eq!(permissions[0].permission_name, "rm");
let permissions = get_permissions(&id("http:fetch-with-cancel"), &acl).unwrap();
assert_eq!(permissions.len(), 2);
assert_eq!(permissions[0].key, "http");
assert_eq!(permissions[0].permission_name, "fetch");
assert_eq!(permissions[1].key, "http");
assert_eq!(permissions[1].permission_name, "fetch-cancel");
let permissions = get_permissions(&id("dialog:extra"), &acl).unwrap();
assert_eq!(permissions.len(), 6);
assert_eq!(permissions[0].key, "dialog");
assert_eq!(permissions[0].permission_name, "save");
assert_eq!(permissions[1].key, "fs");
assert_eq!(permissions[1].permission_name, "read");
assert_eq!(permissions[2].key, "fs");
assert_eq!(permissions[2].permission_name, "exist");
assert_eq!(permissions[3].key, "fs");
assert_eq!(permissions[3].permission_name, "write");
assert_eq!(permissions[4].key, "http");
assert_eq!(permissions[4].permission_name, "fetch");
assert_eq!(permissions[5].key, "http");
assert_eq!(permissions[5].permission_name, "fetch-cancel");
}
}

View file

@ -0,0 +1,445 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Schema generation for ACL items.
use std::{
collections::{BTreeMap, btree_map::Values},
fs,
path::{Path, PathBuf},
slice::Iter,
};
use schemars::Schema;
use super::{Error, PERMISSION_SCHEMAS_FOLDER_NAME};
use crate::{platform::Target, write_if_changed};
use super::{
PERMISSION_SCHEMA_FILE_NAME, Permission, PermissionSet,
capability::CapabilityFile,
manifest::{Manifest, PermissionFile},
};
/// Capability schema file name.
pub const CAPABILITIES_SCHEMA_FILE_NAME: &str = "schema.json";
/// Path of the folder where schemas are saved.
pub const CAPABILITIES_SCHEMA_FOLDER_PATH: &str = "gen/schemas";
// TODO: once MSRV is high enough, remove generic and use impl <trait>
// see https://github.com/tauri-apps/tauri/commit/b5561d74aee431f93c0c5b0fa6784fc0a956effe#diff-7c31d393f83cae149122e74ad44ac98e7d70ffb45c9e5b0a94ec52881b6f1cebR30-R42
/// Permission schema generator trait
pub trait PermissionSchemaGenerator<
'a,
Ps: Iterator<Item = &'a PermissionSet>,
P: Iterator<Item = &'a Permission>,
>
{
/// Whether has a default permission set or not.
fn has_default_permission_set(&self) -> bool;
/// Default permission set description if any.
fn default_set_description(&self) -> Option<&str>;
/// Default permission set's permissions if any.
fn default_set_permissions(&self) -> Option<&Vec<String>>;
/// Permissions sets to generate schema for.
fn permission_sets(&'a self) -> Ps;
/// Permissions to generate schema for.
fn permissions(&'a self) -> P;
/// A utility function to generate a schema for a permission identifier
fn perm_id_schema(name: Option<&str>, id: &str, description: Option<&str>) -> Schema {
let command_name = match name {
Some(name) if name == super::APP_ACL_KEY => id.to_string(),
Some(name) => format!("{name}:{id}"),
_ => id.to_string(),
};
let mut schema = schemars::json_schema!({
"type": "string",
"const": command_name
});
if let Some(description) = description {
schema.insert(
"description".to_string(),
serde_json::Value::String(description.to_string()),
);
// Non-standard, used by vscode for rich hover tooltips
schema.insert(
"markdownDescription".to_string(),
serde_json::Value::String(description.to_string()),
);
}
schema
}
/// Generate schemas for all possible permissions.
fn gen_possible_permission_schemas(&'a self, name: Option<&str>) -> Vec<Schema> {
let mut permission_schemas = Vec::new();
// schema for default set
if self.has_default_permission_set() {
let description = self.default_set_description().unwrap_or_default();
let description = if let Some(permissions) = self.default_set_permissions() {
add_permissions_to_description(description, permissions, true)
} else {
description.to_string()
};
if !description.is_empty() {
let default = Self::perm_id_schema(name, "default", Some(&description));
permission_schemas.push(default);
}
}
// schema for each permission set
for set in self.permission_sets() {
let description = add_permissions_to_description(&set.description, &set.permissions, false);
let schema = Self::perm_id_schema(name, &set.identifier, Some(&description));
permission_schemas.push(schema);
}
// schema for each permission
for perm in self.permissions() {
let schema = Self::perm_id_schema(name, &perm.identifier, perm.description.as_deref());
permission_schemas.push(schema);
}
permission_schemas
}
}
fn add_permissions_to_description(
description: &str,
permissions: &[String],
is_default: bool,
) -> String {
if permissions.is_empty() {
return description.to_string();
}
let permissions_list = permissions
.iter()
.map(|permission| format!("- `{permission}`"))
.collect::<Vec<_>>()
.join("\n");
let default_permission_set = if is_default {
"default permission set"
} else {
"permission set"
};
format!("{description}\n#### This {default_permission_set} includes:\n\n{permissions_list}")
}
impl<'a>
PermissionSchemaGenerator<
'a,
Values<'a, std::string::String, PermissionSet>,
Values<'a, std::string::String, Permission>,
> for Manifest
{
fn has_default_permission_set(&self) -> bool {
self.default_permission.is_some()
}
fn default_set_description(&self) -> Option<&str> {
self
.default_permission
.as_ref()
.map(|d| d.description.as_str())
}
fn default_set_permissions(&self) -> Option<&Vec<String>> {
self.default_permission.as_ref().map(|d| &d.permissions)
}
fn permission_sets(&'a self) -> Values<'a, std::string::String, PermissionSet> {
self.permission_sets.values()
}
fn permissions(&'a self) -> Values<'a, std::string::String, Permission> {
self.permissions.values()
}
}
impl<'a> PermissionSchemaGenerator<'a, Iter<'a, PermissionSet>, Iter<'a, Permission>>
for PermissionFile
{
fn has_default_permission_set(&self) -> bool {
self.default.is_some()
}
fn default_set_description(&self) -> Option<&str> {
self.default.as_ref().and_then(|d| d.description.as_deref())
}
fn default_set_permissions(&self) -> Option<&Vec<String>> {
self.default.as_ref().map(|d| &d.permissions)
}
fn permission_sets(&'a self) -> Iter<'a, PermissionSet> {
self.set.iter()
}
fn permissions(&'a self) -> Iter<'a, Permission> {
self.permission.iter()
}
}
/// Collect and include all possible identifiers in `Identifier` definition in the schema
fn extend_identifier_schema(schema: &mut Schema, acl: &BTreeMap<String, Manifest>) {
let permission_schemas: Vec<serde_json::Value> = acl
.iter()
.flat_map(|(name, manifest)| manifest.gen_possible_permission_schemas(Some(name)))
.map(serde_json::Value::from)
.collect();
if let Some(identifier_schema) = schema
.pointer_mut("/$defs/Identifier")
.and_then(|v| v.as_object_mut())
{
identifier_schema.insert(
"oneOf".to_string(),
serde_json::Value::Array(permission_schemas),
);
identifier_schema.remove("properties");
identifier_schema.remove("type");
identifier_schema.insert(
"description".to_string(),
serde_json::Value::String("Permission identifier".to_string()),
);
}
}
/// Collect permission schemas and its associated scope schema and schema definitions from plugins
/// and replace `PermissionEntry` extend object syntax with a new schema that does conditional
/// checks to serve the relevant scope schema for the right permissions schema, in a nutshell, it
/// will look something like this:
/// ```text
/// PermissionEntry {
/// anyOf {
/// String, // default string syntax
/// Object { // extended object syntax
/// allOf { // JSON allOf is used but actually means anyOf
/// {
/// "if": "identifier" property anyOf "fs" plugin permission,
/// "then": add "allow" and "deny" properties that match "fs" plugin scope schema
/// },
/// {
/// "if": "identifier" property anyOf "http" plugin permission,
/// "then": add "allow" and "deny" properties that match "http" plugin scope schema
/// },
/// ...etc,
/// {
/// No "if" or "then", just "allow" and "deny" properties with default "#/defs/Value"
/// },
/// }
/// }
/// }
/// }
/// ```
fn extend_permission_entry_schema(root_schema: &mut Schema, acl: &BTreeMap<String, Manifest>) {
const IDENTIFIER: &str = "identifier";
const ALLOW: &str = "allow";
const DENY: &str = "deny";
let mut collected_defs: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
// Scope the mutable borrow of root_schema
{
let defs = match root_schema.get_mut("$defs").and_then(|v| v.as_object_mut()) {
Some(d) => d,
None => return,
};
let perm_entry = match defs
.get_mut("PermissionEntry")
.and_then(|v| v.as_object_mut())
{
Some(p) => p,
None => return,
};
let any_of = match perm_entry.get_mut("anyOf").and_then(|v| v.as_array_mut()) {
Some(a) => a,
None => return,
};
let extend_perm_entry = match any_of.last_mut().and_then(|v| v.as_object_mut()) {
Some(e) => e,
None => return,
};
// Remove default properties and save to be added later as a fallback
let default_properties = extend_perm_entry
.remove("properties")
.and_then(|v| match v {
serde_json::Value::Object(m) => Some(m),
_ => None,
})
.unwrap_or_default();
let default_identifier = default_properties.get(IDENTIFIER).cloned().unwrap();
let mut all_of: Vec<serde_json::Value> = vec![];
let schemas = acl.iter().filter_map(|(name, manifest)| {
manifest
.global_scope_schema()
.unwrap_or_else(|e| panic!("invalid JSON schema for plugin {name}: {e}"))
.map(|s| (s, manifest.gen_possible_permission_schemas(Some(name))))
});
for ((scope_schema, defs), acl_perm_schema) in schemas {
let perm_schema_values: Vec<serde_json::Value> = acl_perm_schema
.into_iter()
.map(serde_json::Value::from)
.collect();
let scope_value = serde_json::Value::from(scope_schema);
let obj = serde_json::json!({
"properties": {
IDENTIFIER: default_identifier.clone()
},
"if": {
"properties": {
IDENTIFIER: { "anyOf": perm_schema_values }
}
},
"then": {
"properties": {
ALLOW: scope_value.clone(),
DENY: scope_value,
}
}
});
all_of.push(obj);
collected_defs.extend(defs);
}
// Add back default properties as a fallback
all_of.push(serde_json::json!({
"properties": serde_json::Value::Object(default_properties)
}));
// Replace extended PermissionEntry with the new schema
extend_perm_entry.insert("allOf".to_string(), serde_json::Value::Array(all_of));
}
// Extend root schema with definitions collected from plugins
if !collected_defs.is_empty() {
let root_defs = root_schema
.ensure_object()
.entry("$defs")
.or_insert(serde_json::Value::Object(serde_json::Map::new()))
.as_object_mut()
.unwrap();
root_defs.extend(collected_defs);
}
}
/// Generate schema for CapabilityFile with all possible plugins permissions
pub fn generate_capability_schema(
acl: &BTreeMap<String, Manifest>,
target: Target,
) -> crate::Result<()> {
let mut schema = schemars::SchemaGenerator::new(schemars::generate::SchemaSettings::draft07())
.into_root_schema_for::<CapabilityFile>();
extend_identifier_schema(&mut schema, acl);
extend_permission_entry_schema(&mut schema, acl);
let schema_str = serde_json::to_string_pretty(&schema).unwrap();
let out_dir = PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH);
fs::create_dir_all(&out_dir)?;
let schema_path = out_dir.join(format!("{target}-{CAPABILITIES_SCHEMA_FILE_NAME}"));
if schema_str != fs::read_to_string(&schema_path).unwrap_or_default() {
fs::write(&schema_path, schema_str)?;
fs::copy(
schema_path,
out_dir.join(format!(
"{}-{CAPABILITIES_SCHEMA_FILE_NAME}",
if target.is_desktop() {
"desktop"
} else {
"mobile"
}
)),
)?;
}
Ok(())
}
/// Extend schema with collected permissions from the passed [`PermissionFile`]s.
fn extend_permission_file_schema(schema: &mut Schema, permissions: &[PermissionFile]) {
// Collect possible permissions
let permission_schemas: Vec<serde_json::Value> = permissions
.iter()
.flat_map(|p| p.gen_possible_permission_schemas(None))
.map(serde_json::Value::from)
.collect();
// Update the permissions property to reference PermissionKind
let updated = if let Some(permissions_obj) = schema
.pointer_mut("/$defs/PermissionSet/properties/permissions")
.and_then(|v| v.as_object_mut())
{
permissions_obj.insert(
"items".to_string(),
serde_json::json!({ "$ref": "#/$defs/PermissionKind" }),
);
true
} else {
false
};
// Add the new PermissionKind definition
if updated {
let defs = schema
.ensure_object()
.entry("$defs")
.or_insert(serde_json::Value::Object(serde_json::Map::new()))
.as_object_mut()
.unwrap();
defs.insert(
"PermissionKind".into(),
serde_json::json!({
"type": "string",
"oneOf": permission_schemas,
}),
);
}
}
/// Generate and write a schema based on the format of a [`PermissionFile`].
pub fn generate_permissions_schema<P: AsRef<Path>>(
permissions: &[PermissionFile],
out_dir: P,
) -> Result<(), Error> {
let mut schema = schemars::SchemaGenerator::new(schemars::generate::SchemaSettings::draft07())
.into_root_schema_for::<PermissionFile>();
extend_permission_file_schema(&mut schema, permissions);
let schema_str = serde_json::to_string_pretty(&schema)?;
let out_dir = out_dir.as_ref().join(PERMISSION_SCHEMAS_FOLDER_NAME);
fs::create_dir_all(&out_dir).map_err(|e| Error::CreateDir(e, out_dir.clone()))?;
let schema_path = out_dir.join(PERMISSION_SCHEMA_FILE_NAME);
write_if_changed(&schema_path, schema_str).map_err(|e| Error::WriteFile(e, schema_path))?;
Ok(())
}

View file

@ -0,0 +1,201 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! A [`Value`] that is used instead of [`toml::Value`] or [`serde_json::Value`]
//! to support both formats.
use std::collections::BTreeMap;
use std::fmt::Debug;
use serde::{Deserialize, Serialize};
/// A valid ACL number.
#[derive(Debug, PartialEq, Serialize, Deserialize, Copy, Clone, PartialOrd)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum Number {
/// Represents an [`i64`].
Int(i64),
/// Represents a [`f64`].
Float(f64),
}
impl From<i64> for Number {
#[inline(always)]
fn from(value: i64) -> Self {
Self::Int(value)
}
}
impl From<f64> for Number {
#[inline(always)]
fn from(value: f64) -> Self {
Self::Float(value)
}
}
/// All supported ACL values.
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, PartialOrd)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum Value {
/// Represents a null JSON value.
Null,
/// Represents a [`bool`].
Bool(bool),
/// Represents a valid ACL [`Number`].
Number(Number),
/// Represents a [`String`].
String(String),
/// Represents a list of other [`Value`]s.
List(Vec<Value>),
/// Represents a map of [`String`] keys to [`Value`]s.
Map(BTreeMap<String, Value>),
}
impl From<Value> for serde_json::Value {
fn from(value: Value) -> Self {
match value {
Value::Null => serde_json::Value::Null,
Value::Bool(b) => serde_json::Value::Bool(b),
Value::Number(Number::Float(f)) => {
serde_json::Value::Number(serde_json::Number::from_f64(f).unwrap())
}
Value::Number(Number::Int(i)) => serde_json::Value::Number(i.into()),
Value::String(s) => serde_json::Value::String(s),
Value::List(list) => serde_json::Value::Array(list.into_iter().map(Into::into).collect()),
Value::Map(map) => serde_json::Value::Object(
map
.into_iter()
.map(|(key, value)| (key, value.into()))
.collect(),
),
}
}
}
impl From<serde_json::Value> for Value {
fn from(value: serde_json::Value) -> Self {
match value {
serde_json::Value::Null => Value::Null,
serde_json::Value::Bool(b) => Value::Bool(b),
serde_json::Value::Number(n) => Value::Number(if let Some(f) = n.as_f64() {
Number::Float(f)
} else if let Some(n) = n.as_u64() {
Number::Int(n as i64)
} else if let Some(n) = n.as_i64() {
Number::Int(n)
} else {
Number::Int(0)
}),
serde_json::Value::String(s) => Value::String(s),
serde_json::Value::Array(list) => Value::List(list.into_iter().map(Into::into).collect()),
serde_json::Value::Object(map) => Value::Map(
map
.into_iter()
.map(|(key, value)| (key, value.into()))
.collect(),
),
}
}
}
impl From<bool> for Value {
#[inline(always)]
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
impl<T: Into<Number>> From<T> for Value {
#[inline(always)]
fn from(value: T) -> Self {
Self::Number(value.into())
}
}
impl From<String> for Value {
#[inline(always)]
fn from(value: String) -> Self {
Value::String(value)
}
}
impl From<toml::Value> for Value {
#[inline(always)]
fn from(value: toml::Value) -> Self {
use toml::Value as Toml;
match value {
Toml::String(s) => s.into(),
Toml::Integer(i) => i.into(),
Toml::Float(f) => f.into(),
Toml::Boolean(b) => b.into(),
Toml::Datetime(d) => d.to_string().into(),
Toml::Array(a) => Value::List(a.into_iter().map(Value::from).collect()),
Toml::Table(t) => Value::Map(t.into_iter().map(|(k, v)| (k, v.into())).collect()),
}
}
}
#[cfg(any(feature = "build", feature = "build-2"))]
mod build {
use std::convert::identity;
use crate::tokens::*;
use super::*;
use proc_macro2::TokenStream;
use quote::{ToTokens, TokenStreamExt, quote};
impl ToTokens for Number {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::acl::Number };
tokens.append_all(match self {
Self::Int(i) => {
quote! { #prefix::Int(#i) }
}
Self::Float(f) => {
quote! { #prefix::Float (#f) }
}
});
}
}
impl ToTokens for Value {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::acl::Value };
tokens.append_all(match self {
Value::Null => quote! { #prefix::Null },
Value::Bool(bool) => quote! { #prefix::Bool(#bool) },
Value::Number(number) => quote! { #prefix::Number(#number) },
Value::String(str) => {
let s = str_lit(str);
quote! { #prefix::String(#s) }
}
Value::List(vec) => {
let items = vec_lit(vec, identity);
quote! { #prefix::List(#items) }
}
Value::Map(map) => {
let map = map_lit(
quote! { ::std::collections::BTreeMap },
map,
str_lit,
identity,
);
quote! { #prefix::Map(#map) }
}
});
}
}
}

View file

@ -0,0 +1,212 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! The Assets module allows you to read files that have been bundled by tauri
//! during both compile time and runtime.
#[doc(hidden)]
pub use phf;
use std::{
borrow::Cow,
path::{Component, Path},
};
/// The token used for script nonces.
pub const SCRIPT_NONCE_TOKEN: &str = "__TAURI_SCRIPT_NONCE__";
/// The token used for style nonces.
pub const STYLE_NONCE_TOKEN: &str = "__TAURI_STYLE_NONCE__";
/// Assets iterator.
pub type AssetsIter<'a> = dyn Iterator<Item = (Cow<'a, str>, Cow<'a, [u8]>)> + 'a;
/// Represent an asset file path in a normalized way.
///
/// The following rules are enforced and added if needed:
/// * Unix path component separators
/// * Has a root directory
/// * No trailing slash - directories are not included in assets
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct AssetKey(String);
impl From<AssetKey> for String {
fn from(key: AssetKey) -> Self {
key.0
}
}
impl AsRef<str> for AssetKey {
fn as_ref(&self) -> &str {
&self.0
}
}
impl<P: AsRef<Path>> From<P> for AssetKey {
fn from(path: P) -> Self {
// TODO: change this to utilize `Cow` to prevent allocating an intermediate `PathBuf` when not necessary
let path = path.as_ref().to_owned();
// add in root to mimic how it is used from a server url
let path = if path.has_root() {
path
} else {
Path::new(&Component::RootDir).join(path)
};
let buf = if cfg!(windows) {
let mut buf = String::new();
for component in path.components() {
match component {
Component::RootDir => buf.push('/'),
Component::CurDir => buf.push_str("./"),
Component::ParentDir => buf.push_str("../"),
Component::Prefix(prefix) => buf.push_str(&prefix.as_os_str().to_string_lossy()),
Component::Normal(s) => {
buf.push_str(&s.to_string_lossy());
buf.push('/')
}
}
}
// remove the last slash
if buf != "/" {
buf.pop();
}
buf
} else {
path.to_string_lossy().to_string()
};
AssetKey(buf)
}
}
/// A Content-Security-Policy hash value for a specific directive.
/// For more information see [the MDN page](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives).
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub enum CspHash<'a> {
/// The `script-src` directive.
Script(&'a str),
/// The `style-src` directive.
Style(&'a str),
}
impl CspHash<'_> {
/// The Content-Security-Policy directive this hash applies to.
pub fn directive(&self) -> &'static str {
match self {
Self::Script(_) => "script-src",
Self::Style(_) => "style-src",
}
}
/// The value of the Content-Security-Policy hash.
pub fn hash(&self) -> &str {
match self {
Self::Script(hash) => hash,
Self::Style(hash) => hash,
}
}
}
/// [`Assets`] implementation that only contains compile-time compressed and embedded assets.
pub struct EmbeddedAssets {
assets: phf::Map<&'static str, &'static [u8]>,
// Hashes that must be injected to the CSP of every HTML file.
global_hashes: &'static [CspHash<'static>],
// Hashes that are associated to the CSP of the HTML file identified by the map key (the HTML asset key).
html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
}
/// Temporary struct that overrides the Debug formatting for the `assets` field.
///
/// It reduces the output size compared to the default, as that would format the binary
/// data as a slice of numbers like `[65, 66, 67]` for "ABC". This instead shows the length
/// of the slice.
///
/// For example: `{"/index.html": [u8; 1835], "/index.js": [u8; 212]}`
struct DebugAssetMap<'a>(&'a phf::Map<&'static str, &'static [u8]>);
impl std::fmt::Debug for DebugAssetMap<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut map = f.debug_map();
for (k, v) in self.0.entries() {
map.key(k);
map.value(&format_args!("[u8; {}]", v.len()));
}
map.finish()
}
}
impl std::fmt::Debug for EmbeddedAssets {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EmbeddedAssets")
.field("assets", &DebugAssetMap(&self.assets))
.field("global_hashes", &self.global_hashes)
.field("html_hashes", &self.html_hashes)
.finish()
}
}
impl EmbeddedAssets {
/// Creates a new instance from the given asset map and script hash list.
pub const fn new(
map: phf::Map<&'static str, &'static [u8]>,
global_hashes: &'static [CspHash<'static>],
html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
) -> Self {
Self {
assets: map,
global_hashes,
html_hashes,
}
}
/// Get an asset by key.
#[cfg(feature = "compression")]
pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
let &(mut asdf) = self.assets.get(key.as_ref())?;
// with the exception of extremely small files, output should usually be
// at least as large as the compressed version.
let mut buf = Vec::with_capacity(asdf.len());
brotli::BrotliDecompress(&mut asdf, &mut buf).ok()?;
Some(Cow::Owned(buf))
}
/// Get an asset by key.
#[cfg(not(feature = "compression"))]
pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
Some(Cow::Borrowed(self.assets.get(key.as_ref())?))
}
/// Iterate on the assets.
pub fn iter(&self) -> Box<AssetsIter<'_>> {
Box::new(
self
.assets
.into_iter()
.map(|(k, b)| (Cow::Borrowed(*k), Cow::Borrowed(*b))),
)
}
/// CSP hashes for the given asset.
pub fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
Box::new(
self
.global_hashes
.iter()
.chain(
self
.html_hashes
.get(html_path.as_ref())
.copied()
.into_iter()
.flatten(),
)
.copied(),
)
}
}

View file

@ -0,0 +1,171 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Build script utilities.
/// Link a Swift library.
#[cfg(target_os = "macos")]
pub fn link_apple_library(name: &str, source: impl AsRef<std::path::Path>) {
if source.as_ref().join("Package.swift").exists() {
link_swift_library(name, source);
} else {
link_xcode_library(name, source);
}
}
/// Link a Swift library.
#[cfg(target_os = "macos")]
fn link_swift_library(name: &str, source: impl AsRef<std::path::Path>) {
let source = source.as_ref();
let sdk_root = std::env::var_os("SDKROOT");
unsafe {
std::env::remove_var("SDKROOT");
}
swift_rs::SwiftLinker::new(
&std::env::var("MACOSX_DEPLOYMENT_TARGET").unwrap_or_else(|_| "10.13".into()),
)
.with_ios(&std::env::var("IPHONEOS_DEPLOYMENT_TARGET").unwrap_or_else(|_| "13.0".into()))
.with_package(name, source)
.link();
if let Some(root) = sdk_root {
unsafe {
std::env::set_var("SDKROOT", root);
}
}
}
/// Link a Xcode library.
#[cfg(target_os = "macos")]
fn link_xcode_library(name: &str, source: impl AsRef<std::path::Path>) {
use std::{path::PathBuf, process::Command};
let source = source.as_ref();
let configuration = if std::env::var("DEBUG")
.map(|v| v == "true")
.unwrap_or_default()
{
"Debug"
} else {
"Release"
};
let (sdk, arch) = match std::env::var("TARGET").unwrap().as_str() {
"aarch64-apple-ios" => ("iphoneos", "arm64"),
"aarch64-apple-ios-sim" => ("iphonesimulator", "arm64"),
"x86_64-apple-ios" => ("iphonesimulator", "x86_64"),
_ => return,
};
let out_dir = std::env::var_os("OUT_DIR").map(PathBuf::from).unwrap();
let derived_data_path = out_dir.join(format!("derivedData-{name}"));
let status = Command::new("xcodebuild")
.arg("build")
.arg("-scheme")
.arg(name)
.arg("-configuration")
.arg(configuration)
.arg("-sdk")
.arg(sdk)
.arg("-arch")
.arg(arch)
.arg("-derivedDataPath")
.arg(&derived_data_path)
.arg("BUILD_LIBRARY_FOR_DISTRIBUTION=YES")
.arg("OTHER_SWIFT_FLAGS=-no-verify-emitted-module-interface")
.current_dir(source)
.env_clear()
.env("PATH", std::env::var_os("PATH").unwrap_or_default())
.status()
.unwrap();
assert!(status.success());
let lib_out_dir = derived_data_path
.join("Build")
.join("Products")
.join(format!("{configuration}-{sdk}"));
println!(
"cargo::rustc-link-search=framework={}",
lib_out_dir.display()
);
println!("cargo:rerun-if-changed={}", source.display());
println!("cargo:rustc-link-search=native={}", lib_out_dir.display());
println!("cargo:rustc-link-lib=static={name}");
}
/// Updates the Android manifest by inserting XML content into a specified parent tag.
///
/// The content is wrapped in auto-generated comments and will replace any existing
/// content with the same block identifier.
///
/// # Arguments
///
/// * `block_identifier` - A unique identifier for the block (used in comments)
/// * `parent` - The parent XML tag name (e.g., "activity", "application")
/// * `insert` - The XML content to insert
pub fn update_android_manifest(
block_identifier: &str,
parent: &str,
insert: String,
) -> anyhow::Result<()> {
use std::{
env::var_os,
fs::{read_to_string, write},
path::PathBuf,
};
if let Some(project_path) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
let manifest_path = project_path.join("app/src/main/AndroidManifest.xml");
if !manifest_path.exists() {
return Ok(());
}
let manifest = read_to_string(&manifest_path)?;
let rewritten = insert_into_xml(&manifest, block_identifier, parent, &insert);
if rewritten != manifest {
write(&manifest_path, rewritten)?;
}
}
Ok(())
}
fn xml_block_comment(id: &str) -> String {
format!("<!-- {id}. AUTO-GENERATED. DO NOT REMOVE. -->")
}
fn insert_into_xml(xml: &str, block_identifier: &str, parent_tag: &str, contents: &str) -> String {
let block_comment = xml_block_comment(block_identifier);
let mut rewritten = Vec::new();
let mut found_block = false;
let parent_closing_tag = format!("</{parent_tag}>");
for line in xml.split('\n') {
if line.contains(&block_comment) {
found_block = !found_block;
continue;
}
// found previous block which should be removed
if found_block {
continue;
}
if let Some(index) = line.find(&parent_closing_tag) {
let indentation = " ".repeat(index + 4);
rewritten.push(format!("{indentation}{block_comment}"));
for l in contents.split('\n') {
rewritten.push(format!("{indentation}{l}"));
}
rewritten.push(format!("{indentation}{block_comment}"));
}
rewritten.push(line.to_string());
}
rewritten.join("\n")
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,398 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::config::Config;
use crate::platform::Target;
use json_patch::merge;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use thiserror::Error;
/// All extensions that are possibly supported, but perhaps not enabled.
pub const EXTENSIONS_SUPPORTED: &[&str] = &["json", "json5", "toml"];
/// All configuration formats that are possibly supported, but perhaps not enabled.
pub const SUPPORTED_FORMATS: &[ConfigFormat] =
&[ConfigFormat::Json, ConfigFormat::Json5, ConfigFormat::Toml];
/// All configuration formats that are currently enabled.
pub const ENABLED_FORMATS: &[ConfigFormat] = &[
ConfigFormat::Json,
#[cfg(feature = "config-json5")]
ConfigFormat::Json5,
#[cfg(feature = "config-toml")]
ConfigFormat::Toml,
];
/// The available configuration formats.
#[derive(Debug, Copy, Clone)]
pub enum ConfigFormat {
/// The default JSON (tauri.conf.json) format.
Json,
/// The JSON5 (tauri.conf.json5) format.
Json5,
/// The TOML (Tauri.toml file) format.
Toml,
}
impl ConfigFormat {
/// Maps the config format to its file name.
pub fn into_file_name(self) -> &'static str {
match self {
Self::Json => "tauri.conf.json",
Self::Json5 => "tauri.conf.json5",
Self::Toml => "Tauri.toml",
}
}
fn into_platform_file_name(self, target: Target) -> &'static str {
match self {
Self::Json => match target {
Target::MacOS => "tauri.macos.conf.json",
Target::Windows => "tauri.windows.conf.json",
Target::Linux => "tauri.linux.conf.json",
Target::Android => "tauri.android.conf.json",
Target::Ios => "tauri.ios.conf.json",
},
Self::Json5 => match target {
Target::MacOS => "tauri.macos.conf.json5",
Target::Windows => "tauri.windows.conf.json5",
Target::Linux => "tauri.linux.conf.json5",
Target::Android => "tauri.android.conf.json5",
Target::Ios => "tauri.ios.conf.json5",
},
Self::Toml => match target {
Target::MacOS => "Tauri.macos.toml",
Target::Windows => "Tauri.windows.toml",
Target::Linux => "Tauri.linux.toml",
Target::Android => "Tauri.android.toml",
Target::Ios => "Tauri.ios.toml",
},
}
}
}
/// Represents all the errors that can happen while reading the config.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ConfigError {
/// Failed to parse from JSON.
#[error("unable to parse JSON Tauri config file at {path} because {error}")]
FormatJson {
/// The path that failed to parse into JSON.
path: PathBuf,
/// The parsing [`serde_json::Error`].
error: serde_json::Error,
},
/// Failed to parse from JSON5.
#[cfg(feature = "config-json5")]
#[error("unable to parse JSON5 Tauri config file at {path} because {error}")]
FormatJson5 {
/// The path that failed to parse into JSON5.
path: PathBuf,
/// The parsing [`json5::Error`].
error: ::json5::Error,
},
/// Failed to parse from TOML.
#[cfg(feature = "config-toml")]
#[error("unable to parse toml Tauri config file at {path} because {error}")]
FormatToml {
/// The path that failed to parse into TOML.
path: PathBuf,
/// The parsing [`toml::Error`].
error: Box<::toml::de::Error>,
},
/// Unknown config file name encountered.
#[error("unsupported format encountered {0}")]
UnsupportedFormat(String),
/// Known file extension encountered, but corresponding parser is not enabled (cargo features).
#[error("supported (but disabled) format encountered {extension} - try enabling `{feature}` ")]
DisabledFormat {
/// The extension encountered.
extension: String,
/// The cargo feature to enable it.
feature: String,
},
/// A generic IO error with context of what caused it.
#[error("unable to read Tauri config file at {path} because {error}")]
Io {
/// The path the IO error occurred on.
path: PathBuf,
/// The [`std::io::Error`].
error: std::io::Error,
},
}
/// Determines if the given folder has a configuration file.
pub fn folder_has_configuration_file(target: Target, folder: &Path) -> bool {
folder.join(ConfigFormat::Json.into_file_name()).exists()
|| folder.join(ConfigFormat::Json5.into_file_name()).exists()
|| folder.join(ConfigFormat::Toml.into_file_name()).exists()
// platform file names
|| folder.join(ConfigFormat::Json.into_platform_file_name(target)).exists()
|| folder.join(ConfigFormat::Json5.into_platform_file_name(target)).exists()
|| folder.join(ConfigFormat::Toml.into_platform_file_name(target)).exists()
}
/// Determines if the given file path represents a Tauri configuration file.
pub fn is_configuration_file(target: Target, path: &Path) -> bool {
path
.file_name()
.map(|file_name| {
file_name == OsStr::new(ConfigFormat::Json.into_file_name())
|| file_name == OsStr::new(ConfigFormat::Json5.into_file_name())
|| file_name == OsStr::new(ConfigFormat::Toml.into_file_name())
// platform file names
|| file_name == OsStr::new(ConfigFormat::Json.into_platform_file_name(target))
|| file_name == OsStr::new(ConfigFormat::Json5.into_platform_file_name(target))
|| file_name == OsStr::new(ConfigFormat::Toml.into_platform_file_name(target))
})
.unwrap_or_default()
}
/// Reads the configuration from the given root directory.
///
/// It first looks for a `tauri.conf.json[5]` or `Tauri.toml` file on the given directory. The file must exist.
/// Then it looks for a platform-specific configuration file:
/// - `tauri.macos.conf.json[5]` or `Tauri.macos.toml` on macOS
/// - `tauri.linux.conf.json[5]` or `Tauri.linux.toml` on Linux
/// - `tauri.windows.conf.json[5]` or `Tauri.windows.toml` on Windows
/// - `tauri.android.conf.json[5]` or `Tauri.android.toml` on Android
/// - `tauri.ios.conf.json[5]` or `Tauri.ios.toml` on iOS
/// Merging the configurations using [JSON Merge Patch (RFC 7396)].
///
/// Returns the raw configuration and used config paths.
///
/// [JSON Merge Patch (RFC 7396)]: https://datatracker.ietf.org/doc/html/rfc7396.
pub fn read_from(target: Target, root_dir: &Path) -> Result<(Value, Vec<PathBuf>), ConfigError> {
let (mut config, config_file_path) = parse_value(target, root_dir.join("tauri.conf.json"))?;
let mut config_paths = vec![config_file_path];
if let Some((platform_config, path)) = read_platform(target, root_dir)? {
config_paths.push(path);
merge(&mut config, &platform_config);
}
Ok((config, config_paths))
}
/// Reads the platform-specific configuration file from the given root directory if it exists.
///
/// Check [`read_from`] for more information.
pub fn read_platform(
target: Target,
root_dir: &Path,
) -> Result<Option<(Value, PathBuf)>, ConfigError> {
let platform_config_path = root_dir.join(ConfigFormat::Json.into_platform_file_name(target));
if does_supported_file_name_exist(target, &platform_config_path) {
let (platform_config, path): (Value, PathBuf) = parse_value(target, platform_config_path)?;
Ok(Some((platform_config, path)))
} else {
Ok(None)
}
}
/// Check if a supported config file exists at path.
///
/// The passed path is expected to be the path to the "default" configuration format, in this case
/// JSON with `.json`.
pub fn does_supported_file_name_exist(target: Target, path: impl Into<PathBuf>) -> bool {
let path = path.into();
let source_file_name = path.file_name().unwrap();
let lookup_platform_config = ENABLED_FORMATS
.iter()
.any(|format| source_file_name == format.into_platform_file_name(target));
ENABLED_FORMATS.iter().any(|format| {
path
.with_file_name(if lookup_platform_config {
format.into_platform_file_name(target)
} else {
format.into_file_name()
})
.exists()
})
}
/// Parse the config from path, including alternative formats.
///
/// Hierarchy:
/// 1. Check if `tauri.conf.json` exists
/// a. Parse it with `serde_json`
/// b. Parse it with `json5` if `serde_json` fails
/// c. Return original `serde_json` error if all above steps failed
/// 2. Check if `tauri.conf.json5` exists
/// a. Parse it with `json5`
/// b. Return error if all above steps failed
/// 3. Check if `Tauri.json` exists
/// a. Parse it with `toml`
/// b. Return error if all above steps failed
/// 4. Return error if all above steps failed
pub fn parse(target: Target, path: impl Into<PathBuf>) -> Result<(Config, PathBuf), ConfigError> {
do_parse(target, path.into())
}
/// See [`parse`] for specifics, returns a JSON [`Value`] instead of [`Config`].
pub fn parse_value(
target: Target,
path: impl Into<PathBuf>,
) -> Result<(Value, PathBuf), ConfigError> {
do_parse(target, path.into())
}
fn do_parse<D: DeserializeOwned>(
target: Target,
path: PathBuf,
) -> Result<(D, PathBuf), ConfigError> {
let file_name = path
.file_name()
.map(OsStr::to_string_lossy)
.unwrap_or_default();
let lookup_platform_config = ENABLED_FORMATS
.iter()
.any(|format| file_name == format.into_platform_file_name(target));
let json5 = path.with_file_name(if lookup_platform_config {
ConfigFormat::Json5.into_platform_file_name(target)
} else {
ConfigFormat::Json5.into_file_name()
});
let toml = path.with_file_name(if lookup_platform_config {
ConfigFormat::Toml.into_platform_file_name(target)
} else {
ConfigFormat::Toml.into_file_name()
});
let path_ext = path
.extension()
.map(OsStr::to_string_lossy)
.unwrap_or_default();
if path.exists() {
let raw = read_to_string(&path)?;
// to allow us to easily use the compile-time #[cfg], we always bind
#[allow(clippy::let_and_return)]
let json = do_parse_json(&raw, &path);
// we also want to support **valid** json5 in the .json extension if the feature is enabled.
// if the json5 is not valid the serde_json error for regular json will be returned.
// this could be a bit confusing, so we may want to encourage users using json5 to use the
// .json5 extension instead of .json
#[cfg(feature = "config-json5")]
let json = {
match do_parse_json5(&raw, &path) {
json5 @ Ok(_) => json5,
// assume any errors from json5 in a .json file is because it's not json5
Err(_) => json,
}
};
json.map(|j| (j, path))
} else if json5.exists() {
#[cfg(feature = "config-json5")]
{
let raw = read_to_string(&json5)?;
do_parse_json5(&raw, &json5).map(|config| (config, json5))
}
#[cfg(not(feature = "config-json5"))]
Err(ConfigError::DisabledFormat {
extension: ".json5".into(),
feature: "config-json5".into(),
})
} else if toml.exists() {
#[cfg(feature = "config-toml")]
{
let raw = read_to_string(&toml)?;
do_parse_toml(&raw, &toml).map(|config| (config, toml))
}
#[cfg(not(feature = "config-toml"))]
Err(ConfigError::DisabledFormat {
extension: ".toml".into(),
feature: "config-toml".into(),
})
} else if !EXTENSIONS_SUPPORTED.contains(&path_ext.as_ref()) {
Err(ConfigError::UnsupportedFormat(path_ext.to_string()))
} else {
Err(ConfigError::Io {
path,
error: std::io::ErrorKind::NotFound.into(),
})
}
}
/// "Low-level" helper to parse JSON into a [`Config`].
///
/// `raw` should be the contents of the file that is represented by `path`.
pub fn parse_json(raw: &str, path: &Path) -> Result<Config, ConfigError> {
do_parse_json(raw, path)
}
/// "Low-level" helper to parse JSON into a JSON [`Value`].
///
/// `raw` should be the contents of the file that is represented by `path`.
pub fn parse_json_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
do_parse_json(raw, path)
}
fn do_parse_json<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
serde_json::from_str(raw).map_err(|error| ConfigError::FormatJson {
path: path.into(),
error,
})
}
/// "Low-level" helper to parse JSON5 into a [`Config`].
///
/// `raw` should be the contents of the file that is represented by `path`. This function requires
/// the `config-json5` feature to be enabled.
#[cfg(feature = "config-json5")]
pub fn parse_json5(raw: &str, path: &Path) -> Result<Config, ConfigError> {
do_parse_json5(raw, path)
}
/// "Low-level" helper to parse JSON5 into a JSON [`Value`].
///
/// `raw` should be the contents of the file that is represented by `path`. This function requires
/// the `config-json5` feature to be enabled.
#[cfg(feature = "config-json5")]
pub fn parse_json5_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
do_parse_json5(raw, path)
}
#[cfg(feature = "config-json5")]
fn do_parse_json5<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
::json5::from_str(raw).map_err(|error| ConfigError::FormatJson5 {
path: path.into(),
error,
})
}
#[cfg(feature = "config-toml")]
fn do_parse_toml<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
::toml::from_str(raw).map_err(|error| ConfigError::FormatToml {
path: path.into(),
error: Box::new(error),
})
}
/// Helper function to wrap IO errors from [`std::fs::read_to_string`] into a [`ConfigError`].
fn read_to_string(path: &Path) -> Result<String, ConfigError> {
std::fs::read_to_string(path).map_err(|error| ConfigError::Io {
path: path.into(),
error,
})
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,255 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#![allow(clippy::result_large_err)]
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use thiserror::Error;
/// All extensions that are possibly supported, but perhaps not enabled.
const EXTENSIONS_SUPPORTED: &[&str] = &["json", "json5", "toml"];
/// All configuration formats that are currently enabled.
const ENABLED_FORMATS: &[ConfigFormat] = &[
ConfigFormat::Json,
#[cfg(feature = "config-json5")]
ConfigFormat::Json5,
#[cfg(feature = "config-toml")]
ConfigFormat::Toml,
];
/// The available configuration formats.
#[derive(Debug, Copy, Clone)]
enum ConfigFormat {
/// The default JSON (tauri.conf.json) format.
Json,
/// The JSON5 (tauri.conf.json5) format.
Json5,
/// The TOML (Tauri.toml file) format.
Toml,
}
impl ConfigFormat {
/// Maps the config format to its file name.
fn into_file_name(self) -> &'static str {
match self {
Self::Json => "tauri.conf.json",
Self::Json5 => "tauri.conf.json5",
Self::Toml => "Tauri.toml",
}
}
fn into_platform_file_name(self) -> &'static str {
match self {
Self::Json => {
if cfg!(target_os = "macos") {
"tauri.macos.conf.json"
} else if cfg!(windows) {
"tauri.windows.conf.json"
} else {
"tauri.linux.conf.json"
}
}
Self::Json5 => {
if cfg!(target_os = "macos") {
"tauri.macos.conf.json5"
} else if cfg!(windows) {
"tauri.windows.conf.json5"
} else {
"tauri.linux.conf.json5"
}
}
Self::Toml => {
if cfg!(target_os = "macos") {
"Tauri.macos.toml"
} else if cfg!(windows) {
"Tauri.windows.toml"
} else {
"Tauri.linux.toml"
}
}
}
}
}
/// Represents all the errors that can happen while reading the config.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ConfigError {
/// Failed to parse from JSON.
#[error("unable to parse JSON Tauri config file at {path} because {error}")]
FormatJson {
/// The path that failed to parse into JSON.
path: PathBuf,
/// The parsing [`serde_json::Error`].
error: serde_json::Error,
},
/// Failed to parse from JSON5.
#[cfg(feature = "config-json5")]
#[error("unable to parse JSON5 Tauri config file at {path} because {error}")]
FormatJson5 {
/// The path that failed to parse into JSON5.
path: PathBuf,
/// The parsing [`json5::Error`].
error: ::json5::Error,
},
/// Failed to parse from TOML.
#[cfg(feature = "config-toml")]
#[error("unable to parse toml Tauri config file at {path} because {error}")]
FormatToml {
/// The path that failed to parse into TOML.
path: PathBuf,
/// The parsing [`toml::Error`].
error: ::toml::de::Error,
},
/// Unknown config file name encountered.
#[error("unsupported format encountered {0}")]
UnsupportedFormat(String),
/// Known file extension encountered, but corresponding parser is not enabled (cargo features).
#[error("supported (but disabled) format encountered {extension} - try enabling `{feature}` ")]
DisabledFormat {
/// The extension encountered.
extension: String,
/// The cargo feature to enable it.
feature: String,
},
/// A generic IO error with context of what caused it.
#[error("unable to read Tauri config file at {path} because {error}")]
Io {
/// The path the IO error occurred on.
path: PathBuf,
/// The [`std::io::Error`].
error: std::io::Error,
},
}
/// See [`parse`] for specifics, returns a JSON [`Value`] instead of [`Config`].
pub fn parse_value(path: impl Into<PathBuf>) -> Result<(Value, PathBuf), ConfigError> {
do_parse(path.into())
}
fn do_parse<D: DeserializeOwned>(path: PathBuf) -> Result<(D, PathBuf), ConfigError> {
let file_name = path
.file_name()
.map(OsStr::to_string_lossy)
.unwrap_or_default();
let lookup_platform_config = ENABLED_FORMATS
.iter()
.any(|format| file_name == format.into_platform_file_name());
let json5 = path.with_file_name(if lookup_platform_config {
ConfigFormat::Json5.into_platform_file_name()
} else {
ConfigFormat::Json5.into_file_name()
});
let toml = path.with_file_name(if lookup_platform_config {
ConfigFormat::Toml.into_platform_file_name()
} else {
ConfigFormat::Toml.into_file_name()
});
let path_ext = path
.extension()
.map(OsStr::to_string_lossy)
.unwrap_or_default();
if path.exists() {
let raw = read_to_string(&path)?;
// to allow us to easily use the compile-time #[cfg], we always bind
#[allow(clippy::let_and_return)]
let json = do_parse_json(&raw, &path);
// we also want to support **valid** json5 in the .json extension if the feature is enabled.
// if the json5 is not valid the serde_json error for regular json will be returned.
// this could be a bit confusing, so we may want to encourage users using json5 to use the
// .json5 extension instead of .json
#[cfg(feature = "config-json5")]
let json = {
match do_parse_json5(&raw, &path) {
json5 @ Ok(_) => json5,
// assume any errors from json5 in a .json file is because it's not json5
Err(_) => json,
}
};
json.map(|j| (j, path))
} else if json5.exists() {
#[cfg(feature = "config-json5")]
{
let raw = read_to_string(&json5)?;
do_parse_json5(&raw, &path).map(|config| (config, json5))
}
#[cfg(not(feature = "config-json5"))]
Err(ConfigError::DisabledFormat {
extension: ".json5".into(),
feature: "config-json5".into(),
})
} else if toml.exists() {
#[cfg(feature = "config-toml")]
{
let raw = read_to_string(&toml)?;
do_parse_toml(&raw, &path).map(|config| (config, toml))
}
#[cfg(not(feature = "config-toml"))]
Err(ConfigError::DisabledFormat {
extension: ".toml".into(),
feature: "config-toml".into(),
})
} else if !EXTENSIONS_SUPPORTED.contains(&path_ext.as_ref()) {
Err(ConfigError::UnsupportedFormat(path_ext.to_string()))
} else {
Err(ConfigError::Io {
path,
error: std::io::ErrorKind::NotFound.into(),
})
}
}
fn do_parse_json<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
serde_json::from_str(raw).map_err(|error| ConfigError::FormatJson {
path: path.into(),
error,
})
}
#[cfg(feature = "config-json5")]
fn do_parse_json5<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
::json5::from_str(raw).map_err(|error| ConfigError::FormatJson5 {
path: path.into(),
error,
})
}
#[cfg(feature = "config-toml")]
fn do_parse_toml<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
::toml::from_str(raw).map_err(|error| ConfigError::FormatToml {
path: path.into(),
error,
})
}
/// Helper function to wrap IO errors from [`std::fs::read_to_string`] into a [`ConfigError`].
fn read_to_string(path: &Path) -> Result<String, ConfigError> {
std::fs::read_to_string(path).map_err(|error| ConfigError::Io {
path: path.into(),
error,
})
}

View file

@ -0,0 +1,442 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! The module to process HTML in Tauri.
use std::path::{Path, PathBuf};
use html5ever::{
LocalName,
interface::QualName,
namespace_url, ns,
serialize::{HtmlSerializer, SerializeOpts, Serializer, TraversalScope},
tendril::TendrilSink,
};
pub use kuchiki::NodeRef;
use kuchiki::{Attribute, ExpandedName, NodeData};
use serde::Serialize;
#[cfg(feature = "isolation")]
use serialize_to_javascript::DefaultTemplate;
#[cfg(feature = "isolation")]
use crate::pattern::isolation::IsolationJavascriptCodegen;
use crate::{
assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
config::{DisabledCspModificationKind, PatternKind},
};
// taken from <https://github.com/kuchiki-rs/kuchiki/blob/57ee6920d835315a498e748ba4b07a851ae5e498/src/serializer.rs#L12>
fn serialize_node_ref_internal<S: Serializer>(
node: &NodeRef,
serializer: &mut S,
traversal_scope: TraversalScope,
) -> crate::Result<()> {
match (traversal_scope, node.data()) {
(ref scope, NodeData::Element(element)) => {
if *scope == TraversalScope::IncludeNode {
let attrs = element.attributes.borrow();
// Unfortunately we need to allocate something to hold these &'a QualName
let attrs = attrs
.map
.iter()
.map(|(name, attr)| {
(
QualName::new(attr.prefix.clone(), name.ns.clone(), name.local.clone()),
&attr.value,
)
})
.collect::<Vec<_>>();
serializer.start_elem(
element.name.clone(),
attrs.iter().map(|&(ref name, value)| (name, &**value)),
)?
}
let children = match element.template_contents.as_ref() {
Some(template_root) => template_root.children(),
None => node.children(),
};
for child in children {
serialize_node_ref_internal(&child, serializer, TraversalScope::IncludeNode)?
}
if *scope == TraversalScope::IncludeNode {
serializer.end_elem(element.name.clone())?
}
Ok(())
}
(_, &NodeData::DocumentFragment) | (_, &NodeData::Document(_)) => {
for child in node.children() {
serialize_node_ref_internal(&child, serializer, TraversalScope::IncludeNode)?
}
Ok(())
}
(TraversalScope::ChildrenOnly(_), _) => Ok(()),
(TraversalScope::IncludeNode, NodeData::Doctype(doctype)) => {
serializer.write_doctype(&doctype.name).map_err(Into::into)
}
(TraversalScope::IncludeNode, NodeData::Text(text)) => {
serializer.write_text(&text.borrow()).map_err(Into::into)
}
(TraversalScope::IncludeNode, NodeData::Comment(text)) => {
serializer.write_comment(&text.borrow()).map_err(Into::into)
}
(TraversalScope::IncludeNode, NodeData::ProcessingInstruction(contents)) => {
let contents = contents.borrow();
serializer
.write_processing_instruction(&contents.0, &contents.1)
.map_err(Into::into)
}
}
}
/// Serializes the node to HTML.
pub fn serialize_node(node: &NodeRef) -> Vec<u8> {
let mut u8_vec = Vec::new();
let mut ser = HtmlSerializer::new(
&mut u8_vec,
SerializeOpts {
traversal_scope: TraversalScope::IncludeNode,
..Default::default()
},
);
serialize_node_ref_internal(node, &mut ser, TraversalScope::IncludeNode).unwrap();
u8_vec
}
/// Parses the given HTML string.
pub fn parse(html: String) -> NodeRef {
kuchiki::parse_html().one(html).document_node
}
fn with_head<F: FnOnce(&NodeRef)>(document: &NodeRef, f: F) {
if let Ok(ref node) = document.select_first("head") {
f(node.as_node())
} else {
let node = NodeRef::new_element(
QualName::new(None, ns!(html), LocalName::from("head")),
None,
);
f(&node);
document.prepend(node)
}
}
fn inject_nonce(document: &NodeRef, selector: &str, token: &str) {
if let Ok(elements) = document.select(selector) {
for target in elements {
let node = target.as_node();
let element = node.as_element().unwrap();
let mut attrs = element.attributes.borrow_mut();
// if the node already has the `nonce` attribute, skip it
if attrs.get("nonce").is_some() {
continue;
}
attrs.insert("nonce", token.into());
}
}
}
/// Inject nonce tokens to all scripts and styles.
pub fn inject_nonce_token(
document: &NodeRef,
dangerous_disable_asset_csp_modification: &DisabledCspModificationKind,
) {
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
inject_nonce(document, "script[src^='http']", SCRIPT_NONCE_TOKEN);
}
if dangerous_disable_asset_csp_modification.can_modify("style-src") {
inject_nonce(document, "style", STYLE_NONCE_TOKEN);
}
}
/// Injects a content security policy to the HTML.
pub fn inject_csp(document: &NodeRef, csp: &str) {
with_head(document, |head| {
head.append(create_csp_meta_tag(csp));
});
}
fn create_csp_meta_tag(csp: &str) -> NodeRef {
NodeRef::new_element(
QualName::new(None, ns!(html), LocalName::from("meta")),
vec![
(
ExpandedName::new(ns!(), LocalName::from("http-equiv")),
Attribute {
prefix: None,
value: "Content-Security-Policy".into(),
},
),
(
ExpandedName::new(ns!(), LocalName::from("content")),
Attribute {
prefix: None,
value: csp.into(),
},
),
],
)
}
/// The shape of the JavaScript Pattern config
#[derive(Debug, Serialize)]
#[serde(rename_all = "lowercase", tag = "pattern")]
pub enum PatternObject {
/// Brownfield pattern.
Brownfield,
/// Isolation pattern. Recommended for security purposes.
Isolation {
/// Which `IsolationSide` this `PatternObject` is getting injected into
side: IsolationSide,
},
}
impl From<&PatternKind> for PatternObject {
fn from(pattern_kind: &PatternKind) -> Self {
match pattern_kind {
PatternKind::Brownfield => Self::Brownfield,
PatternKind::Isolation { .. } => Self::Isolation {
side: IsolationSide::default(),
},
}
}
}
/// Where the JavaScript is injected to
#[derive(Debug, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum IsolationSide {
/// Original frame, the Brownfield application
#[default]
Original,
/// Secure frame, the isolation security application
Secure,
}
/// Injects the Isolation JavaScript to a codegen time document.
///
/// Note: This function is not considered part of the stable API.
#[cfg(feature = "isolation")]
pub fn inject_codegen_isolation_script(document: &NodeRef) {
with_head(document, |head| {
let script = NodeRef::new_element(
QualName::new(None, ns!(html), "script".into()),
vec![(
ExpandedName::new(ns!(), LocalName::from("nonce")),
Attribute {
prefix: None,
value: SCRIPT_NONCE_TOKEN.into(),
},
)],
);
script.append(NodeRef::new_text(
IsolationJavascriptCodegen {}
.render_default(&Default::default())
.expect("unable to render codegen isolation script template")
.into_string(),
));
head.prepend(script);
});
}
/// Temporary workaround for Windows not allowing requests
///
/// Note: this does not prevent path traversal due to the isolation application expectation that it
/// is secure.
pub fn inline_isolation(document: &NodeRef, dir: &Path) {
for script in document
.select("script[src]")
.expect("unable to parse document for scripts")
{
let src = {
let attributes = script.attributes.borrow();
attributes
.get(LocalName::from("src"))
.expect("script with src attribute has no src value")
.to_string()
};
let mut path = PathBuf::from(src);
if path.has_root() {
path = path
.strip_prefix("/")
.expect("Tauri \"Isolation\" Pattern only supports relative or absolute (`/`) paths.")
.into();
}
let file = std::fs::read_to_string(dir.join(path)).expect("unable to find isolation file");
script.as_node().append(NodeRef::new_text(file));
let mut attributes = script.attributes.borrow_mut();
attributes.remove(LocalName::from("src"));
}
}
// TODO: Verify this, this is not found in the HTML spec, see https://github.com/tauri-apps/tauri/pull/14265#discussion_r2415396842
/// Normalize line endings in script content to match what the browser uses for CSP hashing.
///
/// According to the HTML spec, browsers normalize:
/// - `\r\n` → `\n`
/// - `\r` → `\n`
pub fn normalize_script_for_csp(input: &[u8]) -> Vec<u8> {
let mut output = Vec::with_capacity(input.len());
let mut i = 0;
while i < input.len() {
match input[i] {
b'\r' => {
if i + 1 < input.len() && input[i + 1] == b'\n' {
// CRLF → LF
output.push(b'\n');
i += 2;
} else {
// Lone CR → LF
output.push(b'\n');
i += 1;
}
}
_ => {
output.push(input[i]);
i += 1;
}
}
}
output
}
#[cfg(test)]
mod tests {
use std::io::Write;
use super::*;
use crate::{
assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
config,
};
#[test]
fn csp() {
let htmls = vec![
"<html><head></head></html>".to_string(),
"<html></html>".to_string(),
];
for html in htmls {
let document = parse(html);
let csp = "csp-string";
inject_csp(&document, csp);
assert_eq!(
String::from_utf8(serialize_node(&document)).unwrap(),
format!(
r#"<html><head><meta http-equiv="Content-Security-Policy" content="{csp}"></head><body></body></html>"#,
)
);
}
}
#[test]
fn normalize_script_for_csp_test() {
let js = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\r// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\r\n\r\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\r\n return payload\r\n}\r\n";
let expected = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\n// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\n\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\n return payload\n}\n";
assert_eq!(normalize_script_for_csp(js.as_bytes()), expected.as_bytes())
}
#[test]
fn parse_and_serialize_roundtrips() {
let htmls = [
"<html><head><title>Test</title></head><body><h1>Hello</h1></body></html>",
"<!DOCTYPE html><html><head></head><body></body></html>",
];
for html in htmls {
let parsed = parse(html.to_string());
let serialized = serialize_node(&parsed);
let result = String::from_utf8(serialized).unwrap();
assert_eq!(result, html);
}
}
#[test]
fn inject_nonce_to_scripts() {
let html = r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#;
let document = parse(html.to_string());
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
assert_eq!(
String::from_utf8(serialize_node(&document)).unwrap(),
format!(
r#"<html><head><script src="http://example.com/script.js" nonce="{SCRIPT_NONCE_TOKEN}"></script></head><body></body></html>"#
)
);
}
#[test]
fn inject_nonce_to_styles() {
let html = r#"<html><head><style>body { color: red; }</style></head><body></body></html>"#;
let document = parse(html.to_string());
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
assert_eq!(
String::from_utf8(serialize_node(&document)).unwrap(),
format!(
r#"<html><head><style nonce="{STYLE_NONCE_TOKEN}">body {{ color: red; }}</style></head><body></body></html>"#
)
);
}
#[test]
fn inject_nonce_skips_existing() {
let html = r#"<html><head><script src="http://example.com/script.js" nonce="existing"></script></head><body></body></html>"#;
let document = parse(html.to_string());
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
assert_eq!(String::from_utf8(serialize_node(&document)).unwrap(), html);
}
#[test]
fn inject_nonce_respects_disabled_modification() {
let html = r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#;
let document = parse(html.to_string());
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(true));
assert_eq!(
String::from_utf8(serialize_node(&document)).unwrap(),
r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#
);
}
#[test]
fn inline_isolation_replaces_src_with_content() {
let temp_dir = tempfile::tempdir().unwrap();
let mut file = tempfile::NamedTempFile::with_suffix_in(".js", &temp_dir).unwrap();
file.write_all(b"console.log('test');").unwrap();
let file_name = file.path().file_name().unwrap().to_str().unwrap();
let html =
format!(r#"<html><head><script src="/{file_name}"></script></head><body></body></html>"#);
let document = parse(html);
inline_isolation(&document, temp_dir.path());
assert_eq!(
String::from_utf8(serialize_node(&document)).unwrap(),
r#"<html><head><script>console.log('test');</script></head><body></body></html>"#
);
}
}

View file

@ -0,0 +1,335 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! The module to process HTML in Tauri.
//!
//! # Stability
//!
//! This is utility used in Tauri internally and not considered part of the stable API.
//! If you use it, note that it may include breaking changes in the future.
use dom_query::NodeRef;
use crate::{
assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
config::DisabledCspModificationKind,
};
/// # Stability
///
/// This dependency might receive updates in minor releases.
pub use dom_query::Document;
/// Serializes the document to HTML.
///
/// # Stability
///
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
pub fn serialize_doc(document: &Document) -> Vec<u8> {
document.html().as_bytes().to_vec()
}
/// Parses the given HTML string.
///
/// # Stability
///
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
pub fn parse_doc(html: String) -> Document {
Document::from(html)
}
fn ensure_head(document: &Document) -> NodeRef<'_> {
document.head().unwrap_or_else(|| {
let html = document.html_root();
let head = document.tree.new_element("head");
html.prepend_child(&head);
head
})
}
fn inject_nonce(document: &Document, selector: &str, token: &str) {
let elements = document.select(selector);
for elem in elements.nodes() {
// if the node already has the `nonce` attribute, skip it
if elem.attr("nonce").is_some() {
continue;
}
elem.set_attr("nonce", token);
}
}
/// Inject nonce tokens to all scripts and styles.
///
/// # Stability
///
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
pub fn inject_nonce_token(
document: &Document,
dangerous_disable_asset_csp_modification: &DisabledCspModificationKind,
) {
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
inject_nonce(document, "script[src^='http']", SCRIPT_NONCE_TOKEN);
}
if dangerous_disable_asset_csp_modification.can_modify("style-src") {
inject_nonce(document, "style", STYLE_NONCE_TOKEN);
}
}
/// Injects a content security policy to the HTML.
///
/// # Stability
///
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
pub fn inject_csp(document: &Document, csp: &str) {
let head = ensure_head(document);
let meta_tag = document.tree.new_element("meta");
meta_tag.set_attr("http-equiv", "Content-Security-Policy");
meta_tag.set_attr("content", csp);
head.append_child(&meta_tag);
}
/// Injects a content security policy to the HTML.
///
/// # Stability
///
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
pub fn append_script_to_head(document: &Document, script: &str) {
let head = ensure_head(document);
let script_tag = document.tree.new_element("script");
script_tag.set_text(script);
head.prepend_child(&script_tag);
}
/// Injects the Isolation JavaScript to a codegen time document.
///
/// Note: This function is not considered part of the stable API.
///
/// # Stability
///
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
#[cfg(feature = "isolation")]
pub fn inject_codegen_isolation_script(document: &Document) {
use crate::pattern::isolation::IsolationJavascriptCodegen;
use serialize_to_javascript::DefaultTemplate;
let head = ensure_head(document);
let script_content = IsolationJavascriptCodegen {}
.render_default(&Default::default())
.expect("unable to render codegen isolation script template")
.into_string();
let script_tag = document.tree.new_element("script");
script_tag.set_attr("nonce", SCRIPT_NONCE_TOKEN);
script_tag.set_text(script_content);
head.prepend_child(&script_tag);
}
/// Temporary workaround for Windows not allowing requests
///
/// Note: this does not prevent path traversal due to the isolation application expectation that it
/// is secure.
///
/// # Stability
///
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
#[cfg(feature = "isolation")]
pub fn inline_isolation(document: &Document, dir: &std::path::Path) {
let scripts = document.select("script[src]");
for script in scripts.nodes() {
let src = match script.attr("src") {
Some(s) => s.to_string(),
None => continue,
};
let mut path = std::path::PathBuf::from(src);
if path.has_root() {
path = path
.strip_prefix("/")
.expect("Tauri \"Isolation\" Pattern only supports relative or absolute (`/`) paths.")
.into();
}
let file = std::fs::read_to_string(dir.join(path)).expect("unable to find isolation file");
script.set_text(file);
script.remove_attr("src");
}
}
// TODO: Verify this, this is not found in the HTML spec, see https://github.com/tauri-apps/tauri/pull/14265#discussion_r2415396842
/// Normalize line endings in script content to match what the browser uses for CSP hashing.
///
/// According to the HTML spec, browsers normalize:
/// - `\r\n` → `\n`
/// - `\r` → `\n`
pub fn normalize_script_for_csp(input: &[u8]) -> Vec<u8> {
let mut output = Vec::with_capacity(input.len());
let mut i = 0;
while i < input.len() {
match input[i] {
b'\r' => {
if i + 1 < input.len() && input[i + 1] == b'\n' {
// CRLF → LF
output.push(b'\n');
i += 2;
} else {
// Lone CR → LF
output.push(b'\n');
i += 1;
}
}
_ => {
output.push(input[i]);
i += 1;
}
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
config,
};
#[test]
fn csp() {
let htmls = vec![
"<html><head></head></html>".to_string(),
"<html></html>".to_string(),
];
for html in htmls {
let document = parse_doc(html);
let csp = "csp-string";
inject_csp(&document, csp);
assert_eq!(
String::from_utf8(serialize_doc(&document)).unwrap(),
format!(
r#"<html><head><meta http-equiv="Content-Security-Policy" content="{csp}"></head><body></body></html>"#
)
);
}
}
#[test]
fn normalize_script_for_csp_test() {
let js = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\r// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\r\n\r\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\r\n return payload\r\n}\r\n";
let expected = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\n// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\n\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\n return payload\n}\n";
assert_eq!(normalize_script_for_csp(js.as_bytes()), expected.as_bytes())
}
#[test]
fn parse_and_serialize_roundtrips() {
let htmls = [
"<html><head><title>Test</title></head><body><h1>Hello</h1></body></html>",
"<!DOCTYPE html><html><head></head><body></body></html>",
];
for html in htmls {
let parsed = parse_doc(html.to_string());
let serialized = serialize_doc(&parsed);
let result = String::from_utf8(serialized).unwrap();
assert_eq!(result, html);
}
}
#[test]
fn inject_nonce_to_scripts() {
let html = r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#;
let document = parse_doc(html.to_string());
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
assert_eq!(
String::from_utf8(serialize_doc(&document)).unwrap(),
format!(
r#"<html><head><script src="http://example.com/script.js" nonce="{SCRIPT_NONCE_TOKEN}"></script></head><body></body></html>"#
)
);
}
#[test]
fn inject_nonce_to_styles() {
let html = r#"<html><head><style>body { color: red; }</style></head><body></body></html>"#;
let document = parse_doc(html.to_string());
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
assert_eq!(
String::from_utf8(serialize_doc(&document)).unwrap(),
format!(
r#"<html><head><style nonce="{STYLE_NONCE_TOKEN}">body {{ color: red; }}</style></head><body></body></html>"#
)
);
}
#[test]
fn append_script_to_head_test() {
let html = r#"<html><head></head><body></body></html>"#;
let document = parse_doc(html.to_string());
append_script_to_head(&document, r#"console.log('Test')"#);
assert_eq!(
String::from_utf8(serialize_doc(&document)).unwrap(),
format!(r#"<html><head><script>console.log('Test')</script></head><body></body></html>"#)
);
}
#[test]
fn inject_nonce_skips_existing() {
let html = r#"<html><head><script src="http://example.com/script.js" nonce="existing"></script></head><body></body></html>"#;
let document = parse_doc(html.to_string());
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
assert_eq!(String::from_utf8(serialize_doc(&document)).unwrap(), html);
}
#[test]
fn inject_nonce_respects_disabled_modification() {
let html = r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#;
let document = parse_doc(html.to_string());
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(true));
assert_eq!(
String::from_utf8(serialize_doc(&document)).unwrap(),
r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#
);
}
#[test]
#[cfg(feature = "isolation")]
fn inline_isolation_replaces_src_with_content() {
use std::io::Write;
let temp_dir = tempfile::tempdir().unwrap();
let mut file = tempfile::NamedTempFile::with_suffix_in(".js", &temp_dir).unwrap();
file.write_all(b"console.log('test');").unwrap();
let file_name = file.path().file_name().unwrap().to_str().unwrap();
let html =
format!(r#"<html><head><script src="/{file_name}"></script></head><body></body></html>"#);
let document = parse_doc(html);
inline_isolation(&document, temp_dir.path());
assert_eq!(
String::from_utf8(serialize_doc(&document)).unwrap(),
r#"<html><head><script>console.log('test');</script></head><body></body></html>"#
);
}
}

View file

@ -0,0 +1,47 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! IO helpers.
use std::io::BufRead;
/// Read all bytes until a newline (the `0xA` byte) or a carriage return (`\r`) is reached, and append them to the provided buffer.
///
/// Adapted from <https://doc.rust-lang.org/std/io/trait.BufRead.html#method.read_line>.
pub fn read_line<R: BufRead + ?Sized>(r: &mut R, buf: &mut Vec<u8>) -> std::io::Result<usize> {
let mut read = 0;
loop {
let (done, used) = {
let available = match r.fill_buf() {
Ok(n) => n,
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
};
match memchr::memchr(b'\n', available) {
Some(i) => {
let end = i + 1;
buf.extend_from_slice(&available[..end]);
(true, end)
}
None => match memchr::memchr(b'\r', available) {
Some(i) => {
let end = i + 1;
buf.extend_from_slice(&available[..end]);
(true, end)
}
None => {
buf.extend_from_slice(available);
(false, available.len())
}
},
}
};
r.consume(used);
read += used;
if done || used == 0 {
return Ok(read);
}
}
}

View file

@ -0,0 +1,404 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! This crate contains common code that is reused in many places and offers useful utilities like parsing configuration files, detecting platform triples, injecting the CSP, and managing assets.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png"
)]
#![warn(missing_docs, rust_2018_idioms)]
#![allow(clippy::deprecated_semver)]
use std::{
ffi::OsString,
fmt::Display,
path::{Path, PathBuf},
};
use semver::Version;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub mod acl;
pub mod assets;
pub mod config;
pub mod config_v1;
#[cfg(feature = "html-manipulation")]
pub mod html;
#[cfg(feature = "html-manipulation-2")]
pub mod html2;
pub mod io;
pub mod mime_type;
pub mod platform;
pub mod plugin;
/// Prepare application resources and sidecars.
#[cfg(feature = "resources")]
pub mod resources;
#[cfg(any(feature = "build", feature = "build-2"))]
pub mod tokens;
#[cfg(any(feature = "build", feature = "build-2"))]
pub mod build;
/// Application pattern.
pub mod pattern;
/// `tauri::App` package information.
#[derive(Debug, Clone)]
pub struct PackageInfo {
/// App name
pub name: String,
/// App version
pub version: Version,
/// The crate authors.
pub authors: &'static str,
/// The crate description.
pub description: &'static str,
/// The crate name.
pub crate_name: &'static str,
}
#[allow(deprecated)]
mod window_effects {
use super::*;
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
/// Platform-specific window effects
pub enum WindowEffect {
/// A default material appropriate for the view's effectiveAppearance. **macOS 10.14-**
#[deprecated(
since = "macOS 10.14",
note = "You should instead choose an appropriate semantic material."
)]
AppearanceBased,
/// **macOS 10.14-**
#[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")]
Light,
/// **macOS 10.14-**
#[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")]
Dark,
/// **macOS 10.14-**
#[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")]
MediumLight,
/// **macOS 10.14-**
#[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")]
UltraDark,
/// **macOS 10.10+**
Titlebar,
/// **macOS 10.10+**
Selection,
/// **macOS 10.11+**
Menu,
/// **macOS 10.11+**
Popover,
/// **macOS 10.11+**
Sidebar,
/// **macOS 10.14+**
HeaderView,
/// **macOS 10.14+**
Sheet,
/// **macOS 10.14+**
WindowBackground,
/// **macOS 10.14+**
HudWindow,
/// **macOS 10.14+**
FullScreenUI,
/// **macOS 10.14+**
Tooltip,
/// **macOS 10.14+**
ContentBackground,
/// **macOS 10.14+**
UnderWindowBackground,
/// **macOS 10.14+**
UnderPageBackground,
/// Mica effect that matches the system dark preference **Windows 11 Only**
Mica,
/// Mica effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only**
MicaDark,
/// Mica effect with light mode **Windows 11 Only**
MicaLight,
/// Tabbed effect that matches the system dark preference **Windows 11 Only**
Tabbed,
/// Tabbed effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only**
TabbedDark,
/// Tabbed effect with light mode **Windows 11 Only**
TabbedLight,
/// **Windows 7/10/11(22H1) Only**
///
/// ## Notes
///
/// This effect has bad performance when resizing/dragging the window on Windows 11 build 22621.
Blur,
/// **Windows 10/11 Only**
///
/// ## Notes
///
/// This effect has bad performance when resizing/dragging the window on Windows 10 v1903+ and Windows 11 build 22000.
Acrylic,
}
/// Window effect state **macOS only**
///
/// <https://developer.apple.com/documentation/appkit/nsvisualeffectview/state>
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum WindowEffectState {
/// Make window effect state follow the window's active state
FollowsWindowActiveState,
/// Make window effect state always active
Active,
/// Make window effect state always inactive
Inactive,
}
}
pub use window_effects::{WindowEffect, WindowEffectState};
/// How the window title bar should be displayed on macOS.
#[derive(Debug, Clone, PartialEq, Eq, Copy, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum TitleBarStyle {
/// A normal title bar.
#[default]
Visible,
/// Makes the title bar transparent, so the window background color is shown instead.
///
/// Useful if you don't need to have actual HTML under the title bar. This lets you avoid the caveats of using `TitleBarStyle::Overlay`. Will be more useful when Tauri lets you set a custom window background color.
Transparent,
/// Shows the title bar as a transparent overlay over the window's content.
///
/// Keep in mind:
/// - The height of the title bar is different on different OS versions, which can lead to window the controls and title not being where you don't expect.
/// - You need to define a custom drag region to make your window draggable, however due to a limitation you can't drag the window when it's not in focus <https://github.com/tauri-apps/tauri/issues/4316>.
/// - The color of the window title depends on the system theme.
Overlay,
}
impl Serialize for TitleBarStyle {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
impl<'de> Deserialize<'de> for TitleBarStyle {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(match s.to_lowercase().as_str() {
"transparent" => Self::Transparent,
"overlay" => Self::Overlay,
_ => Self::Visible,
})
}
}
impl Display for TitleBarStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Visible => "Visible",
Self::Transparent => "Transparent",
Self::Overlay => "Overlay",
}
)
}
}
/// System theme.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum Theme {
/// Light theme.
Light,
/// Dark theme.
Dark,
}
impl Serialize for Theme {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
impl<'de> Deserialize<'de> for Theme {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(match s.to_lowercase().as_str() {
"dark" => Self::Dark,
_ => Self::Light,
})
}
}
impl Display for Theme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Light => "light",
Self::Dark => "dark",
}
)
}
}
/// Information about environment variables.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Env {
/// The APPIMAGE environment variable.
#[cfg(target_os = "linux")]
pub appimage: Option<std::ffi::OsString>,
/// The APPDIR environment variable.
#[cfg(target_os = "linux")]
pub appdir: Option<std::ffi::OsString>,
/// The command line arguments of the current process.
pub args_os: Vec<OsString>,
}
#[allow(clippy::derivable_impls)]
impl Default for Env {
fn default() -> Self {
let args_os = std::env::args_os().collect();
#[cfg(target_os = "linux")]
{
let env = Self {
#[cfg(target_os = "linux")]
appimage: std::env::var_os("APPIMAGE"),
#[cfg(target_os = "linux")]
appdir: std::env::var_os("APPDIR"),
args_os,
};
if env.appimage.is_some() || env.appdir.is_some() {
// validate that we're actually running on an AppImage
// an AppImage is mounted to `/$TEMPDIR/.mount_${appPrefix}${hash}`
// see <https://github.com/AppImage/AppImageKit/blob/1681fd84dbe09c7d9b22e13cdb16ea601aa0ec47/src/runtime.c#L501>
// note that it is safe to use `std::env::current_exe` here since we just loaded an AppImage.
let is_temp = std::env::current_exe()
.map(|p| {
p.display()
.to_string()
.starts_with(&format!("{}/.mount_", std::env::temp_dir().display()))
})
.unwrap_or(true);
if !is_temp {
log::warn!(
"`APPDIR` or `APPIMAGE` environment variable found but this application was not detected as an AppImage; this might be a security issue."
);
}
}
env
}
#[cfg(not(target_os = "linux"))]
{
Self { args_os }
}
}
}
/// The result type of `tauri-utils`.
pub type Result<T> = std::result::Result<T, Error>;
/// The error type of `tauri-utils`.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// Target triple architecture error
#[error("Unable to determine target-architecture")]
Architecture,
/// Target triple OS error
#[error("Unable to determine target-os")]
Os,
/// Target triple environment error
#[error("Unable to determine target-environment")]
Environment,
/// Tried to get resource on an unsupported platform
#[error("Unsupported platform for reading resources")]
UnsupportedPlatform,
/// Get parent process error
#[error("Could not get parent process")]
ParentProcess,
/// Get parent process PID error
#[error("Could not get parent PID")]
ParentPid,
/// Get child process error
#[error("Could not get child process")]
ChildProcess,
/// IO error
#[error("{0}")]
Io(#[from] std::io::Error),
/// Invalid pattern.
#[error("invalid pattern `{0}`. Expected either `brownfield` or `isolation`.")]
InvalidPattern(String),
/// Invalid glob pattern.
#[cfg(feature = "resources")]
#[error("{0}")]
GlobPattern(#[from] glob::PatternError),
/// Failed to use glob pattern.
#[cfg(feature = "resources")]
#[error("`{0}`")]
Glob(#[from] glob::GlobError),
/// Glob pattern did not find any results.
#[cfg(feature = "resources")]
#[error("glob pattern {0} path not found or didn't match any files.")]
GlobPathNotFound(String),
/// Error walking directory.
#[cfg(feature = "resources")]
#[error("{0}")]
WalkdirError(#[from] walkdir::Error),
/// Not allowed to walk dir.
#[cfg(feature = "resources")]
#[error(
"could not walk directory `{0}`, try changing `allow_walk` to true on the `ResourcePaths` constructor."
)]
NotAllowedToWalkDir(std::path::PathBuf),
/// Resource path doesn't exist
#[cfg(feature = "resources")]
#[error("resource path `{0}` doesn't exist")]
ResourcePathNotFound(std::path::PathBuf),
}
/// Reconstructs a path from its components using the platform separator then converts it to String and removes UNC prefixes on Windows if it exists.
pub fn display_path<P: AsRef<Path>>(p: P) -> String {
dunce::simplified(&p.as_ref().components().collect::<PathBuf>())
.display()
.to_string()
}
/// Write the file only if the content of the existing file (if any) is different.
///
/// This will always write unless the file exists with identical content.
pub fn write_if_changed<P, C>(path: P, content: C) -> std::io::Result<()>
where
P: AsRef<Path>,
C: AsRef<[u8]>,
{
if let Ok(existing) = std::fs::read(&path)
&& existing == content.as_ref()
{
return Ok(());
}
std::fs::write(path, content)
}

View file

@ -0,0 +1,154 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Determine a mime type from a URI or file contents.
use std::fmt;
const MIMETYPE_PLAIN: &str = "text/plain";
/// [Web Compatible MimeTypes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#important_mime_types_for_web_developers)
#[allow(missing_docs)]
pub enum MimeType {
Css,
Csv,
Html,
Ico,
Js,
Json,
Jsonld,
Mp4,
OctetStream,
Rtf,
Svg,
Txt,
}
impl std::fmt::Display for MimeType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mime = match self {
MimeType::Css => "text/css",
MimeType::Csv => "text/csv",
MimeType::Html => "text/html",
MimeType::Ico => "image/vnd.microsoft.icon",
MimeType::Js => "text/javascript",
MimeType::Json => "application/json",
MimeType::Jsonld => "application/ld+json",
MimeType::Mp4 => "video/mp4",
MimeType::OctetStream => "application/octet-stream",
MimeType::Rtf => "application/rtf",
MimeType::Svg => "image/svg+xml",
MimeType::Txt => MIMETYPE_PLAIN,
};
write!(f, "{mime}")
}
}
impl MimeType {
/// parse a URI suffix to convert text/plain mimeType to their actual web compatible mimeType.
pub fn parse_from_uri(uri: &str) -> MimeType {
Self::parse_from_uri_with_fallback(uri, Self::Html)
}
/// parse a URI suffix to convert text/plain mimeType to their actual web compatible mimeType with specified fallback for unknown file extensions.
pub fn parse_from_uri_with_fallback(uri: &str, fallback: MimeType) -> MimeType {
let suffix = uri.split('.').next_back();
match suffix {
Some("bin") => Self::OctetStream,
Some("css" | "less" | "sass" | "styl") => Self::Css,
Some("csv") => Self::Csv,
Some("html") => Self::Html,
Some("ico") => Self::Ico,
Some("js") => Self::Js,
Some("json") => Self::Json,
Some("jsonld") => Self::Jsonld,
Some("mjs") => Self::Js,
Some("mp4") => Self::Mp4,
Some("rtf") => Self::Rtf,
Some("svg") => Self::Svg,
Some("txt") => Self::Txt,
// Assume HTML when a TLD is found for eg. `wry:://tauri.app` | `wry://hello.com`
Some(_) => fallback,
// using octet stream according to this:
// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types>
None => Self::OctetStream,
}
}
/// infer mimetype from content (or) URI if needed.
pub fn parse(content: &[u8], uri: &str) -> String {
Self::parse_with_fallback(content, uri, Self::Html)
}
/// infer mimetype from content (or) URI if needed with specified fallback for unknown file extensions.
pub fn parse_with_fallback(content: &[u8], uri: &str, fallback: MimeType) -> String {
let mime = if uri.ends_with(".svg") {
// when reading svg, we can't use `infer`
None
} else {
infer::get(content).map(|info| info.mime_type())
};
match mime {
Some(mime) if mime == MIMETYPE_PLAIN => {
Self::parse_from_uri_with_fallback(uri, fallback).to_string()
}
None => Self::parse_from_uri_with_fallback(uri, fallback).to_string(),
Some(mime) => mime.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_parse_mimetype_from_uri() {
let css = MimeType::parse_from_uri(
"https://unpkg.com/browse/bootstrap@4.1.0/dist/css/bootstrap-grid.css",
)
.to_string();
assert_eq!(css, "text/css".to_string());
let csv: String = MimeType::parse_from_uri("https://example.com/random.csv").to_string();
assert_eq!(csv, "text/csv".to_string());
let ico: String =
MimeType::parse_from_uri("https://icons.duckduckgo.com/ip3/microsoft.com.ico").to_string();
assert_eq!(ico, String::from("image/vnd.microsoft.icon"));
let html: String = MimeType::parse_from_uri("https://tauri.app/index.html").to_string();
assert_eq!(html, String::from("text/html"));
let js: String =
MimeType::parse_from_uri("https://unpkg.com/react@17.0.1/umd/react.production.min.js")
.to_string();
assert_eq!(js, "text/javascript".to_string());
let json: String =
MimeType::parse_from_uri("https://unpkg.com/browse/react@17.0.1/build-info.json").to_string();
assert_eq!(json, String::from("application/json"));
let jsonld: String = MimeType::parse_from_uri("https:/example.com/hello.jsonld").to_string();
assert_eq!(jsonld, String::from("application/ld+json"));
let mjs: String = MimeType::parse_from_uri("https://example.com/bundled.mjs").to_string();
assert_eq!(mjs, String::from("text/javascript"));
let mp4: String = MimeType::parse_from_uri("https://example.com/video.mp4").to_string();
assert_eq!(mp4, String::from("video/mp4"));
let rtf: String = MimeType::parse_from_uri("https://example.com/document.rtf").to_string();
assert_eq!(rtf, String::from("application/rtf"));
let svg: String = MimeType::parse_from_uri("https://example.com/picture.svg").to_string();
assert_eq!(svg, String::from("image/svg+xml"));
let txt: String = MimeType::parse_from_uri("https://example.com/file.txt").to_string();
assert_eq!(txt, String::from("text/plain"));
let custom_scheme = MimeType::parse_from_uri("wry://tauri.app").to_string();
assert_eq!(custom_scheme, String::from("text/html"));
}
}

View file

@ -0,0 +1,154 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
/**
* IMPORTANT: See ipc.js for the main frame implementation.
* main frame -> isolation frame = isolation payload
* isolation frame -> main frame = isolation message
*/
;(async function () {
/**
* Sends the message to the isolation frame.
* @param {any} message
*/
function sendMessage(message) {
window.parent.postMessage(message, '*')
}
/**
* @type {string} - The main frame origin.
*/
const origin = __TEMPLATE_origin__
/**
* @type {Uint8Array} - Injected by Tauri during runtime
*/
const aesGcmKeyRaw = new Uint8Array(__TEMPLATE_runtime_aes_gcm_key__)
/**
* @type {CryptoKey}
*/
const aesGcmKey = await window.crypto.subtle.importKey(
'raw',
aesGcmKeyRaw,
'AES-GCM',
false,
['encrypt']
)
/**
* @param {object} data
* @return {Promise<{nonce: number[], payload: number[]}>}
*/
async function encrypt(payload) {
const algorithm = Object.create(null)
algorithm.name = 'AES-GCM'
algorithm.iv = window.crypto.getRandomValues(new Uint8Array(12))
const { contentType, data } = __RAW_process_ipc_message_fn__(payload)
const message =
typeof data === 'string'
? new TextEncoder().encode(data)
: ArrayBuffer.isView(data) || data instanceof ArrayBuffer
? data
: new Uint8Array(data)
return window.crypto.subtle
.encrypt(algorithm, aesGcmKey, message)
.then((payload) => {
const result = Object.create(null)
result.nonce = Array.from(new Uint8Array(algorithm.iv))
result.payload = Array.from(new Uint8Array(payload))
result.contentType = contentType
return result
})
}
/**
* Detects if a message event is a valid isolation message.
*
* @param {MessageEvent<object>} event - a message event that is expected to be an isolation message
* @return {boolean} - if the event was a valid isolation message
*/
function isIsolationMessage(data) {
if (typeof data === 'object' && typeof data.payload === 'object') {
const keys = data.payload ? Object.keys(data.payload) : []
return (
keys.length > 0
&& keys.every(
(key) => key === 'nonce' || key === 'payload' || key === 'contentType'
)
)
}
return false
}
/**
* Detect if a message event is a valid isolation payload.
*
* @param {MessageEvent<object>} event - a message event that is expected to be an isolation payload
* @return boolean
*/
function isIsolationPayload(data) {
return (
typeof data === 'object'
&& 'callback' in data
&& 'error' in data
&& !isIsolationMessage(data)
)
}
/**
* Handle incoming payload events.
* @param {MessageEvent<any>} event
*/
async function payloadHandler(event) {
if (event.origin !== origin || !isIsolationPayload(event.data)) {
return
}
let data = event.data
if (typeof window.__TAURI_ISOLATION_HOOK__ === 'function') {
// await even if it's not async so that we can support async ones
data = await window.__TAURI_ISOLATION_HOOK__(data)
}
const message = Object.create(null)
message.cmd = data.cmd
message.callback = data.callback
message.error = data.error
message.options = data.options
message.payload = await encrypt(data.payload)
sendMessage(message)
}
window.addEventListener('message', payloadHandler, false)
/**
* @type {number} - How many milliseconds to wait between ready checks
*/
const readyIntervalMs = 50
/**
* Wait until this Isolation context is ready to receive messages, and let the main frame know.
*/
function waitUntilReady() {
// consider either a function or an explicitly set null value as the ready signal
if (
typeof window.__TAURI_ISOLATION_HOOK__ === 'function'
|| window.__TAURI_ISOLATION_HOOK__ === null
) {
sendMessage('__TAURI_ISOLATION_READY__')
} else {
setTimeout(waitUntilReady, readyIntervalMs)
}
}
setTimeout(waitUntilReady, readyIntervalMs)
})()
document.currentScript?.remove()

View file

@ -0,0 +1,171 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::array::TryFromSliceError;
use std::borrow::Cow;
use std::fmt::{Debug, Formatter};
use std::string::FromUtf8Error;
use aes_gcm::aead::Aead;
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use getrandom::Error as CsprngError;
use serialize_to_javascript::{Template, default_template};
/// The style for the isolation iframe.
pub const IFRAME_STYLE: &str = "#__tauri_isolation__ { display: none !important }";
/// Errors that can occur during Isolation keys generation.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// Something went wrong with the CSPRNG.
#[error("CSPRNG error")]
Csprng(#[from] CsprngError),
/// Something went wrong with decrypting an AES-GCM payload
#[error("AES-GCM")]
Aes,
/// Nonce was not 96 bits
#[error("Nonce: {0}")]
NonceSize(#[from] TryFromSliceError),
/// Payload was not valid utf8
#[error("{0}")]
Utf8(#[from] FromUtf8Error),
/// Invalid json format
#[error("{0}")]
Json(#[from] serde_json::Error),
}
/// A formatted AES-GCM cipher instance along with the key used to initialize it.
#[derive(Clone)]
pub struct AesGcmPair {
raw: [u8; 32],
key: Aes256Gcm,
}
impl Debug for AesGcmPair {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "AesGcmPair(...)")
}
}
impl AesGcmPair {
fn new() -> Result<Self, Error> {
let mut raw = [0u8; 32];
getrandom::fill(&mut raw)?;
let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&raw);
Ok(Self {
raw,
key: Aes256Gcm::new(key),
})
}
/// The raw value used to create the AES-GCM key
pub fn raw(&self) -> &[u8; 32] {
&self.raw
}
/// The formatted AES-GCM key
pub fn key(&self) -> &Aes256Gcm {
&self.key
}
#[doc(hidden)]
pub fn encrypt(&self, nonce: &[u8; 12], payload: &[u8]) -> Result<Vec<u8>, Error> {
self
.key
.encrypt(nonce.into(), payload)
.map_err(|_| self::Error::Aes)
}
}
/// All cryptographic keys required for Isolation encryption
#[derive(Debug, Clone)]
pub struct Keys {
/// AES-GCM key
aes_gcm: AesGcmPair,
}
impl Keys {
/// Securely generate required keys for Isolation encryption.
pub fn new() -> Result<Self, Error> {
AesGcmPair::new().map(|aes_gcm| Self { aes_gcm })
}
/// The AES-GCM data (and raw data).
pub fn aes_gcm(&self) -> &AesGcmPair {
&self.aes_gcm
}
/// Decrypts a message using the generated keys.
pub fn decrypt(&self, raw: RawIsolationPayload<'_>) -> Result<Vec<u8>, Error> {
let RawIsolationPayload { nonce, payload, .. } = raw;
let nonce: [u8; 12] = nonce.as_ref().try_into()?;
self
.aes_gcm
.key
.decrypt(Nonce::from_slice(&nonce), payload.as_ref())
.map_err(|_| self::Error::Aes)
}
}
/// Raw representation of
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawIsolationPayload<'a> {
nonce: Cow<'a, [u8]>,
payload: Cow<'a, [u8]>,
content_type: Cow<'a, str>,
}
impl<'a> RawIsolationPayload<'a> {
/// Content type of this payload.
pub fn content_type(&self) -> &Cow<'a, str> {
&self.content_type
}
}
impl<'a> TryFrom<&'a Vec<u8>> for RawIsolationPayload<'a> {
type Error = Error;
fn try_from(value: &'a Vec<u8>) -> Result<Self, Self::Error> {
serde_json::from_slice(value).map_err(Into::into)
}
}
/// The Isolation JavaScript template meant to be injected during codegen.
///
/// Note: This struct is not considered part of the stable API
#[derive(Template)]
#[default_template("isolation.js")]
pub struct IsolationJavascriptCodegen {
// this template intentionally does not include the runtime field
}
/// The Isolation JavaScript template meant to be injected during runtime.
///
/// Note: This struct is not considered part of the stable API
#[derive(Template)]
#[default_template("isolation.js")]
pub struct IsolationJavascriptRuntime<'a> {
/// The key used on the Rust backend and the Isolation Javascript
pub runtime_aes_gcm_key: &'a [u8; 32],
/// The origin the isolation application is expecting messages from.
pub origin: String,
/// The function that processes the IPC message.
#[raw]
pub process_ipc_message_fn: &'a str,
}
#[cfg(test)]
mod test {
#[test]
fn create_keys() -> Result<(), Box<dyn std::error::Error>> {
let _ = super::Keys::new()?;
Ok(())
}
}

View file

@ -0,0 +1,7 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
/// Handling the Tauri "Isolation" Pattern.
#[cfg(feature = "isolation")]
pub mod isolation;

View file

@ -0,0 +1,434 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Platform helper functions.
use std::{fmt::Display, path::PathBuf};
use serde::{Deserialize, Serialize};
use crate::{Env, PackageInfo, config::BundleType};
mod starting_binary;
/// URI prefix of a Tauri asset.
///
/// This is referenced in the Tauri Android library,
/// which resolves these assets to a file descriptor.
#[cfg(target_os = "android")]
pub const ANDROID_ASSET_PROTOCOL_URI_PREFIX: &str = "asset://localhost/";
/// Platform target.
#[derive(PartialEq, Eq, Copy, Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum Target {
/// MacOS.
#[serde(rename = "macOS")]
MacOS,
/// Windows.
Windows,
/// Linux.
Linux,
/// Android.
Android,
/// iOS.
#[serde(rename = "iOS")]
Ios,
}
impl Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::MacOS => "macOS",
Self::Windows => "windows",
Self::Linux => "linux",
Self::Android => "android",
Self::Ios => "iOS",
}
)
}
}
impl Target {
/// Parses the target from the given target triple.
pub fn from_triple(target: &str) -> Self {
if target.contains("darwin") {
Self::MacOS
} else if target.contains("windows") {
Self::Windows
} else if target.contains("android") {
Self::Android
} else if target.contains("ios") {
Self::Ios
} else {
Self::Linux
}
}
/// Gets the current build target.
pub fn current() -> Self {
if cfg!(target_os = "macos") {
Self::MacOS
} else if cfg!(target_os = "windows") {
Self::Windows
} else if cfg!(target_os = "ios") {
Self::Ios
} else if cfg!(target_os = "android") {
Self::Android
} else {
Self::Linux
}
}
/// Whether the target is mobile or not.
pub fn is_mobile(&self) -> bool {
matches!(self, Target::Android | Target::Ios)
}
/// Whether the target is desktop or not.
pub fn is_desktop(&self) -> bool {
!self.is_mobile()
}
}
/// Retrieves the currently running binary's path, taking into account security considerations.
///
/// The path is cached as soon as possible (before even `main` runs) and that value is returned
/// repeatedly instead of fetching the path every time. It is possible for the path to not be found,
/// or explicitly disabled (see following macOS specific behavior).
///
/// # Platform-specific behavior
///
/// On `macOS`, this function will return an error if the original path contained any symlinks
/// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the
/// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*.
///
/// # Security
///
/// If the above platform-specific behavior does **not** take place, this function uses the
/// following resolution.
///
/// We canonicalize the path we received from [`std::env::current_exe`] to resolve any soft links.
/// This avoids the usual issue of needing the file to exist at the passed path because a valid
/// current executable result for our purpose should always exist. Notably,
/// [`std::env::current_exe`] also has a security section that goes over a theoretical attack using
/// hard links. Let's cover some specific topics that relate to different ways an attacker might
/// try to trick this function into returning the wrong binary path.
///
/// ## Symlinks ("Soft Links")
///
/// [`std::path::Path::canonicalize`] is used to resolve symbolic links to the original path,
/// including nested symbolic links (`link2 -> link1 -> bin`). On macOS, any results that include
/// a symlink are rejected by default due to lesser symlink protections. This can be disabled,
/// **although discouraged**, with the `process-relaunch-dangerous-allow-symlink-macos` feature.
///
/// ## Hard Links
///
/// A [Hard Link] is a named entry that points to a file in the file system.
/// On most systems, this is what you would think of as a "file". The term is
/// used on filesystems that allow multiple entries to point to the same file.
/// The linked [Hard Link] Wikipedia page provides a decent overview.
///
/// In short, unless the attacker was able to create the link with elevated
/// permissions, it should generally not be possible for them to hard link
/// to a file they do not have permissions to - with exception to possible
/// operating system exploits.
///
/// There are also some platform-specific information about this below.
///
/// ### Windows
///
/// Windows requires a permission to be set for the user to create a symlink
/// or a hard link, regardless of ownership status of the target. Elevated
/// permissions users have the ability to create them.
///
/// ### macOS
///
/// macOS allows for the creation of symlinks and hard links to any file.
/// Accessing through those links will fail if the user who owns the links
/// does not have the proper permissions on the original file.
///
/// ### Linux
///
/// Linux allows for the creation of symlinks to any file. Accessing the
/// symlink will fail if the user who owns the symlink does not have the
/// proper permissions on the original file.
///
/// Linux additionally provides a kernel hardening feature since version
/// 3.6 (30 September 2012). Most distributions since then have enabled
/// the protection (setting `fs.protected_hardlinks = 1`) by default, which
/// means that a vast majority of desktop Linux users should have it enabled.
/// **The feature prevents the creation of hardlinks that the user does not own
/// or have read/write access to.** [See the patch that enabled this].
///
/// [Hard Link]: https://en.wikipedia.org/wiki/Hard_link
/// [See the patch that enabled this]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=800179c9b8a1e796e441674776d11cd4c05d61d7
pub fn current_exe() -> std::io::Result<PathBuf> {
self::starting_binary::STARTING_BINARY.cloned()
}
/// Try to determine the current target triple.
///
/// Returns a target triple (e.g. `x86_64-unknown-linux-gnu` or `i686-pc-windows-msvc`) or an
/// `Error::Config` if the current config cannot be determined or is not some combination of the
/// following values:
/// `linux, mac, windows` -- `i686, x86, armv7` -- `gnu, musl, msvc`
///
/// * Errors:
/// * Unexpected system config
pub fn target_triple() -> crate::Result<String> {
let arch = if cfg!(target_arch = "x86") {
"i686"
} else if cfg!(target_arch = "x86_64") {
"x86_64"
} else if cfg!(target_arch = "arm") {
"armv7"
} else if cfg!(target_arch = "aarch64") {
"aarch64"
} else if cfg!(target_arch = "riscv64") {
"riscv64"
} else {
return Err(crate::Error::Architecture);
};
let os = if cfg!(target_os = "linux") {
"unknown-linux"
} else if cfg!(target_os = "macos") {
"apple-darwin"
} else if cfg!(target_os = "windows") {
"pc-windows"
} else if cfg!(target_os = "freebsd") {
"unknown-freebsd"
} else {
return Err(crate::Error::Os);
};
let os = if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") {
String::from(os)
} else {
let env = if cfg!(target_env = "gnu") {
"gnu"
} else if cfg!(target_env = "musl") {
"musl"
} else if cfg!(target_env = "msvc") {
"msvc"
} else {
return Err(crate::Error::Environment);
};
format!("{os}-{env}")
};
Ok(format!("{arch}-{os}"))
}
#[cfg(all(not(test), not(target_os = "android")))]
fn is_cargo_output_directory(path: &std::path::Path) -> bool {
path.join(".cargo-lock").exists()
}
#[cfg(test)]
const CARGO_OUTPUT_DIRECTORIES: &[&str] = &["debug", "release", "custom-profile"];
#[cfg(test)]
fn is_cargo_output_directory(path: &std::path::Path) -> bool {
let Some(last_component) = path.components().next_back() else {
return false;
};
CARGO_OUTPUT_DIRECTORIES
.iter()
.any(|dirname| &last_component.as_os_str() == dirname)
}
/// Computes the resource directory of the current environment.
///
/// ## Platform-specific
///
/// - **Windows:** Resolves to the directory that contains the main executable.
/// - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to
/// the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`.
/// If not running in an AppImage, the path is `/usr/lib/${exe_name}`.
/// When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`.
/// - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app).
/// - **iOS:** Resolves to `${exe_dir}/assets`.
/// - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path,
/// we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/),
/// with that, you can read the files through [`FsExt::fs`](https://docs.rs/tauri-plugin-fs/latest/tauri_plugin_fs/trait.FsExt.html#tymethod.fs)
/// like this: `app.fs().read_to_string(app.path().resource_dir().unwrap().join("resource"));`
pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> crate::Result<PathBuf> {
#[cfg(target_os = "android")]
return resource_dir_android(package_info, env);
#[cfg(not(target_os = "android"))]
{
let exe = current_exe()?;
resource_dir_from(exe, package_info, env)
}
}
#[cfg(target_os = "android")]
fn resource_dir_android(_package_info: &PackageInfo, _env: &Env) -> crate::Result<PathBuf> {
Ok(PathBuf::from(ANDROID_ASSET_PROTOCOL_URI_PREFIX))
}
#[cfg(not(target_os = "android"))]
#[allow(unused_variables)]
fn resource_dir_from<P: AsRef<std::path::Path>>(
exe: P,
package_info: &PackageInfo,
env: &Env,
) -> crate::Result<PathBuf> {
let exe_dir = exe.as_ref().parent().expect("failed to get exe directory");
let curr_dir = exe_dir.display().to_string();
let parts: Vec<&str> = curr_dir.split(std::path::MAIN_SEPARATOR).collect();
let len = parts.len();
// Check if running from the Cargo output directory, which means it's an executable in a development machine
// We check if the binary is inside a `target` folder which can be either `target/$profile` or `target/$triple/$profile`
// and see if there's a .cargo-lock file along the executable
// This ensures the check is safer so it doesn't affect apps in production
// Windows also includes the resources in the executable folder so we check that too
if cfg!(target_os = "windows")
|| ((len >= 2 && parts[len - 2] == "target") || (len >= 3 && parts[len - 3] == "target"))
&& is_cargo_output_directory(exe_dir)
{
return Ok(exe_dir.to_path_buf());
}
#[allow(unused_mut, unused_assignments)]
let mut res = Err(crate::Error::UnsupportedPlatform);
#[cfg(target_os = "linux")]
{
// (canonicalize checks for existence, so there's no need for an extra check)
res = if let Ok(bundle_dir) = exe_dir
.join(format!("../lib/{}", package_info.name))
.canonicalize()
{
Ok(bundle_dir)
} else if let Some(appdir) = &env.appdir {
let appdir: &std::path::Path = appdir.as_ref();
Ok(PathBuf::from(format!(
"{}/usr/lib/{}",
appdir.display(),
package_info.name
)))
} else {
// running bundle
Ok(PathBuf::from(format!("/usr/lib/{}", package_info.name)))
};
}
#[cfg(target_os = "macos")]
{
res = exe_dir
.join("../Resources")
.canonicalize()
.map_err(Into::into);
}
#[cfg(target_os = "ios")]
{
res = exe_dir.join("assets").canonicalize().map_err(Into::into);
}
res
}
// Variable holding the type of bundle the executable is stored in. This is modified by binary
// patching during build
#[used]
// Marked as `mut` because it could get optimized away without it,
// see https://github.com/tauri-apps/tauri/pull/13812
static mut __TAURI_BUNDLE_TYPE: &str = "__TAURI_BUNDLE_TYPE_VAR_UNK";
/// Get the type of the bundle current binary is packaged in.
/// If the bundle type is unknown, it returns [`Option::None`].
pub fn bundle_type() -> Option<BundleType> {
unsafe {
match __TAURI_BUNDLE_TYPE {
"__TAURI_BUNDLE_TYPE_VAR_DEB" => Some(BundleType::Deb),
"__TAURI_BUNDLE_TYPE_VAR_RPM" => Some(BundleType::Rpm),
"__TAURI_BUNDLE_TYPE_VAR_APP" => Some(BundleType::AppImage),
"__TAURI_BUNDLE_TYPE_VAR_MSI" => Some(BundleType::Msi),
"__TAURI_BUNDLE_TYPE_VAR_NSS" => Some(BundleType::Nsis),
_ => {
if cfg!(target_os = "macos") {
Some(BundleType::App)
} else {
None
}
}
}
}
}
#[cfg(any(feature = "build", feature = "build-2"))]
mod build {
use proc_macro2::TokenStream;
use quote::{ToTokens, TokenStreamExt, quote};
use super::*;
impl ToTokens for Target {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::platform::Target };
tokens.append_all(match self {
Self::MacOS => quote! { #prefix::MacOS },
Self::Linux => quote! { #prefix::Linux },
Self::Windows => quote! { #prefix::Windows },
Self::Android => quote! { #prefix::Android },
Self::Ios => quote! { #prefix::Ios },
});
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::{Env, PackageInfo};
#[test]
#[cfg(not(target_os = "android"))]
fn resolve_resource_dir() {
let package_info = PackageInfo {
name: "MyApp".into(),
version: "1.0.0".parse().unwrap(),
authors: "",
description: "",
crate_name: "my-app",
};
let env = Env::default();
let path = PathBuf::from("/path/to/target/aarch64-apple-darwin/debug/app");
let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
assert_eq!(resource_dir, path.parent().unwrap());
let path = PathBuf::from("/path/to/target/custom-profile/app");
let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
assert_eq!(resource_dir, path.parent().unwrap());
let path = PathBuf::from("/path/to/target/release/app");
let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
assert_eq!(resource_dir, path.parent().unwrap());
let path = PathBuf::from("/path/to/target/unknown-profile/app");
#[allow(clippy::needless_borrows_for_generic_args)]
let resource_dir = super::resource_dir_from(&path, &package_info, &env);
#[cfg(target_os = "macos")]
assert!(resource_dir.is_err());
#[cfg(target_os = "linux")]
assert_eq!(resource_dir.unwrap(), PathBuf::from("/usr/lib/MyApp"));
#[cfg(windows)]
assert_eq!(resource_dir.unwrap(), path.parent().unwrap());
}
}

View file

@ -0,0 +1,83 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use ctor::ctor;
use std::{
io::{Error, ErrorKind, Result},
path::{Path, PathBuf},
};
/// A cached version of the current binary using [`ctor`] to cache it before even `main` runs.
#[ctor]
#[used]
pub(super) static STARTING_BINARY: StartingBinary = unsafe { StartingBinary::new() };
/// Represents a binary path that was cached when the program was loaded.
pub(super) struct StartingBinary(std::io::Result<PathBuf>);
impl StartingBinary {
/// Find the starting executable as safely as possible.
fn new() -> Self {
// see notes on current_exe() for security implications
let dangerous_path = match std::env::current_exe() {
Ok(dangerous_path) => dangerous_path,
error @ Err(_) => return Self(error),
};
// note: this only checks symlinks on problematic platforms, see implementation below
if let Some(symlink) = Self::has_symlink(&dangerous_path) {
return Self(Err(Error::new(
ErrorKind::InvalidData,
format!(
"StartingBinary found current_exe() that contains a symlink on a non-allowed platform: {}",
symlink.display()
),
)));
}
// we canonicalize the path to resolve any symlinks to the real exe path
Self(dangerous_path.canonicalize())
}
/// A clone of the [`PathBuf`] found to be the starting path.
///
/// Because [`Error`] is not clone-able, it is recreated instead.
pub(super) fn cloned(&self) -> Result<PathBuf> {
// false positive
#[allow(clippy::useless_asref)]
self
.0
.as_ref()
.map(Clone::clone)
.map_err(|e| Error::new(e.kind(), e.to_string()))
}
/// We only care about checking this on macOS currently, as it has the least symlink protections.
#[cfg(any(
not(target_os = "macos"),
feature = "process-relaunch-dangerous-allow-symlink-macos"
))]
fn has_symlink(_: &Path) -> Option<&Path> {
None
}
/// We only care about checking this on macOS currently, as it has the least symlink protections.
#[cfg(all(
target_os = "macos",
not(feature = "process-relaunch-dangerous-allow-symlink-macos")
))]
fn has_symlink(path: &Path) -> Option<&Path> {
path.ancestors().find(|ancestor| {
matches!(
ancestor
.symlink_metadata()
.as_ref()
.map(std::fs::Metadata::file_type)
.as_ref()
.map(std::fs::FileType::is_symlink),
Ok(true)
)
})
}
}

View file

@ -0,0 +1,88 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Compile-time and runtime types for Tauri plugins.
#[cfg(any(feature = "build", feature = "build-2"))]
pub use build::*;
#[cfg(any(feature = "build", feature = "build-2"))]
mod build {
use std::{
env::vars_os,
fs,
path::{Path, PathBuf},
};
const GLOBAL_API_SCRIPT_PATH_KEY: &str = "GLOBAL_API_SCRIPT_PATH";
/// Known file name of the file that contains an array with the path of all API scripts defined with [`define_global_api_script_path`].
pub const GLOBAL_API_SCRIPT_FILE_LIST_PATH: &str = "__global-api-script.js";
/// Defines the path to the global API script using Cargo instructions.
pub fn define_global_api_script_path(path: &Path) {
println!(
"cargo:{GLOBAL_API_SCRIPT_PATH_KEY}={}",
path
.canonicalize()
.expect("failed to canonicalize global API script path")
.display()
)
}
/// Collects the path of all the global API scripts defined with [`define_global_api_script_path`]
/// and saves them to the out dir with filename [`GLOBAL_API_SCRIPT_FILE_LIST_PATH`].
///
/// `tauri_global_scripts` is only used in Tauri's monorepo for the examples to work
/// since they don't have a build script to run `tauri-build` and pull in the deps env vars
pub fn save_global_api_scripts_paths(out_dir: &Path, mut tauri_global_scripts: Option<PathBuf>) {
let mut scripts = Vec::new();
for (key, value) in vars_os() {
let key = key.to_string_lossy();
if key == format!("DEP_TAURI_{GLOBAL_API_SCRIPT_PATH_KEY}") {
tauri_global_scripts = Some(PathBuf::from(value));
} else if key.starts_with("DEP_") && key.ends_with(GLOBAL_API_SCRIPT_PATH_KEY) {
let script_path = PathBuf::from(value);
scripts.push(script_path);
}
}
if let Some(tauri_global_scripts) = tauri_global_scripts {
scripts.insert(0, tauri_global_scripts);
}
fs::write(
out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH),
serde_json::to_string(&scripts).expect("failed to serialize global API script paths"),
)
.expect("failed to write global API script");
}
/// Read global api scripts from [`GLOBAL_API_SCRIPT_FILE_LIST_PATH`]
pub fn read_global_api_scripts(out_dir: &Path) -> Option<Vec<String>> {
let global_scripts_path = out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH);
if !global_scripts_path.exists() {
return None;
}
let global_scripts_str = fs::read_to_string(global_scripts_path)
.expect("failed to read plugin global API script paths");
let global_scripts = serde_json::from_str::<Vec<PathBuf>>(&global_scripts_str)
.expect("failed to parse plugin global API script paths");
Some(
global_scripts
.into_iter()
.map(|p| {
fs::read_to_string(&p).unwrap_or_else(|e| {
panic!(
"failed to read plugin global API script {}: {e}",
p.display()
)
})
})
.collect(),
)
}
}

View file

@ -0,0 +1,643 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
collections::HashMap,
path::{Component, Path, PathBuf},
};
use walkdir::WalkDir;
use crate::platform::Target as TargetPlatform;
/// Given a path (absolute or relative) to a resource file, returns the
/// relative path from the bundle resources directory where that resource
/// should be stored.
pub fn resource_relpath(path: &Path) -> PathBuf {
let mut dest = PathBuf::new();
for component in path.components() {
match component {
Component::Prefix(_) => {}
Component::RootDir => dest.push("_root_"),
Component::CurDir => {}
Component::ParentDir => dest.push("_up_"),
Component::Normal(string) => dest.push(string),
}
}
dest
}
fn normalize(path: &Path) -> PathBuf {
let mut dest = PathBuf::new();
for component in path.components() {
match component {
Component::Prefix(_) => {}
Component::RootDir => dest.push("/"),
Component::CurDir => {}
Component::ParentDir => dest.push(".."),
Component::Normal(string) => dest.push(string),
}
}
dest
}
/// Parses the external binaries to bundle, adding the target triple suffix to each of them.
pub fn external_binaries(
external_binaries: &[String],
target_triple: &str,
target_platform: &TargetPlatform,
) -> Vec<String> {
let mut paths = Vec::new();
for curr_path in external_binaries {
let extension = if matches!(target_platform, TargetPlatform::Windows) {
".exe"
} else {
""
};
paths.push(format!("{curr_path}-{target_triple}{extension}"));
}
paths
}
/// Information for a resource.
#[derive(Debug)]
pub struct Resource {
path: PathBuf,
target: PathBuf,
}
impl Resource {
/// The path of the resource.
pub fn path(&self) -> &Path {
&self.path
}
/// The target location of the resource.
pub fn target(&self) -> &Path {
&self.target
}
}
#[derive(Debug)]
enum PatternIter<'a> {
Slice(std::slice::Iter<'a, String>),
Map(std::collections::hash_map::Iter<'a, String, String>),
}
/// A helper to iterate through resources.
pub struct ResourcePaths<'a> {
iter: ResourcePathsIter<'a>,
}
impl<'a> ResourcePaths<'a> {
/// Creates a new ResourcePaths from a slice of patterns to iterate
pub fn new(patterns: &'a [String], allow_walk: bool) -> ResourcePaths<'a> {
ResourcePaths {
iter: ResourcePathsIter {
pattern_iter: PatternIter::Slice(patterns.iter()),
allow_walk,
current_dest: None,
current_iter: None,
},
}
}
/// Creates a new ResourcePaths from a slice of patterns to iterate
pub fn from_map(patterns: &'a HashMap<String, String>, allow_walk: bool) -> ResourcePaths<'a> {
ResourcePaths {
iter: ResourcePathsIter {
pattern_iter: PatternIter::Map(patterns.iter()),
allow_walk,
current_dest: None,
current_iter: None,
},
}
}
/// Returns the resource iterator that yields the source and target paths.
/// Needed when using [`Self::from_map`].
pub fn iter(self) -> ResourcePathsIter<'a> {
self.iter
}
}
/// Iterator of a [`ResourcePaths`].
#[derive(Debug)]
pub struct ResourcePathsIter<'a> {
/// the patterns to iterate.
pattern_iter: PatternIter<'a>,
/// whether the resource paths allows directories or not.
allow_walk: bool,
/// The value of map when [`Self::pattern_iter`] is a [`PatternIter::Map`],
/// used for determining [`Resource::target`]
current_dest: Option<PathBuf>,
/// The iter for the current pattern. The cycle goes like this:
/// [`ResourcePaths::next`] -> [`Self::next`] -> [`Self::pattern_iter::next`] -> [`Self::current_iter::next`]
current_iter: Option<ResourcePathsInnerIter>,
}
#[derive(Debug)]
enum ResourcePathsInnerIter {
Walk {
iter: walkdir::IntoIter,
/// The key of map when [`ResourcePathsIter::pattern_iter`] is a [`PatternIter::Map`],
/// used for determining [`Resource::target`]
current_pattern: Option<PathBuf>,
},
Glob {
iter: glob::Paths,
},
}
impl Iterator for ResourcePathsInnerIter {
type Item = crate::Result<PathBuf>;
fn next(&mut self) -> Option<crate::Result<PathBuf>> {
match self {
ResourcePathsInnerIter::Walk { iter, .. } => Some(
iter
.next()?
.map(|entry| entry.into_path())
.map_err(Into::into),
),
ResourcePathsInnerIter::Glob { iter } => Some(iter.next()?.map_err(Into::into)),
}
}
}
impl ResourcePathsIter<'_> {
fn next_current_iter(&mut self) -> Option<crate::Result<Resource>> {
let current_iter = self.current_iter.as_mut().unwrap();
let entry = current_iter.next()?;
Some(match entry {
Ok(entry) => {
// Skip directories
if entry.is_dir() {
self.next_current_iter()?
} else {
self.resource_from_path(normalize(&entry))
}
}
Err(error) => Err(error),
})
}
fn resource_from_path(&self, path: PathBuf) -> crate::Result<Resource> {
if !path.exists() {
return Err(crate::Error::ResourcePathNotFound(path));
}
Ok(Resource {
target: if let Some(dest) = &self.current_dest {
match &self.current_iter {
Some(current_iter) => match current_iter {
// if processing a directory, preserve directory structure under current_dest
ResourcePathsInnerIter::Walk {
current_pattern, ..
} => {
if let Some(pattern) = current_pattern {
dest.join(path.strip_prefix(pattern).unwrap_or(&path))
} else {
dest.join(&path)
}
}
// if processing a glob and current_dest is not empty
// we put all globbed paths under current_dest
// preserving the file name as it is
ResourcePathsInnerIter::Glob { .. } => dest.join(path.file_name().unwrap()),
},
None => dest.clone(),
}
} else {
// If [`ResourcePathsIter::pattern_iter`] is a [`PatternIter::Slice`]
resource_relpath(&path)
},
path,
})
}
fn next_pattern(&mut self) -> Option<crate::Result<Resource>> {
self.current_dest = None;
let pattern = match &mut self.pattern_iter {
PatternIter::Slice(iter) => iter.next()?,
PatternIter::Map(iter) => {
let (pattern, dest) = iter.next()?;
self.current_dest = Some(resource_relpath(Path::new(dest)));
pattern
}
};
if pattern.contains('*') {
self.current_iter = match glob::glob(pattern) {
Ok(glob) => Some(ResourcePathsInnerIter::Glob { iter: glob }),
Err(error) => return Some(Err(error.into())),
};
match self.next_current_iter() {
Some(r) => return Some(r),
None => {
self.current_iter = None;
return Some(Err(crate::Error::GlobPathNotFound(pattern.clone())));
}
}
} else {
let path = normalize(Path::new(pattern));
if path.is_dir() {
if !self.allow_walk {
return Some(Err(crate::Error::NotAllowedToWalkDir(path)));
}
self.current_iter = Some(ResourcePathsInnerIter::Walk {
iter: WalkDir::new(&path).into_iter(),
current_pattern: if matches!(self.pattern_iter, PatternIter::Map(_)) {
Some(path)
} else {
None
},
});
} else {
return Some(self.resource_from_path(path));
}
}
self.next_current_iter()
}
}
impl Iterator for ResourcePaths<'_> {
type Item = crate::Result<PathBuf>;
fn next(&mut self) -> Option<crate::Result<PathBuf>> {
self.iter.next().map(|r| r.map(|res| res.path))
}
}
impl Iterator for ResourcePathsIter<'_> {
type Item = crate::Result<Resource>;
fn next(&mut self) -> Option<crate::Result<Resource>> {
if self.current_iter.is_some() {
match self.next_current_iter() {
Some(r) => return Some(r),
None => self.current_iter = None,
}
}
self.next_pattern()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
impl PartialEq for Resource {
fn eq(&self, other: &Self) -> bool {
self.path == other.path && self.target == other.target
}
}
fn expected_resources(resources: &[(&str, &str)]) -> Vec<Resource> {
resources
.iter()
.map(|(path, target)| Resource {
path: Path::new(path).components().collect(),
target: Path::new(target).components().collect(),
})
.collect()
}
fn setup_test_dirs() {
let mut random = [0; 1];
getrandom::fill(&mut random).unwrap();
let temp = std::env::temp_dir();
let temp = temp.join(format!("tauri_resource_paths_iter_test_{}", random[0]));
let _ = fs::remove_dir_all(&temp);
fs::create_dir_all(&temp).unwrap();
std::env::set_current_dir(&temp).unwrap();
let paths = [
"src-tauri/tauri.conf.json",
"src-tauri/some-other-json.json",
"src-tauri/Cargo.toml",
"src-tauri/Tauri.toml",
"src-tauri/build.rs",
"src-tauri/some-folder/some-file.txt",
"src/assets/javascript.svg",
"src/assets/tauri.svg",
"src/assets/rust.svg",
"src/assets/lang/en.json",
"src/assets/lang/ar.json",
"src/sounds/lang/es.wav",
"src/sounds/lang/fr.wav",
"src/textures/ground/earth.tex",
"src/textures/ground/sand.tex",
"src/textures/water.tex",
"src/textures/fire.tex",
"src/tiles/sky/grey.tile",
"src/tiles/sky/yellow.tile",
"src/tiles/grass.tile",
"src/tiles/stones.tile",
"src/index.html",
"src/style.css",
"src/script.js",
"src/dir/another-dir/file1.txt",
"src/dir/another-dir2/file2.txt",
];
for path in paths {
let path = Path::new(path);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, "").unwrap();
}
}
fn resources_map(literal: &[(&str, &str)]) -> HashMap<String, String> {
literal
.iter()
.map(|(from, to)| (from.to_string(), to.to_string()))
.collect()
}
#[test]
#[serial_test::serial(resources)]
fn resource_paths_iter_slice_allow_walk() {
setup_test_dirs();
let dir = std::env::current_dir().unwrap().join("src-tauri");
let _ = std::env::set_current_dir(dir);
let resources = ResourcePaths::new(
&[
"../src/script.js".into(),
"../src/assets".into(),
"../src/index.html".into(),
"../src/sounds".into(),
// Should be the same as `../src/textures/` or `../src/textures`
"../src/textures/**/*".into(),
"*.toml".into(),
"*.conf.json".into(),
],
true,
)
.iter()
.flatten()
.collect::<Vec<_>>();
let expected = expected_resources(&[
// From `../src/script.js`
("../src/script.js", "_up_/src/script.js"),
// From `../src/assets`
(
"../src/assets/javascript.svg",
"_up_/src/assets/javascript.svg",
),
("../src/assets/tauri.svg", "_up_/src/assets/tauri.svg"),
("../src/assets/rust.svg", "_up_/src/assets/rust.svg"),
("../src/assets/lang/en.json", "_up_/src/assets/lang/en.json"),
("../src/assets/lang/ar.json", "_up_/src/assets/lang/ar.json"),
// From `../src/index.html`
("../src/index.html", "_up_/src/index.html"),
// From `../src/sounds`
("../src/sounds/lang/es.wav", "_up_/src/sounds/lang/es.wav"),
("../src/sounds/lang/fr.wav", "_up_/src/sounds/lang/fr.wav"),
// From `../src/textures/**/*`
(
"../src/textures/ground/earth.tex",
"_up_/src/textures/ground/earth.tex",
),
(
"../src/textures/ground/sand.tex",
"_up_/src/textures/ground/sand.tex",
),
("../src/textures/water.tex", "_up_/src/textures/water.tex"),
("../src/textures/fire.tex", "_up_/src/textures/fire.tex"),
// From `*.toml`
("Cargo.toml", "Cargo.toml"),
("Tauri.toml", "Tauri.toml"),
// From `*.conf.json`
("tauri.conf.json", "tauri.conf.json"),
]);
assert_eq!(resources.len(), expected.len());
for resource in expected {
if !resources.contains(&resource) {
panic!("{resource:?} was expected but not found in {resources:?}");
}
}
}
#[test]
#[serial_test::serial(resources)]
fn resource_paths_iter_slice_no_walk() {
setup_test_dirs();
let dir = std::env::current_dir().unwrap().join("src-tauri");
let _ = std::env::set_current_dir(dir);
let resources = ResourcePaths::new(
&[
"../src/script.js".into(),
"../src/assets".into(),
"../src/index.html".into(),
"../src/sounds".into(),
"*.toml".into(),
"*.conf.json".into(),
],
false,
)
.iter()
.flatten()
.collect::<Vec<_>>();
let expected = expected_resources(&[
("../src/script.js", "_up_/src/script.js"),
("../src/index.html", "_up_/src/index.html"),
("Cargo.toml", "Cargo.toml"),
("Tauri.toml", "Tauri.toml"),
("tauri.conf.json", "tauri.conf.json"),
]);
assert_eq!(resources.len(), expected.len());
for resource in expected {
if !resources.contains(&resource) {
panic!("{resource:?} was expected but not found in {resources:?}");
}
}
}
#[test]
#[serial_test::serial(resources)]
fn resource_paths_iter_map_allow_walk() {
setup_test_dirs();
let dir = std::env::current_dir().unwrap().join("src-tauri");
let _ = std::env::set_current_dir(dir);
let resources = ResourcePaths::from_map(
&resources_map(&[
("../src/script.js", "main.js"),
("../src/assets", ""),
("../src/index.html", "frontend/index.html"),
("../src/sounds", "voices"),
("../src/textures/*", "textures"),
("../src/tiles/**/*", "tiles"),
("*.toml", ""),
("*.conf.json", "json"),
("./some-folder/", "some-target-folder/"),
("../non-existent-file", "asd"), // invalid case
("../non/*", "asd"), // invalid case
]),
true,
)
.iter()
.flatten()
.collect::<Vec<_>>();
let expected = expected_resources(&[
("../src/script.js", "main.js"),
("../src/assets/javascript.svg", "javascript.svg"),
("../src/assets/tauri.svg", "tauri.svg"),
("../src/assets/rust.svg", "rust.svg"),
("../src/assets/lang/en.json", "lang/en.json"),
("../src/assets/lang/ar.json", "lang/ar.json"),
("../src/index.html", "frontend/index.html"),
("../src/sounds/lang/es.wav", "voices/lang/es.wav"),
("../src/sounds/lang/fr.wav", "voices/lang/fr.wav"),
("../src/textures/water.tex", "textures/water.tex"),
("../src/textures/fire.tex", "textures/fire.tex"),
("../src/tiles/grass.tile", "tiles/grass.tile"),
("../src/tiles/stones.tile", "tiles/stones.tile"),
("../src/tiles/sky/grey.tile", "tiles/grey.tile"),
("../src/tiles/sky/yellow.tile", "tiles/yellow.tile"),
("Cargo.toml", "Cargo.toml"),
("Tauri.toml", "Tauri.toml"),
("tauri.conf.json", "json/tauri.conf.json"),
(
"some-folder/some-file.txt",
"some-target-folder/some-file.txt",
),
]);
assert_eq!(resources.len(), expected.len());
for resource in expected {
if !resources.contains(&resource) {
panic!("{resource:?} was expected but not found in {resources:?}");
}
}
}
#[test]
#[serial_test::serial(resources)]
fn resource_paths_iter_map_no_walk() {
setup_test_dirs();
let dir = std::env::current_dir().unwrap().join("src-tauri");
let _ = std::env::set_current_dir(dir);
let resources = ResourcePaths::from_map(
&resources_map(&[
("../src/script.js", "main.js"),
("../src/assets", ""),
("../src/index.html", "frontend/index.html"),
("../src/sounds", "voices"),
("*.toml", ""),
("*.conf.json", "json"),
]),
false,
)
.iter()
.flatten()
.collect::<Vec<_>>();
let expected = expected_resources(&[
("../src/script.js", "main.js"),
("../src/index.html", "frontend/index.html"),
("Cargo.toml", "Cargo.toml"),
("Tauri.toml", "Tauri.toml"),
("tauri.conf.json", "json/tauri.conf.json"),
]);
assert_eq!(resources.len(), expected.len());
for resource in expected {
if !resources.contains(&resource) {
panic!("{resource:?} was expected but not found in {resources:?}");
}
}
}
#[test]
#[serial_test::serial(resources)]
fn resource_paths_errors() {
setup_test_dirs();
let dir = std::env::current_dir().unwrap().join("src-tauri");
let _ = std::env::set_current_dir(dir);
let resources = ResourcePaths::from_map(
&resources_map(&[
("../non-existent-file", "file"),
("../non-existent-dir", "dir"),
// exists but not allowed to walk
("../src", "dir2"),
// doesn't exist but it is a glob and will return an error
("../non-existent-glob-dir/*", "glob"),
// exists but only contains directories and will not produce any values
("../src/dir/*", "dir3"),
]),
false,
)
.iter()
.collect::<Vec<_>>();
assert_eq!(resources.len(), 5);
assert!(resources.iter().all(|r| r.is_err()));
// hashmap order is not guaranteed so we check the error variant exists and how many
assert!(
resources
.iter()
.any(|r| matches!(r, Err(crate::Error::ResourcePathNotFound(_))))
);
assert_eq!(
resources
.iter()
.filter(|r| matches!(r, Err(crate::Error::ResourcePathNotFound(_))))
.count(),
2
);
assert!(
resources
.iter()
.any(|r| matches!(r, Err(crate::Error::NotAllowedToWalkDir(_))))
);
assert_eq!(
resources
.iter()
.filter(|r| matches!(r, Err(crate::Error::NotAllowedToWalkDir(_))))
.count(),
1
);
assert!(
resources
.iter()
.any(|r| matches!(r, Err(crate::Error::GlobPathNotFound(_))))
);
assert_eq!(
resources
.iter()
.filter(|r| matches!(r, Err(crate::Error::GlobPathNotFound(_))))
.count(),
2
);
}
}

View file

@ -0,0 +1,175 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Utilities to implement [`ToTokens`] for a type.
use std::path::Path;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use serde_json::Value as JsonValue;
use url::Url;
/// Write a `TokenStream` of the `$struct`'s fields to the `$tokens`.
///
/// All fields must represent a binding of the same name that implements `ToTokens`.
#[macro_export]
macro_rules! literal_struct {
($tokens:ident, $struct:path, $($field:ident),+) => {
$tokens.append_all(quote! {
$struct {
$($field: #$field),+
}
})
};
}
/// Create a `String` constructor `TokenStream`.
///
/// e.g. `"Hello World"` -> `String::from("Hello World")`.
/// This takes a `&String` to reduce casting all the `&String` -> `&str` manually.
pub fn str_lit(s: impl AsRef<str>) -> TokenStream {
let s = s.as_ref();
quote! { #s.into() }
}
/// Create an `Option` constructor `TokenStream`.
pub fn opt_lit(item: Option<&impl ToTokens>) -> TokenStream {
match item {
None => quote! { ::core::option::Option::None },
Some(item) => quote! { ::core::option::Option::Some(#item) },
}
}
/// Create an `Option` constructor `TokenStream` over an owned [`ToTokens`] impl type.
pub fn opt_lit_owned(item: Option<impl ToTokens>) -> TokenStream {
match item {
None => quote! { ::core::option::Option::None },
Some(item) => quote! { ::core::option::Option::Some(#item) },
}
}
/// Helper function to combine an `opt_lit` with `str_lit`.
pub fn opt_str_lit(item: Option<impl AsRef<str>>) -> TokenStream {
opt_lit(item.map(str_lit).as_ref())
}
/// Helper function to combine an `opt_lit` with a list of `str_lit`
pub fn opt_vec_lit<Raw, Tokens>(
item: Option<impl IntoIterator<Item = Raw>>,
map: impl Fn(Raw) -> Tokens,
) -> TokenStream
where
Tokens: ToTokens,
{
opt_lit(item.map(|list| vec_lit(list, map)).as_ref())
}
/// Create a `Vec` constructor, mapping items with a function that spits out `TokenStream`s.
pub fn vec_lit<Raw, Tokens>(
list: impl IntoIterator<Item = Raw>,
map: impl Fn(Raw) -> Tokens,
) -> TokenStream
where
Tokens: ToTokens,
{
let items = list.into_iter().map(map);
quote! { vec![#(#items),*] }
}
/// Create a `PathBuf` constructor `TokenStream`.
///
/// e.g. `"Hello World" -> String::from("Hello World").
pub fn path_buf_lit(s: impl AsRef<Path>) -> TokenStream {
let s = s.as_ref().to_string_lossy().into_owned();
quote! { ::std::path::PathBuf::from(#s) }
}
/// Creates a `Url` constructor `TokenStream`.
pub fn url_lit(url: &Url) -> TokenStream {
let url = url.as_str();
quote! { #url.parse().unwrap() }
}
/// Create a map constructor, mapping keys and values with other `TokenStream`s.
///
/// This function is pretty generic because the types of keys AND values get transformed.
pub fn map_lit<Map, Key, Value, TokenStreamKey, TokenStreamValue, FuncKey, FuncValue>(
map_type: TokenStream,
map: Map,
map_key: FuncKey,
map_value: FuncValue,
) -> TokenStream
where
<Map as IntoIterator>::IntoIter: ExactSizeIterator,
Map: IntoIterator<Item = (Key, Value)>,
TokenStreamKey: ToTokens,
TokenStreamValue: ToTokens,
FuncKey: Fn(Key) -> TokenStreamKey,
FuncValue: Fn(Value) -> TokenStreamValue,
{
let ident = quote::format_ident!("map");
let map = map.into_iter();
if map.len() > 0 {
let items = map.map(|(key, value)| {
let key = map_key(key);
let value = map_value(value);
quote! { #ident.insert(#key, #value); }
});
quote! {{
let mut #ident = #map_type::new();
#(#items)*
#ident
}}
} else {
quote! { #map_type::new() }
}
}
/// Create a `serde_json::Value` variant `TokenStream` for a number
pub fn json_value_number_lit(num: &serde_json::Number) -> TokenStream {
// See <https://docs.rs/serde_json/1/serde_json/struct.Number.html> for guarantees
let prefix = quote! { ::serde_json::Value };
if num.is_u64() {
// guaranteed u64
let num = num.as_u64().unwrap();
quote! { #prefix::Number(#num.into()) }
} else if num.is_i64() {
// guaranteed i64
let num = num.as_i64().unwrap();
quote! { #prefix::Number(#num.into()) }
} else if num.is_f64() {
// guaranteed f64
let num = num.as_f64().unwrap();
quote! { #prefix::Number(::serde_json::Number::from_f64(#num).unwrap(/* safe to unwrap, guaranteed f64 */)) }
} else {
// invalid number
quote! { #prefix::Null }
}
}
/// Create a `serde_json::Value` constructor `TokenStream`
pub fn json_value_lit(jv: &JsonValue) -> TokenStream {
let prefix = quote! { ::serde_json::Value };
match jv {
JsonValue::Null => quote! { #prefix::Null },
JsonValue::Bool(bool) => quote! { #prefix::Bool(#bool) },
JsonValue::Number(number) => json_value_number_lit(number),
JsonValue::String(str) => {
let s = str_lit(str);
quote! { #prefix::String(#s) }
}
JsonValue::Array(vec) => {
let items = vec.iter().map(json_value_lit);
quote! { #prefix::Array(vec![#(#items),*]) }
}
JsonValue::Object(map) => {
let map = map_lit(quote! { ::serde_json::Map }, map, str_lit, json_value_lit);
quote! { #prefix::Object(#map) }
}
}
}

View file

@ -13,10 +13,10 @@
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/inter": "^5.2.8",
"@noble/curves": "^2.0.1", "@noble/curves": "^2.2.0",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.4",
"@tanstack/react-router": "^1.0.0", "@tanstack/react-router": "^1.169.1",
"@tanstack/react-virtual": "^3.0.0", "@tanstack/react-virtual": "^3.13.24",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tensamin/call": "workspace:*", "@tensamin/call": "workspace:*",
"@tensamin/chat": "workspace:*", "@tensamin/chat": "workspace:*",
@ -33,17 +33,16 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"comlink": "^4.4.2", "comlink": "^4.4.2",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
"lucide-react": "^0.564.0", "lucide-react": "^1.14.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"shadcn": "^4.0.7", "shadcn": "^4.6.0",
"sonner": "^1.0.0", "sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwind-scrollbar-hide": "^4.0.0", "tailwind-scrollbar-hide": "^4.0.0",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.4",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"vite-tsconfig-paths": "^6.1.1",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
@ -51,11 +50,11 @@
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.0.3", "eslint": "^10.0.3",
"globals": "^17.4.0", "globals": "^17.4.0",
"typescript": "~5.9.3", "typescript": "~6.0.3",
"typescript-eslint": "^8.57.0", "typescript-eslint": "^8.57.0",
"vite": "^7.3.1" "vite": "^8.0.10"
} }
} }

View file

@ -86,7 +86,11 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
<Button <Button
disabled={callState !== "closed"} disabled={callState !== "closed"}
onClick={() => { onClick={() => {
void joinCall(id, currentCalls[0].secret, currentCalls[0].id); void joinCall(
id,
currentCalls[0].call_secret,
currentCalls[0].call_id,
);
}} }}
className="w-9 h-9 aspect-square rounded-lg" className="w-9 h-9 aspect-square rounded-lg"
> >
@ -105,13 +109,13 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
<SelectContent> <SelectContent>
{currentCalls.map((call) => ( {currentCalls.map((call) => (
<SelectItem <SelectItem
value={call.id} value={call.call_id}
key={call.id} key={call.call_id}
onSelect={() => { onSelect={() => {
void joinCall(id, call.secret, call.id); void joinCall(id, call.call_secret, call.call_id);
}} }}
> >
{displayCallId(call.id)} {displayCallId(call.call_id)}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>

View file

@ -50,11 +50,6 @@ export default function Screen(props: { children: React.ReactNode }) {
useEffect(() => { useEffect(() => {
let active = true; let active = true;
setLoading(true);
setError("");
setErrorDescription("");
void (async () => { void (async () => {
try { try {
const id = await load("user_id"); const id = await load("user_id");

View file

@ -5,7 +5,6 @@ import { fileURLToPath } from "node:url";
import { defineConfig, type Plugin } from "vite"; import { defineConfig, type Plugin } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
const host = process.env.TAURI_DEV_HOST; const host = process.env.TAURI_DEV_HOST;
@ -54,6 +53,7 @@ function deepFilterAssetHeaders(rootDir: string): Plugin {
export default defineConfig({ export default defineConfig({
clearScreen: false, clearScreen: false,
resolve: { resolve: {
tsconfigPaths: true,
alias: [ alias: [
{ {
find: "@codemirror/commands", find: "@codemirror/commands",
@ -102,7 +102,6 @@ export default defineConfig({
plugins: [ plugins: [
deepFilterAssetHeaders(resolve(appDir, "public")), deepFilterAssetHeaders(resolve(appDir, "public")),
react(), react(),
tsconfigPaths(),
tailwindcss(), tailwindcss(),
], ],
worker: { worker: {

357
bun.lock
View file

@ -14,14 +14,14 @@
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@typescript-eslint/parser": "^8.58.1", "@typescript-eslint/parser": "^8.59.1",
"eslint": "^10.2.0", "eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.1.1",
"globals": "^17.4.0", "globals": "^17.5.0",
"jsonc-parser": "^3.3.1", "jsonc-parser": "^3.3.1",
"prettier": "^3.8.2", "prettier": "^3.8.3",
"typescript": "^6.0.2", "typescript": "^6.0.3",
"typescript-eslint": "^8.58.1", "typescript-eslint": "^8.59.1",
}, },
}, },
"apps/tauri": { "apps/tauri": {
@ -34,12 +34,12 @@
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",
"@tensamin/ui": "*", "@tensamin/ui": "*",
"lucide-react": "^1.7.0", "lucide-react": "^1.14.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2.11.0",
}, },
}, },
"apps/web": { "apps/web": {
@ -47,10 +47,10 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/inter": "^5.2.8",
"@noble/curves": "^2.0.1", "@noble/curves": "^2.2.0",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.4",
"@tanstack/react-router": "^1.0.0", "@tanstack/react-router": "^1.169.1",
"@tanstack/react-virtual": "^3.0.0", "@tanstack/react-virtual": "^3.13.24",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tensamin/call": "workspace:*", "@tensamin/call": "workspace:*",
"@tensamin/chat": "workspace:*", "@tensamin/chat": "workspace:*",
@ -67,17 +67,16 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"comlink": "^4.4.2", "comlink": "^4.4.2",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
"lucide-react": "^0.564.0", "lucide-react": "^1.14.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"shadcn": "^4.0.7", "shadcn": "^4.6.0",
"sonner": "^1.0.0", "sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwind-scrollbar-hide": "^4.0.0", "tailwind-scrollbar-hide": "^4.0.0",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.4",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"vite-tsconfig-paths": "^6.1.1",
"zod": "^4.3.6", "zod": "^4.3.6",
}, },
"devDependencies": { "devDependencies": {
@ -85,12 +84,12 @@
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.0.3", "eslint": "^10.0.3",
"globals": "^17.4.0", "globals": "^17.4.0",
"typescript": "~5.9.3", "typescript": "~6.0.3",
"typescript-eslint": "^8.57.0", "typescript-eslint": "^8.57.0",
"vite": "^7.3.1", "vite": "^8.0.10",
}, },
}, },
"packages/call": { "packages/call": {
@ -98,9 +97,9 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@livekit/components-react": "^2.9.20", "@livekit/components-react": "^2.9.20",
"@tanstack/react-query": "^5.0.0", "@tanstack/react-query": "^5.100.7",
"@tanstack/react-router": "^1.0.0", "@tanstack/react-router": "^1.169.1",
"@tanstack/react-virtual": "^3.0.0", "@tanstack/react-virtual": "^3.13.24",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
"@tensamin/markdown": "workspace:*", "@tensamin/markdown": "workspace:*",
@ -111,8 +110,8 @@
"@tensamin/ui": "*", "@tensamin/ui": "*",
"@tensamin/user": "workspace:*", "@tensamin/user": "workspace:*",
"deepfilternet3-noise-filter": "^1.2.1", "deepfilternet3-noise-filter": "^1.2.1",
"livekit-client": "^2.18.1", "livekit-client": "^2.18.8",
"lucide-react": "^0.564.0", "lucide-react": "^1.14.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"recharts": "^3.8.1", "recharts": "^3.8.1",
@ -135,7 +134,7 @@
"@tensamin/ttp": "workspace:*", "@tensamin/ttp": "workspace:*",
"@tensamin/ui": "*", "@tensamin/ui": "*",
"@tensamin/user": "workspace:*", "@tensamin/user": "workspace:*",
"lucide-react": "^0.564.0", "lucide-react": "^1.14.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"zod": "^4.3.6", "zod": "^4.3.6",
@ -158,7 +157,7 @@
"@codemirror/commands": "^6.10.2", "@codemirror/commands": "^6.10.2",
"@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-markdown": "^6.5.0",
"@codemirror/state": "^6.5.4", "@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.15", "@codemirror/view": "^6.41.1",
"@tensamin/ui": "*", "@tensamin/ui": "*",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
@ -185,10 +184,10 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@tensamin/ui": "*", "@tensamin/ui": "*",
"lucide-react": "^0.564.0", "lucide-react": "^1.14.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"sonner": "^1.0.0", "sonner": "^2.0.7",
"zod": "^4.3.6", "zod": "^4.3.6",
}, },
}, },
@ -255,7 +254,7 @@
}, },
}, },
"overrides": { "overrides": {
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.13.tar.gz", "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.15.tar.gz",
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.30.tar.gz", "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.30.tar.gz",
}, },
"packages": { "packages": {
@ -307,10 +306,6 @@
"@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="],
"@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="],
@ -347,7 +342,7 @@
"@codemirror/state": ["@codemirror/state@6.6.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ=="], "@codemirror/state": ["@codemirror/state@6.6.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ=="],
"@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="], "@codemirror/view": ["@codemirror/view@6.41.1", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-ToDnWKbBnke+ZLrP6vgTTDScGi5H37YYuZGniQaBzxMVdtCxMrslsmtnOvbPZk4RX9bvkQqnWR/WS/35tJA0qg=="],
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
@ -355,57 +350,11 @@
"@ecies/ciphers": ["@ecies/ciphers@0.2.6", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="], "@ecies/ciphers": ["@ecies/ciphers@0.2.6", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
@ -483,7 +432,7 @@
"@livekit/mutex": ["@livekit/mutex@1.1.1", "", {}, "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw=="], "@livekit/mutex": ["@livekit/mutex@1.1.1", "", {}, "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw=="],
"@livekit/protocol": ["@livekit/protocol@1.44.0", "", { "dependencies": { "@bufbuild/protobuf": "^1.10.0" } }, "sha512-/vfhDUGcUKO8Q43r6i+5FrDhl5oZjm/X3U4x2Iciqvgn5C8qbj+57YPcWSJ1kyIZm5Cm6AV2nAPjMm3ETD/iyg=="], "@livekit/protocol": ["@livekit/protocol@1.45.3", "", { "dependencies": { "@bufbuild/protobuf": "^1.10.0" } }, "sha512-WmMxBTsy4dRBqcrswFwUUlgq3Z0nnhOqKR6tX749Rb/PcB1yBMUtrHxZvcsS6qi3/5+86zHeVG+exmu1sZqfJg=="],
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
@ -491,11 +440,13 @@
"@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="], "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
"@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="], "@noble/curves": ["@noble/curves@2.2.0", "", { "dependencies": { "@noble/hashes": "2.2.0" } }, "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ=="],
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@ -509,6 +460,8 @@
"@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
@ -545,57 +498,37 @@
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="], "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="], "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="], "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="], "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="], "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="], "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="], "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="], "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="], "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="], "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="], "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="], "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="], "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="],
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
@ -607,79 +540,79 @@
"@tabby_ai/hijri-converter": ["@tabby_ai/hijri-converter@1.0.5", "", {}, "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ=="], "@tabby_ai/hijri-converter": ["@tabby_ai/hijri-converter@1.0.5", "", {}, "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], "@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="], "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="], "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="], "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="], "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="], "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="], "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="], "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="], "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="], "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="], "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.4", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="], "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="], "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.2.4", "", { "dependencies": { "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "tailwindcss": "4.2.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw=="],
"@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="], "@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="],
"@tanstack/query-core": ["@tanstack/query-core@5.99.0", "", {}, "sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ=="], "@tanstack/query-core": ["@tanstack/query-core@5.100.7", "", {}, "sha512-5R7i6ENJLhVeeJrrUz7jKBXUXv/BJrxf9FQJSkR13bPrb3zOcE8A0Z0PxYCcsKPOsiIlTibrBL/zZbtUO1TFyQ=="],
"@tanstack/react-query": ["@tanstack/react-query@5.99.0", "", { "dependencies": { "@tanstack/query-core": "5.99.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw=="], "@tanstack/react-query": ["@tanstack/react-query@5.100.7", "", { "dependencies": { "@tanstack/query-core": "5.100.7" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-LoISYWz8dOOuQbeIctF8K6yi42TWtR1WPGpwGuRUpF3u79JVVIg/PVR0MQdIA0VSHqD/ydf/b7PhKTkg3I4fLQ=="],
"@tanstack/react-router": ["@tanstack/react-router@1.168.18", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.14", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-RmBptS3/qtkGhvG/u41JWOgxz1FIWybBz7iBTgLUIoFkqOj6NE4XlhUOsP2fabxACtbZdJnpvCWcJFWpWGIngw=="], "@tanstack/react-router": ["@tanstack/react-router@1.169.1", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.169.1", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-MBtQKSvac3OCcsSa6oBpDrrN90IV47I6Gtv05NxhbFVh+gVjtqvs6HSU4XM9+y5sHZPgS+35eArflX4vM8GEnQ=="],
"@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="],
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.23", "", { "dependencies": { "@tanstack/virtual-core": "3.13.23" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ=="], "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.24", "", { "dependencies": { "@tanstack/virtual-core": "3.14.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg=="],
"@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="], "@tanstack/router-core": ["@tanstack/router-core@1.169.1", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-x+2gIGKTTE1qAn7tLieGfrB5ciOviDmmi2ox9fAWUubRV+yTU5ruGFXocoCIWF+lB+SOtnHjo2E9BLSWyYoEmA=="],
"@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.23", "", {}, "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg=="], "@tanstack/virtual-core": ["@tanstack/virtual-core@3.14.0", "", {}, "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q=="],
"@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="], "@tauri-apps/api": ["@tauri-apps/api@2.11.0", "", {}, "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.10.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.1", "@tauri-apps/cli-darwin-x64": "2.10.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", "@tauri-apps/cli-linux-arm64-musl": "2.10.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-musl": "2.10.1", "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", "@tauri-apps/cli-win32-x64-msvc": "2.10.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g=="], "@tauri-apps/cli": ["@tauri-apps/cli@2.11.0", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.0", "@tauri-apps/cli-darwin-x64": "2.11.0", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.0", "@tauri-apps/cli-linux-arm64-gnu": "2.11.0", "@tauri-apps/cli-linux-arm64-musl": "2.11.0", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.0", "@tauri-apps/cli-linux-x64-gnu": "2.11.0", "@tauri-apps/cli-linux-x64-musl": "2.11.0", "@tauri-apps/cli-win32-arm64-msvc": "2.11.0", "@tauri-apps/cli-win32-ia32-msvc": "2.11.0", "@tauri-apps/cli-win32-x64-msvc": "2.11.0" }, "bin": { "tauri": "tauri.js" } }, "sha512-W5Wbuqsb2pHFPTj4TaRNKTj5rwXhDShPiLSY9T18y4ouSR/NNCptAEFxFsBtyNRgL6Vs1a/q9LzfqqYzEwC+Jw=="],
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.10.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ=="], "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UfMeDNlgIP252rm/KSTuu8yHatPua5TjtUEUf+jyIzVwBNcIl7Ywkdpfj+e5jVVg3EfCTp+4gwuL1dNpgF8clg=="],
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.10.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw=="], "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.11.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-lY1+aPlgyMN7vgjtCdQ3+WODfZkebAcxnrCrO0HjqDpKSXieDkrJbimqeaoM4RwhTSrCLRHfVYiYrfE5E131tg=="],
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.10.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w=="], "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.11.0", "", { "os": "linux", "cpu": "arm" }, "sha512-5uCP0AusgN3NrKC8EpkuJwjek1k8pEffBdugJSpXPey/QGbPEb8vZ542n/giJ2mZPjMSllDkdhG2QIDpBY4PpQ=="],
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA=="], "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.11.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-loDPqtRHMSbIcrH2VBd4GgHoQlF7jJnrZj7MxA2lj1cixS/jEgMAPFqj83U6Wvjete4HfYplbE/gCpSFifA9jw=="],
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg=="], "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.11.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DtSE8ZBlB9H+L+eHkfZ3myt00EVEyAB3e41juEHoE2qT88fgVlJvyrwa9SZYc/xTwCS9TnmK+R84tpg+ZsAg7Q=="],
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.10.1", "", { "os": "linux", "cpu": "none" }, "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw=="], "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.11.0", "", { "os": "linux", "cpu": "none" }, "sha512-5QdgS4LD+kntClI1aj2JmwjW38LosNXxwCe8viIHEwqYIWuMPdNEIau6/cLogI38Yzx9DnfCPRfEWLyI+5li8Q=="],
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw=="], "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.11.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5UynPXo3Zq9khjVdAbD+YogeLltdVUeOah2ioSIM3tu6H7wY9vMy6rgGJhv9r5R8ZXmk9GttMippdqYJWrnLnA=="],
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ=="], "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.11.0", "", { "os": "linux", "cpu": "x64" }, "sha512-CNz7fHbApz1Zyhhq73jtGn9JqgNEV/lIWnTnUo6h6ujw+mHsTmkLszvJSM8W6JBaDjNpTTFr/RSNoVL5FMwcTg=="],
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.10.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg=="], "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.11.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-K+br+VXZ+Xx0n/9FdWohpW5Ugq+2FQUpJScqcPl1hTxXfh3fgjYgt4qA2NgrjlJo+zZPNrmUMl+NLvm0ufEqBQ=="],
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.10.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw=="], "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.11.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-OFV+s3MLZnd75zl0ZAFU5riMpGK4waUEA8ZDuijDsnkU0btz/gHhqh5jVlOn8thyvgdtT3Xyoxqo099MMifH3g=="],
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="], "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.0", "", { "os": "win32", "cpu": "x64" }, "sha512-AeDTWBd2cOZ6TX133BWsoo+LutG9o0JRcgjMsIfLE13ZugpgCMv/2dJbUiBGeRvbPOGin5A3aYmsArPVV6ZSHQ=="],
"@tauri-apps/plugin-barcode-scanner": ["@tauri-apps/plugin-barcode-scanner@2.4.4", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-uXvyMI8UgQjSrGxzTU5isNoQarMGRxFmTmb4TsgiWZHf/g7LsIyAQCwoFShjax0fXCK5mdVKDOvlkfOr21fo6g=="], "@tauri-apps/plugin-barcode-scanner": ["@tauri-apps/plugin-barcode-scanner@2.4.4", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-uXvyMI8UgQjSrGxzTU5isNoQarMGRxFmTmb4TsgiWZHf/g7LsIyAQCwoFShjax0fXCK5mdVKDOvlkfOr21fo6g=="],
@ -707,7 +640,7 @@
"@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"], "@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"],
"@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.13.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.58.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.2", "typescript-eslint": "^8.58.1", "zod": "^4.3.6" } }, "sha512-ib5E8n9IfMzai+ucOXwTNMtu4j6m1RSHANGbFVJxP1VwJDtqIVMqpg/NtTKdRNXQYM1hK5WAScfl6r+aeyaj4g=="], "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.15.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.59.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "zod": "^4.4.1" } }, "sha512-KzDZ/p63dZd5q67oZakWs12rO8Mc2deqnoLM9P+fiDSrly0jNpAiTSWhe5bd5TS/uyoqk5opp2Ge047JS8q/hA=="],
"@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.30.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "@tauri-apps/api": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^1.8.0", "next-themes": "^0.4.6", "react-day-picker": "^9.14.0", "react-resizable-panels": "^4.10.0", "recharts": "3.8.0", "shadcn": "^3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.3.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } }, "sha512-Eh/ko+4x4gF/25JyB38PNN2n/UVUIR6yxxZTpyZPx4maL00992pGmFt62kb1ejJQoooEpEfSo/ALtQDF5cjdoA=="], "@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.30.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "@tauri-apps/api": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^1.8.0", "next-themes": "^0.4.6", "react-day-picker": "^9.14.0", "react-resizable-panels": "^4.10.0", "recharts": "3.8.0", "shadcn": "^3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.3.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } }, "sha512-Eh/ko+4x4gF/25JyB38PNN2n/UVUIR6yxxZTpyZPx4maL00992pGmFt62kb1ejJQoooEpEfSo/ALtQDF5cjdoA=="],
@ -717,13 +650,7 @@
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
@ -765,27 +692,27 @@
"@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/type-utils": "8.58.1", "@typescript-eslint/utils": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/type-utils": "8.59.1", "@typescript-eslint/utils": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.1", "@typescript-eslint/types": "^8.58.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g=="], "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.1", "@typescript-eslint/types": "^8.59.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1" } }, "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.1", "", { "dependencies": { "@typescript-eslint/types": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1" } }, "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw=="], "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1", "@typescript-eslint/utils": "8.58.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.1", "", { "dependencies": { "@typescript-eslint/types": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/utils": "8.59.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.58.1", "", {}, "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.59.1", "", {}, "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.1", "@typescript-eslint/tsconfig-utils": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg=="], "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.1", "@typescript-eslint/tsconfig-utils": "8.59.1", "@typescript-eslint/types": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.1", "", { "dependencies": { "@typescript-eslint/types": "8.59.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
"@webtransport-bun/webtransport": ["@webtransport-bun/webtransport@0.3.0", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-/OX/TCgBD64n/0BjMNytObq5NK2pAM0KOoXBEw49l3sTmbVXNTPP1ZRPIdIyV43agM+mKtZBaV3Er3k3YoB6sw=="], "@webtransport-bun/webtransport": ["@webtransport-bun/webtransport@0.3.0", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-/OX/TCgBD64n/0BjMNytObq5NK2pAM0KOoXBEw49l3sTmbVXNTPP1ZRPIdIyV43agM+mKtZBaV3Er3k3YoB6sw=="],
@ -981,17 +908,15 @@
"es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="], "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
"esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.2.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.4", "@eslint/config-helpers": "^0.5.4", "@eslint/core": "^1.2.0", "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA=="], "eslint": ["eslint@10.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
@ -1093,8 +1018,6 @@
"globals": ["globals@17.5.0", "", {}, "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g=="], "globals": ["globals@17.5.0", "", {}, "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g=="],
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
@ -1235,7 +1158,7 @@
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"livekit-client": ["livekit-client@2.18.1", "", { "dependencies": { "@livekit/mutex": "1.1.1", "@livekit/protocol": "1.44.0", "events": "^3.3.0", "jose": "^6.1.0", "loglevel": "^1.9.2", "sdp-transform": "^2.15.0", "tslib": "2.8.1", "typed-emitter": "^2.1.0", "webrtc-adapter": "^9.0.1" }, "peerDependencies": { "@types/dom-mediacapture-record": "^1" } }, "sha512-nGjuEEV1mVN01EcAMwGIwG3J1gpBMqwn2V4R6W/8zz9Rah1CaAohIm6AMLG7BdctQpyeh34dfAOLfDodIsWyYA=="], "livekit-client": ["livekit-client@2.18.8", "", { "dependencies": { "@livekit/mutex": "1.1.1", "@livekit/protocol": "1.45.3", "events": "^3.3.0", "jose": "^6.1.0", "loglevel": "^1.9.2", "sdp-transform": "^2.15.0", "tslib": "2.8.1", "typed-emitter": "^2.1.0", "webrtc-adapter": "9.0.5" }, "peerDependencies": { "@types/dom-mediacapture-record": "^1" } }, "sha512-E+bSpnBVng/1xG4RfL1Q51dHUpBwL14Wix4sR5bS0djEzKMEtrxcUyhWLltdwQ0USf1t0PaxW6WL4oVb2s4Fsw=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
@ -1247,7 +1170,7 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.564.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg=="], "lucide-react": ["lucide-react@1.14.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
@ -1353,7 +1276,7 @@
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
"postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="], "postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
@ -1361,7 +1284,7 @@
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.8.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q=="], "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
"pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
@ -1391,8 +1314,6 @@
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
@ -1425,7 +1346,7 @@
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
@ -1457,7 +1378,7 @@
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shadcn": ["shadcn@4.2.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-ZDuV340itidaUd4Gi1BxQX+Y7Ush6BHp6URZBM2RyxUUBZ6yFtOWIr4nVY+Ro+YRSpo82v7JrsmtcU5xoBCMJQ=="], "shadcn": ["shadcn@4.6.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-4XeMwFf8ZZxmqQQp+U+Nsq2M+cY4Da8Joo/EaMdHVc4uVuWSTJoeidlZ3gDjyxXCjYB1FLcxYwR4lYQAH8emOg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@ -1507,7 +1428,7 @@
"tailwind-scrollbar-hide": ["tailwind-scrollbar-hide@4.0.0", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20" } }, "sha512-gobtvVcThB2Dxhy0EeYSS1RKQJ5baDFkamkhwBvzvevwX6L4XQfpZ3me9s25Ss1ecFVT5jPYJ50n+7xTBJG9WQ=="], "tailwind-scrollbar-hide": ["tailwind-scrollbar-hide@4.0.0", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20" } }, "sha512-gobtvVcThB2Dxhy0EeYSS1RKQJ5baDFkamkhwBvzvevwX6L4XQfpZ3me9s25Ss1ecFVT5jPYJ50n+7xTBJG9WQ=="],
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], "tailwindcss": ["tailwindcss@4.2.4", "", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="],
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
@ -1531,8 +1452,6 @@
"ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="],
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
"tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@ -1547,9 +1466,9 @@
"typed-emitter": ["typed-emitter@2.1.0", "", { "optionalDependencies": { "rxjs": "*" } }, "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA=="], "typed-emitter": ["typed-emitter@2.1.0", "", { "optionalDependencies": { "rxjs": "*" } }, "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA=="],
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"typescript-eslint": ["typescript-eslint@8.58.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.1", "@typescript-eslint/parser": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1", "@typescript-eslint/utils": "8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg=="], "typescript-eslint": ["typescript-eslint@8.59.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.1", "@typescript-eslint/parser": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/utils": "8.59.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ=="],
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
@ -1583,15 +1502,13 @@
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
"vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], "vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="],
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"webrtc-adapter": ["webrtc-adapter@9.0.4", "", { "dependencies": { "sdp": "^3.2.0" } }, "sha512-5ZZY1+lGq8LEKuDlg9M2RPJHlH3R7OVwyHqMcUsLKCgd9Wvf+QrFTCItkXXYPmrJn8H6gRLXbSgxLLdexiqHxw=="], "webrtc-adapter": ["webrtc-adapter@9.0.5", "", { "dependencies": { "sdp": "^3.2.0" } }, "sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@ -1621,7 +1538,7 @@
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
@ -1635,6 +1552,20 @@
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@codemirror/autocomplete/@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="],
"@codemirror/commands/@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="],
"@codemirror/lang-html/@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="],
"@codemirror/lang-javascript/@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="],
"@codemirror/lang-markdown/@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="],
"@codemirror/language/@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="],
"@codemirror/lint/@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="],
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
@ -1649,6 +1580,8 @@
"@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@ -1661,25 +1594,25 @@
"@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tensamin/notifications/@tauri-apps/api": ["@tauri-apps/api@2.11.0", "", {}, "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA=="], "@tauri-apps/plugin-barcode-scanner/@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
"@tensamin/shared/sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="], "@tauri-apps/plugin-deep-link/@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
"@tensamin/tauri/lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="], "@tauri-apps/plugin-opener/@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
"@tensamin/tauth/lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="], "@tensamin/ui/@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
"@tensamin/ui/lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="], "@tensamin/ui/lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="],
@ -1687,10 +1620,6 @@
"@tensamin/ui/shadcn": ["shadcn@3.8.5", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA=="], "@tensamin/ui/shadcn": ["shadcn@3.8.5", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA=="],
"@tensamin/web/sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="],
"@tensamin/web/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
@ -1719,6 +1648,8 @@
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="],
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
@ -1745,6 +1676,8 @@
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@tensamin/ui/shadcn/postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="],
"@tensamin/ui/shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@tensamin/ui/shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],

View file

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) Alec Larson Copyright (c) 2017 - Present Tauri Apps Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 - Present Tauri Apps Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -29,13 +29,13 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/@codemirror_state@6.6.0` - Folder: `licenses/@codemirror_state@6.6.0`
- Source package dir: `packages/markdown/node_modules/@codemirror/state` - Source package dir: `packages/markdown/node_modules/@codemirror/state`
## @codemirror/view@6.41.0 ## @codemirror/view@6.41.1
- License: MIT - License: MIT
- Repository: git+https://github.com/codemirror/view.git - Repository: git+https://code.haverbeke.berlin/codemirror/view.git
- Description: DOM view component for the CodeMirror code editor - Description: DOM view component for the CodeMirror code editor
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/@codemirror_view@6.41.0` - Folder: `licenses/@codemirror_view@6.41.1`
- Source package dir: `packages/markdown/node_modules/@codemirror/view` - Source package dir: `packages/markdown/node_modules/@codemirror/view`
## @eslint/js@10.0.1 ## @eslint/js@10.0.1
@ -66,74 +66,74 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/@livekit_components-react@2.9.20` - Folder: `licenses/@livekit_components-react@2.9.20`
- Source package dir: `packages/call/node_modules/@livekit/components-react` - Source package dir: `packages/call/node_modules/@livekit/components-react`
## @noble/curves@2.0.1 ## @noble/curves@2.2.0
- License: MIT - License: MIT
- Homepage: https://paulmillr.com/noble/ - Homepage: https://paulmillr.com/noble/
- Repository: git+https://github.com/paulmillr/noble-curves.git - Repository: git+https://github.com/paulmillr/noble-curves.git
- Description: Audited & minimal JS implementation of elliptic curve cryptography - Description: Audited & minimal JS implementation of elliptic curve cryptography
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/@noble_curves@2.0.1` - Folder: `licenses/@noble_curves@2.2.0`
- Source package dir: `apps/web/node_modules/@noble/curves` - Source package dir: `apps/web/node_modules/@noble/curves`
## @tailwindcss/vite@4.2.2 ## @tailwindcss/vite@4.2.4
- License: MIT - License: MIT
- Homepage: https://tailwindcss.com - Homepage: https://tailwindcss.com
- Repository: https://github.com/tailwindlabs/tailwindcss.git - Repository: https://github.com/tailwindlabs/tailwindcss.git
- Description: A utility-first CSS framework for rapidly building custom user interfaces. - Description: A utility-first CSS framework for rapidly building custom user interfaces.
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/@tailwindcss_vite@4.2.2` - Folder: `licenses/@tailwindcss_vite@4.2.4`
- Source package dir: `apps/web/node_modules/@tailwindcss/vite` - Source package dir: `apps/web/node_modules/@tailwindcss/vite`
## @tanstack/react-query@5.99.0 ## @tanstack/react-query@5.100.7
- License: MIT - License: MIT
- Homepage: https://tanstack.com/query - Homepage: https://tanstack.com/query
- Repository: git+https://github.com/TanStack/query.git - Repository: git+https://github.com/TanStack/query.git
- Description: Hooks for managing, caching and syncing asynchronous and remote data in React - Description: Hooks for managing, caching and syncing asynchronous and remote data in React
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/@tanstack_react-query@5.99.0` - Folder: `licenses/@tanstack_react-query@5.100.7`
- Source package dir: `packages/call/node_modules/@tanstack/react-query` - Source package dir: `packages/call/node_modules/@tanstack/react-query`
## @tanstack/react-router@1.168.18 ## @tanstack/react-router@1.169.1
- License: MIT - License: MIT
- Homepage: https://tanstack.com/router - Homepage: https://tanstack.com/router
- Repository: git+https://github.com/TanStack/router.git - Repository: git+https://github.com/TanStack/router.git
- Description: Modern and scalable routing for React applications - Description: Modern and scalable routing for React applications
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/@tanstack_react-router@1.168.18` - Folder: `licenses/@tanstack_react-router@1.169.1`
- Source package dir: `apps/web/node_modules/@tanstack/react-router` - Source package dir: `apps/web/node_modules/@tanstack/react-router`
## @tanstack/react-virtual@3.13.23 ## @tanstack/react-virtual@3.13.24
- License: MIT - License: MIT
- Homepage: https://tanstack.com/virtual - Homepage: https://tanstack.com/virtual
- Repository: git+https://github.com/TanStack/virtual.git - Repository: git+https://github.com/TanStack/virtual.git
- Description: Headless UI for virtualizing scrollable elements in React - Description: Headless UI for virtualizing scrollable elements in React
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/@tanstack_react-virtual@3.13.23` - Folder: `licenses/@tanstack_react-virtual@3.13.24`
- Source package dir: `apps/web/node_modules/@tanstack/react-virtual` - Source package dir: `apps/web/node_modules/@tanstack/react-virtual`
## @tauri-apps/api@2.10.1 ## @tauri-apps/api@2.11.0
- License: Apache-2.0 OR MIT - License: Apache-2.0 OR MIT
- Homepage: https://github.com/tauri-apps/tauri#readme - Homepage: https://github.com/tauri-apps/tauri#readme
- Repository: git+https://github.com/tauri-apps/tauri.git - Repository: git+https://github.com/tauri-apps/tauri.git
- Description: Tauri API definitions - Description: Tauri API definitions
- Included files: LICENSE_MIT, LICENSE_APACHE-2.0 - Included files: LICENSE_MIT, LICENSE_APACHE-2.0
- Folder: `licenses/@tauri-apps_api@2.10.1` - Folder: `licenses/@tauri-apps_api@2.11.0`
- Source package dir: `apps/tauri/node_modules/@tauri-apps/api` - Source package dir: `apps/tauri/node_modules/@tauri-apps/api`
## @tauri-apps/cli@2.10.1 ## @tauri-apps/cli@2.11.0
- License: Apache-2.0 OR MIT - License: Apache-2.0 OR MIT
- Homepage: https://github.com/tauri-apps/tauri#readme - Homepage: https://github.com/tauri-apps/tauri#readme
- Repository: git+https://github.com/tauri-apps/tauri.git - Repository: git+https://github.com/tauri-apps/tauri.git
- Description: Command line interface for building Tauri apps - Description: Command line interface for building Tauri apps
- Included files: LICENSE_MIT, LICENSE_APACHE-2.0 - Included files: LICENSE_MIT, LICENSE_APACHE-2.0
- Folder: `licenses/@tauri-apps_cli@2.10.1` - Folder: `licenses/@tauri-apps_cli@2.11.0`
- Source package dir: `apps/tauri/node_modules/@tauri-apps/cli` - Source package dir: `apps/tauri/node_modules/@tauri-apps/cli`
## @tauri-apps/plugin-barcode-scanner@2.4.4 ## @tauri-apps/plugin-barcode-scanner@2.4.4
@ -163,11 +163,11 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/@tauri-apps_plugin-opener@2.5.3` - Folder: `licenses/@tauri-apps_plugin-opener@2.5.3`
- Source package dir: `apps/tauri/node_modules/@tauri-apps/plugin-opener` - Source package dir: `apps/tauri/node_modules/@tauri-apps/plugin-opener`
## @tensamin/ttp-core@0.0.13 ## @tensamin/ttp-core@0.0.15
- License: UNKNOWN - License: UNKNOWN
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/@tensamin_ttp-core@0.0.13` - Folder: `licenses/@tensamin_ttp-core@0.0.15`
- Source package dir: `packages/ttp/node_modules/@tensamin/ttp-core` - Source package dir: `packages/ttp/node_modules/@tensamin/ttp-core`
## @tensamin/ui@0.0.30 ## @tensamin/ui@0.0.30
@ -217,24 +217,24 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/@types_react-dom@19.2.3` - Folder: `licenses/@types_react-dom@19.2.3`
- Source package dir: `apps/web/node_modules/@types/react-dom` - Source package dir: `apps/web/node_modules/@types/react-dom`
## @typescript-eslint/parser@8.58.1 ## @typescript-eslint/parser@8.59.1
- License: MIT - License: MIT
- Homepage: https://typescript-eslint.io/packages/parser - Homepage: https://typescript-eslint.io/packages/parser
- Repository: https://github.com/typescript-eslint/typescript-eslint.git - Repository: https://github.com/typescript-eslint/typescript-eslint.git
- Description: An ESLint custom parser which leverages TypeScript ESTree - Description: An ESLint custom parser which leverages TypeScript ESTree
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/@typescript-eslint_parser@8.58.1` - Folder: `licenses/@typescript-eslint_parser@8.59.1`
- Source package dir: `node_modules/@typescript-eslint/parser` - Source package dir: `node_modules/@typescript-eslint/parser`
## @vitejs/plugin-react@5.2.0 ## @vitejs/plugin-react@6.0.1
- License: MIT - License: MIT
- Homepage: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme - Homepage: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme
- Repository: git+https://github.com/vitejs/vite-plugin-react.git - Repository: git+https://github.com/vitejs/vite-plugin-react.git
- Description: The default Vite plugin for React projects - Description: The default Vite plugin for React projects
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/@vitejs_plugin-react@5.2.0` - Folder: `licenses/@vitejs_plugin-react@6.0.1`
- Source package dir: `apps/web/node_modules/@vitejs/plugin-react` - Source package dir: `apps/web/node_modules/@vitejs/plugin-react`
## class-variance-authority@0.7.1 ## class-variance-authority@0.7.1
@ -275,24 +275,24 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/deepfilternet3-noise-filter@1.2.1` - Folder: `licenses/deepfilternet3-noise-filter@1.2.1`
- Source package dir: `packages/call/node_modules/deepfilternet3-noise-filter` - Source package dir: `packages/call/node_modules/deepfilternet3-noise-filter`
## eslint@10.2.0 ## eslint@10.2.1
- License: MIT - License: MIT
- Homepage: https://eslint.org - Homepage: https://eslint.org
- Repository: eslint/eslint - Repository: eslint/eslint
- Description: An AST-based pattern checker for JavaScript. - Description: An AST-based pattern checker for JavaScript.
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/eslint@10.2.0` - Folder: `licenses/eslint@10.2.1`
- Source package dir: `apps/web/node_modules/eslint` - Source package dir: `apps/web/node_modules/eslint`
## eslint-plugin-react-hooks@7.0.1 ## eslint-plugin-react-hooks@7.1.1
- License: MIT - License: MIT
- Homepage: https://react.dev/ - Homepage: https://react.dev/
- Repository: https://github.com/facebook/react.git - Repository: https://github.com/facebook/react.git
- Description: ESLint rules for React Hooks - Description: ESLint rules for React Hooks
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/eslint-plugin-react-hooks@7.0.1` - Folder: `licenses/eslint-plugin-react-hooks@7.1.1`
- Source package dir: `node_modules/eslint-plugin-react-hooks` - Source package dir: `node_modules/eslint-plugin-react-hooks`
## framer-motion@12.38.0 ## framer-motion@12.38.0
@ -322,33 +322,33 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/jsonc-parser@3.3.1` - Folder: `licenses/jsonc-parser@3.3.1`
- Source package dir: `node_modules/jsonc-parser` - Source package dir: `node_modules/jsonc-parser`
## livekit-client@2.18.1 ## livekit-client@2.18.8
- License: Apache-2.0 - License: Apache-2.0
- Repository: git@github.com:livekit/client-sdk-js.git - Repository: git@github.com:livekit/client-sdk-js.git
- Description: JavaScript/TypeScript client SDK for LiveKit - Description: JavaScript/TypeScript client SDK for LiveKit
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/livekit-client@2.18.1` - Folder: `licenses/livekit-client@2.18.8`
- Source package dir: `packages/call/node_modules/livekit-client` - Source package dir: `packages/call/node_modules/livekit-client`
## lucide-react@1.8.0 ## lucide-react@1.14.0
- License: ISC - License: ISC
- Homepage: https://lucide.dev - Homepage: https://lucide.dev
- Repository: https://github.com/lucide-icons/lucide.git - Repository: https://github.com/lucide-icons/lucide.git
- Description: A Lucide icon library package for React applications. - Description: A Lucide icon library package for React applications.
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/lucide-react@1.8.0` - Folder: `licenses/lucide-react@1.14.0`
- Source package dir: `apps/tauri/node_modules/lucide-react` - Source package dir: `apps/tauri/node_modules/lucide-react`
## prettier@3.8.2 ## prettier@3.8.3
- License: MIT - License: MIT
- Homepage: https://prettier.io - Homepage: https://prettier.io
- Repository: prettier/prettier - Repository: prettier/prettier
- Description: Prettier is an opinionated code formatter - Description: Prettier is an opinionated code formatter
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/prettier@3.8.2` - Folder: `licenses/prettier@3.8.3`
- Source package dir: `node_modules/prettier` - Source package dir: `node_modules/prettier`
## qrcode@1.5.4 ## qrcode@1.5.4
@ -391,23 +391,23 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/recharts@3.8.1` - Folder: `licenses/recharts@3.8.1`
- Source package dir: `packages/call/node_modules/recharts` - Source package dir: `packages/call/node_modules/recharts`
## shadcn@4.2.0 ## shadcn@4.6.0
- License: MIT - License: MIT
- Repository: https://github.com/shadcn-ui/ui.git - Repository: https://github.com/shadcn-ui/ui.git
- Description: Add components to your apps. - Description: Add components to your apps.
- Included files: LICENSE.md - Included files: LICENSE.md
- Folder: `licenses/shadcn@4.2.0` - Folder: `licenses/shadcn@4.6.0`
- Source package dir: `apps/web/node_modules/shadcn` - Source package dir: `apps/web/node_modules/shadcn`
## sonner@1.7.4 ## sonner@2.0.7
- License: MIT - License: MIT
- Homepage: https://sonner.emilkowal.ski/ - Homepage: https://sonner.emilkowal.ski/
- Repository: git+https://github.com/emilkowalski/sonner.git - Repository: git+https://github.com/emilkowalski/sonner.git
- Description: An opinionated toast component for React. - Description: An opinionated toast component for React.
- Included files: LICENSE.md - Included files: LICENSE.md
- Folder: `licenses/sonner@1.7.4` - Folder: `licenses/sonner@2.0.7`
- Source package dir: `apps/web/node_modules/sonner` - Source package dir: `apps/web/node_modules/sonner`
## tailwind-merge@3.5.0 ## tailwind-merge@3.5.0
@ -430,14 +430,14 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/tailwind-scrollbar-hide@4.0.0` - Folder: `licenses/tailwind-scrollbar-hide@4.0.0`
- Source package dir: `apps/web/node_modules/tailwind-scrollbar-hide` - Source package dir: `apps/web/node_modules/tailwind-scrollbar-hide`
## tailwindcss@4.2.2 ## tailwindcss@4.2.4
- License: MIT - License: MIT
- Homepage: https://tailwindcss.com - Homepage: https://tailwindcss.com
- Repository: https://github.com/tailwindlabs/tailwindcss.git - Repository: https://github.com/tailwindlabs/tailwindcss.git
- Description: A utility-first CSS framework for rapidly building custom user interfaces. - Description: A utility-first CSS framework for rapidly building custom user interfaces.
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/tailwindcss@4.2.2` - Folder: `licenses/tailwindcss@4.2.4`
- Source package dir: `apps/web/node_modules/tailwindcss` - Source package dir: `apps/web/node_modules/tailwindcss`
## tw-animate-css@1.4.0 ## tw-animate-css@1.4.0
@ -450,53 +450,44 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/tw-animate-css@1.4.0` - Folder: `licenses/tw-animate-css@1.4.0`
- Source package dir: `apps/web/node_modules/tw-animate-css` - Source package dir: `apps/web/node_modules/tw-animate-css`
## typescript@5.9.3 ## typescript@6.0.3
- License: Apache-2.0 - License: Apache-2.0
- Homepage: https://www.typescriptlang.org/ - Homepage: https://www.typescriptlang.org/
- Repository: https://github.com/microsoft/TypeScript.git - Repository: https://github.com/microsoft/TypeScript.git
- Description: TypeScript is a language for application scale JavaScript development - Description: TypeScript is a language for application scale JavaScript development
- Included files: LICENSE.txt - Included files: LICENSE.txt
- Folder: `licenses/typescript@5.9.3` - Folder: `licenses/typescript@6.0.3`
- Source package dir: `apps/web/node_modules/typescript` - Source package dir: `apps/web/node_modules/typescript`
## typescript-eslint@8.58.1 ## typescript-eslint@8.59.1
- License: MIT - License: MIT
- Homepage: https://typescript-eslint.io/packages/typescript-eslint - Homepage: https://typescript-eslint.io/packages/typescript-eslint
- Repository: https://github.com/typescript-eslint/typescript-eslint.git - Repository: https://github.com/typescript-eslint/typescript-eslint.git
- Description: Tooling which enables you to use TypeScript with ESLint - Description: Tooling which enables you to use TypeScript with ESLint
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/typescript-eslint@8.58.1` - Folder: `licenses/typescript-eslint@8.59.1`
- Source package dir: `apps/web/node_modules/typescript-eslint` - Source package dir: `apps/web/node_modules/typescript-eslint`
## vite@7.3.2 ## vite@8.0.10
- License: MIT - License: MIT
- Homepage: https://vite.dev - Homepage: https://vite.dev
- Repository: git+https://github.com/vitejs/vite.git - Repository: git+https://github.com/vitejs/vite.git
- Description: Native-ESM powered web dev build tool - Description: Native-ESM powered web dev build tool
- Included files: LICENSE.md - Included files: LICENSE.md
- Folder: `licenses/vite@7.3.2` - Folder: `licenses/vite@8.0.10`
- Source package dir: `apps/web/node_modules/vite` - Source package dir: `apps/web/node_modules/vite`
## vite-tsconfig-paths@6.1.1 ## zod@4.4.1
- License: MIT
- Repository: aleclarson/vite-tsconfig-paths
- Description: Vite resolver for TypeScript compilerOptions.paths
- Included files: LICENSE
- Folder: `licenses/vite-tsconfig-paths@6.1.1`
- Source package dir: `apps/web/node_modules/vite-tsconfig-paths`
## zod@4.3.6
- License: MIT - License: MIT
- Homepage: https://zod.dev - Homepage: https://zod.dev
- Repository: git+https://github.com/colinhacks/zod.git - Repository: git+https://github.com/colinhacks/zod.git
- Description: TypeScript-first schema declaration and validation library with static type inference - Description: TypeScript-first schema declaration and validation library with static type inference
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/zod@4.3.6` - Folder: `licenses/zod@4.4.1`
- Source package dir: `apps/web/node_modules/zod` - Source package dir: `apps/web/node_modules/zod`
## zustand@5.0.12 ## zustand@5.0.12

View file

@ -3,7 +3,7 @@
"specVersion": "1.5", "specVersion": "1.5",
"version": 1, "version": 1,
"metadata": { "metadata": {
"timestamp": "2026-05-01T13:23:05.336Z", "timestamp": "2026-05-01T17:38:04.763Z",
"tools": [ "tools": [
{ {
"vendor": "OpenAI", "vendor": "OpenAI",
@ -111,10 +111,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40codemirror/view@6.41.0", "bomRef": "pkg:npm/%40codemirror/view@6.41.1",
"name": "@codemirror/view", "name": "@codemirror/view",
"version": "6.41.0", "version": "6.41.1",
"purl": "pkg:npm/%40codemirror/view@6.41.0", "purl": "pkg:npm/%40codemirror/view@6.41.1",
"description": "DOM view component for the CodeMirror code editor", "description": "DOM view component for the CodeMirror code editor",
"licenses": [ "licenses": [
{ {
@ -126,13 +126,13 @@
"externalReferences": [ "externalReferences": [
{ {
"type": "vcs", "type": "vcs",
"url": "git+https://github.com/codemirror/view.git" "url": "git+https://code.haverbeke.berlin/codemirror/view.git"
} }
], ],
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@codemirror_view@6.41.0" "value": "licenses/@codemirror_view@6.41.1"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -242,10 +242,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40noble/curves@2.0.1", "bomRef": "pkg:npm/%40noble/curves@2.2.0",
"name": "@noble/curves", "name": "@noble/curves",
"version": "2.0.1", "version": "2.2.0",
"purl": "pkg:npm/%40noble/curves@2.0.1", "purl": "pkg:npm/%40noble/curves@2.2.0",
"description": "Audited & minimal JS implementation of elliptic curve cryptography", "description": "Audited & minimal JS implementation of elliptic curve cryptography",
"licenses": [ "licenses": [
{ {
@ -267,7 +267,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@noble_curves@2.0.1" "value": "licenses/@noble_curves@2.2.0"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -277,10 +277,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40tailwindcss/vite@4.2.2", "bomRef": "pkg:npm/%40tailwindcss/vite@4.2.4",
"name": "@tailwindcss/vite", "name": "@tailwindcss/vite",
"version": "4.2.2", "version": "4.2.4",
"purl": "pkg:npm/%40tailwindcss/vite@4.2.2", "purl": "pkg:npm/%40tailwindcss/vite@4.2.4",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.", "description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"licenses": [ "licenses": [
{ {
@ -302,7 +302,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@tailwindcss_vite@4.2.2" "value": "licenses/@tailwindcss_vite@4.2.4"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -312,10 +312,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40tanstack/react-query@5.99.0", "bomRef": "pkg:npm/%40tanstack/react-query@5.100.7",
"name": "@tanstack/react-query", "name": "@tanstack/react-query",
"version": "5.99.0", "version": "5.100.7",
"purl": "pkg:npm/%40tanstack/react-query@5.99.0", "purl": "pkg:npm/%40tanstack/react-query@5.100.7",
"description": "Hooks for managing, caching and syncing asynchronous and remote data in React", "description": "Hooks for managing, caching and syncing asynchronous and remote data in React",
"licenses": [ "licenses": [
{ {
@ -337,7 +337,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@tanstack_react-query@5.99.0" "value": "licenses/@tanstack_react-query@5.100.7"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -347,10 +347,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40tanstack/react-router@1.168.18", "bomRef": "pkg:npm/%40tanstack/react-router@1.169.1",
"name": "@tanstack/react-router", "name": "@tanstack/react-router",
"version": "1.168.18", "version": "1.169.1",
"purl": "pkg:npm/%40tanstack/react-router@1.168.18", "purl": "pkg:npm/%40tanstack/react-router@1.169.1",
"description": "Modern and scalable routing for React applications", "description": "Modern and scalable routing for React applications",
"licenses": [ "licenses": [
{ {
@ -372,7 +372,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@tanstack_react-router@1.168.18" "value": "licenses/@tanstack_react-router@1.169.1"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -382,10 +382,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40tanstack/react-virtual@3.13.23", "bomRef": "pkg:npm/%40tanstack/react-virtual@3.13.24",
"name": "@tanstack/react-virtual", "name": "@tanstack/react-virtual",
"version": "3.13.23", "version": "3.13.24",
"purl": "pkg:npm/%40tanstack/react-virtual@3.13.23", "purl": "pkg:npm/%40tanstack/react-virtual@3.13.24",
"description": "Headless UI for virtualizing scrollable elements in React", "description": "Headless UI for virtualizing scrollable elements in React",
"licenses": [ "licenses": [
{ {
@ -407,7 +407,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@tanstack_react-virtual@3.13.23" "value": "licenses/@tanstack_react-virtual@3.13.24"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -417,10 +417,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40tauri-apps/api@2.10.1", "bomRef": "pkg:npm/%40tauri-apps/api@2.11.0",
"name": "@tauri-apps/api", "name": "@tauri-apps/api",
"version": "2.10.1", "version": "2.11.0",
"purl": "pkg:npm/%40tauri-apps/api@2.10.1", "purl": "pkg:npm/%40tauri-apps/api@2.11.0",
"description": "Tauri API definitions", "description": "Tauri API definitions",
"licenses": [ "licenses": [
{ {
@ -442,7 +442,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@tauri-apps_api@2.10.1" "value": "licenses/@tauri-apps_api@2.11.0"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -452,10 +452,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40tauri-apps/cli@2.10.1", "bomRef": "pkg:npm/%40tauri-apps/cli@2.11.0",
"name": "@tauri-apps/cli", "name": "@tauri-apps/cli",
"version": "2.10.1", "version": "2.11.0",
"purl": "pkg:npm/%40tauri-apps/cli@2.10.1", "purl": "pkg:npm/%40tauri-apps/cli@2.11.0",
"description": "Command line interface for building Tauri apps", "description": "Command line interface for building Tauri apps",
"licenses": [ "licenses": [
{ {
@ -477,7 +477,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@tauri-apps_cli@2.10.1" "value": "licenses/@tauri-apps_cli@2.11.0"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -580,15 +580,15 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40tensamin/ttp-core@0.0.13", "bomRef": "pkg:npm/%40tensamin/ttp-core@0.0.15",
"name": "@tensamin/ttp-core", "name": "@tensamin/ttp-core",
"version": "0.0.13", "version": "0.0.15",
"purl": "pkg:npm/%40tensamin/ttp-core@0.0.13", "purl": "pkg:npm/%40tensamin/ttp-core@0.0.15",
"externalReferences": [], "externalReferences": [],
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@tensamin_ttp-core@0.0.13" "value": "licenses/@tensamin_ttp-core@0.0.15"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -756,10 +756,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40typescript-eslint/parser@8.58.1", "bomRef": "pkg:npm/%40typescript-eslint/parser@8.59.1",
"name": "@typescript-eslint/parser", "name": "@typescript-eslint/parser",
"version": "8.58.1", "version": "8.59.1",
"purl": "pkg:npm/%40typescript-eslint/parser@8.58.1", "purl": "pkg:npm/%40typescript-eslint/parser@8.59.1",
"description": "An ESLint custom parser which leverages TypeScript ESTree", "description": "An ESLint custom parser which leverages TypeScript ESTree",
"licenses": [ "licenses": [
{ {
@ -781,7 +781,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@typescript-eslint_parser@8.58.1" "value": "licenses/@typescript-eslint_parser@8.59.1"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -791,10 +791,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40vitejs/plugin-react@5.2.0", "bomRef": "pkg:npm/%40vitejs/plugin-react@6.0.1",
"name": "@vitejs/plugin-react", "name": "@vitejs/plugin-react",
"version": "5.2.0", "version": "6.0.1",
"purl": "pkg:npm/%40vitejs/plugin-react@5.2.0", "purl": "pkg:npm/%40vitejs/plugin-react@6.0.1",
"description": "The default Vite plugin for React projects", "description": "The default Vite plugin for React projects",
"licenses": [ "licenses": [
{ {
@ -816,7 +816,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@vitejs_plugin-react@5.2.0" "value": "licenses/@vitejs_plugin-react@6.0.1"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -958,10 +958,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/eslint@10.2.0", "bomRef": "pkg:npm/eslint@10.2.1",
"name": "eslint", "name": "eslint",
"version": "10.2.0", "version": "10.2.1",
"purl": "pkg:npm/eslint@10.2.0", "purl": "pkg:npm/eslint@10.2.1",
"description": "An AST-based pattern checker for JavaScript.", "description": "An AST-based pattern checker for JavaScript.",
"licenses": [ "licenses": [
{ {
@ -983,7 +983,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/eslint@10.2.0" "value": "licenses/eslint@10.2.1"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -993,10 +993,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/eslint-plugin-react-hooks@7.0.1", "bomRef": "pkg:npm/eslint-plugin-react-hooks@7.1.1",
"name": "eslint-plugin-react-hooks", "name": "eslint-plugin-react-hooks",
"version": "7.0.1", "version": "7.1.1",
"purl": "pkg:npm/eslint-plugin-react-hooks@7.0.1", "purl": "pkg:npm/eslint-plugin-react-hooks@7.1.1",
"description": "ESLint rules for React Hooks", "description": "ESLint rules for React Hooks",
"licenses": [ "licenses": [
{ {
@ -1018,7 +1018,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/eslint-plugin-react-hooks@7.0.1" "value": "licenses/eslint-plugin-react-hooks@7.1.1"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -1121,10 +1121,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/livekit-client@2.18.1", "bomRef": "pkg:npm/livekit-client@2.18.8",
"name": "livekit-client", "name": "livekit-client",
"version": "2.18.1", "version": "2.18.8",
"purl": "pkg:npm/livekit-client@2.18.1", "purl": "pkg:npm/livekit-client@2.18.8",
"description": "JavaScript/TypeScript client SDK for LiveKit", "description": "JavaScript/TypeScript client SDK for LiveKit",
"licenses": [ "licenses": [
{ {
@ -1142,7 +1142,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/livekit-client@2.18.1" "value": "licenses/livekit-client@2.18.8"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -1152,10 +1152,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/lucide-react@1.8.0", "bomRef": "pkg:npm/lucide-react@1.14.0",
"name": "lucide-react", "name": "lucide-react",
"version": "1.8.0", "version": "1.14.0",
"purl": "pkg:npm/lucide-react@1.8.0", "purl": "pkg:npm/lucide-react@1.14.0",
"description": "A Lucide icon library package for React applications.", "description": "A Lucide icon library package for React applications.",
"licenses": [ "licenses": [
{ {
@ -1177,7 +1177,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/lucide-react@1.8.0" "value": "licenses/lucide-react@1.14.0"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -1187,10 +1187,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/prettier@3.8.2", "bomRef": "pkg:npm/prettier@3.8.3",
"name": "prettier", "name": "prettier",
"version": "3.8.2", "version": "3.8.3",
"purl": "pkg:npm/prettier@3.8.2", "purl": "pkg:npm/prettier@3.8.3",
"description": "Prettier is an opinionated code formatter", "description": "Prettier is an opinionated code formatter",
"licenses": [ "licenses": [
{ {
@ -1212,7 +1212,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/prettier@3.8.2" "value": "licenses/prettier@3.8.3"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -1362,10 +1362,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/shadcn@4.2.0", "bomRef": "pkg:npm/shadcn@4.6.0",
"name": "shadcn", "name": "shadcn",
"version": "4.2.0", "version": "4.6.0",
"purl": "pkg:npm/shadcn@4.2.0", "purl": "pkg:npm/shadcn@4.6.0",
"description": "Add components to your apps.", "description": "Add components to your apps.",
"licenses": [ "licenses": [
{ {
@ -1383,7 +1383,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/shadcn@4.2.0" "value": "licenses/shadcn@4.6.0"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -1393,10 +1393,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/sonner@1.7.4", "bomRef": "pkg:npm/sonner@2.0.7",
"name": "sonner", "name": "sonner",
"version": "1.7.4", "version": "2.0.7",
"purl": "pkg:npm/sonner@1.7.4", "purl": "pkg:npm/sonner@2.0.7",
"description": "An opinionated toast component for React.", "description": "An opinionated toast component for React.",
"licenses": [ "licenses": [
{ {
@ -1418,7 +1418,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/sonner@1.7.4" "value": "licenses/sonner@2.0.7"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -1498,10 +1498,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/tailwindcss@4.2.2", "bomRef": "pkg:npm/tailwindcss@4.2.4",
"name": "tailwindcss", "name": "tailwindcss",
"version": "4.2.2", "version": "4.2.4",
"purl": "pkg:npm/tailwindcss@4.2.2", "purl": "pkg:npm/tailwindcss@4.2.4",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.", "description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"licenses": [ "licenses": [
{ {
@ -1523,7 +1523,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/tailwindcss@4.2.2" "value": "licenses/tailwindcss@4.2.4"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -1568,10 +1568,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/typescript@5.9.3", "bomRef": "pkg:npm/typescript@6.0.3",
"name": "typescript", "name": "typescript",
"version": "5.9.3", "version": "6.0.3",
"purl": "pkg:npm/typescript@5.9.3", "purl": "pkg:npm/typescript@6.0.3",
"description": "TypeScript is a language for application scale JavaScript development", "description": "TypeScript is a language for application scale JavaScript development",
"licenses": [ "licenses": [
{ {
@ -1593,7 +1593,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/typescript@5.9.3" "value": "licenses/typescript@6.0.3"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -1603,10 +1603,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/typescript-eslint@8.58.1", "bomRef": "pkg:npm/typescript-eslint@8.59.1",
"name": "typescript-eslint", "name": "typescript-eslint",
"version": "8.58.1", "version": "8.59.1",
"purl": "pkg:npm/typescript-eslint@8.58.1", "purl": "pkg:npm/typescript-eslint@8.59.1",
"description": "Tooling which enables you to use TypeScript with ESLint", "description": "Tooling which enables you to use TypeScript with ESLint",
"licenses": [ "licenses": [
{ {
@ -1628,7 +1628,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/typescript-eslint@8.58.1" "value": "licenses/typescript-eslint@8.59.1"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -1638,10 +1638,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/vite@7.3.2", "bomRef": "pkg:npm/vite@8.0.10",
"name": "vite", "name": "vite",
"version": "7.3.2", "version": "8.0.10",
"purl": "pkg:npm/vite@7.3.2", "purl": "pkg:npm/vite@8.0.10",
"description": "Native-ESM powered web dev build tool", "description": "Native-ESM powered web dev build tool",
"licenses": [ "licenses": [
{ {
@ -1663,7 +1663,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/vite@7.3.2" "value": "licenses/vite@8.0.10"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -1673,41 +1673,10 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/vite-tsconfig-paths@6.1.1", "bomRef": "pkg:npm/zod@4.4.1",
"name": "vite-tsconfig-paths",
"version": "6.1.1",
"purl": "pkg:npm/vite-tsconfig-paths@6.1.1",
"description": "Vite resolver for TypeScript compilerOptions.paths",
"licenses": [
{
"license": {
"id": "MIT"
}
}
],
"externalReferences": [
{
"type": "vcs",
"url": "aleclarson/vite-tsconfig-paths"
}
],
"properties": [
{
"name": "local:licenseFolder",
"value": "licenses/vite-tsconfig-paths@6.1.1"
},
{
"name": "local:sourcePackageDir",
"value": "apps/web/node_modules/vite-tsconfig-paths"
}
]
},
{
"type": "library",
"bomRef": "pkg:npm/zod@4.3.6",
"name": "zod", "name": "zod",
"version": "4.3.6", "version": "4.4.1",
"purl": "pkg:npm/zod@4.3.6", "purl": "pkg:npm/zod@4.4.1",
"description": "TypeScript-first schema declaration and validation library with static type inference", "description": "TypeScript-first schema declaration and validation library with static type inference",
"licenses": [ "licenses": [
{ {
@ -1729,7 +1698,7 @@
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/zod@4.3.6" "value": "licenses/zod@4.4.1"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",

View file

@ -1,6 +1,6 @@
{ {
"generatedAt": "2026-05-01T13:23:05.335Z", "generatedAt": "2026-05-01T17:38:04.762Z",
"packageCount": 53, "packageCount": 52,
"packages": [ "packages": [
{ {
"name": "@codemirror/commands", "name": "@codemirror/commands",
@ -43,15 +43,15 @@
}, },
{ {
"name": "@codemirror/view", "name": "@codemirror/view",
"version": "6.41.0", "version": "6.41.1",
"license": "MIT", "license": "MIT",
"homepage": null, "homepage": null,
"repository": "git+https://github.com/codemirror/view.git", "repository": "git+https://code.haverbeke.berlin/codemirror/view.git",
"description": "DOM view component for the CodeMirror code editor", "description": "DOM view component for the CodeMirror code editor",
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/@codemirror_view@6.41.0", "licenseFolder": "licenses/@codemirror_view@6.41.1",
"sourcePackageDir": "packages/markdown/node_modules/@codemirror/view" "sourcePackageDir": "packages/markdown/node_modules/@codemirror/view"
}, },
{ {
@ -95,7 +95,7 @@
}, },
{ {
"name": "@noble/curves", "name": "@noble/curves",
"version": "2.0.1", "version": "2.2.0",
"license": "MIT", "license": "MIT",
"homepage": "https://paulmillr.com/noble/", "homepage": "https://paulmillr.com/noble/",
"repository": "git+https://github.com/paulmillr/noble-curves.git", "repository": "git+https://github.com/paulmillr/noble-curves.git",
@ -103,12 +103,12 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/@noble_curves@2.0.1", "licenseFolder": "licenses/@noble_curves@2.2.0",
"sourcePackageDir": "apps/web/node_modules/@noble/curves" "sourcePackageDir": "apps/web/node_modules/@noble/curves"
}, },
{ {
"name": "@tailwindcss/vite", "name": "@tailwindcss/vite",
"version": "4.2.2", "version": "4.2.4",
"license": "MIT", "license": "MIT",
"homepage": "https://tailwindcss.com", "homepage": "https://tailwindcss.com",
"repository": "https://github.com/tailwindlabs/tailwindcss.git", "repository": "https://github.com/tailwindlabs/tailwindcss.git",
@ -116,12 +116,12 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/@tailwindcss_vite@4.2.2", "licenseFolder": "licenses/@tailwindcss_vite@4.2.4",
"sourcePackageDir": "apps/web/node_modules/@tailwindcss/vite" "sourcePackageDir": "apps/web/node_modules/@tailwindcss/vite"
}, },
{ {
"name": "@tanstack/react-query", "name": "@tanstack/react-query",
"version": "5.99.0", "version": "5.100.7",
"license": "MIT", "license": "MIT",
"homepage": "https://tanstack.com/query", "homepage": "https://tanstack.com/query",
"repository": "git+https://github.com/TanStack/query.git", "repository": "git+https://github.com/TanStack/query.git",
@ -129,12 +129,12 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/@tanstack_react-query@5.99.0", "licenseFolder": "licenses/@tanstack_react-query@5.100.7",
"sourcePackageDir": "packages/call/node_modules/@tanstack/react-query" "sourcePackageDir": "packages/call/node_modules/@tanstack/react-query"
}, },
{ {
"name": "@tanstack/react-router", "name": "@tanstack/react-router",
"version": "1.168.18", "version": "1.169.1",
"license": "MIT", "license": "MIT",
"homepage": "https://tanstack.com/router", "homepage": "https://tanstack.com/router",
"repository": "git+https://github.com/TanStack/router.git", "repository": "git+https://github.com/TanStack/router.git",
@ -142,12 +142,12 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/@tanstack_react-router@1.168.18", "licenseFolder": "licenses/@tanstack_react-router@1.169.1",
"sourcePackageDir": "apps/web/node_modules/@tanstack/react-router" "sourcePackageDir": "apps/web/node_modules/@tanstack/react-router"
}, },
{ {
"name": "@tanstack/react-virtual", "name": "@tanstack/react-virtual",
"version": "3.13.23", "version": "3.13.24",
"license": "MIT", "license": "MIT",
"homepage": "https://tanstack.com/virtual", "homepage": "https://tanstack.com/virtual",
"repository": "git+https://github.com/TanStack/virtual.git", "repository": "git+https://github.com/TanStack/virtual.git",
@ -155,12 +155,12 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/@tanstack_react-virtual@3.13.23", "licenseFolder": "licenses/@tanstack_react-virtual@3.13.24",
"sourcePackageDir": "apps/web/node_modules/@tanstack/react-virtual" "sourcePackageDir": "apps/web/node_modules/@tanstack/react-virtual"
}, },
{ {
"name": "@tauri-apps/api", "name": "@tauri-apps/api",
"version": "2.10.1", "version": "2.11.0",
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/tauri-apps/tauri#readme", "homepage": "https://github.com/tauri-apps/tauri#readme",
"repository": "git+https://github.com/tauri-apps/tauri.git", "repository": "git+https://github.com/tauri-apps/tauri.git",
@ -169,12 +169,12 @@
"LICENSE_MIT", "LICENSE_MIT",
"LICENSE_APACHE-2.0" "LICENSE_APACHE-2.0"
], ],
"licenseFolder": "licenses/@tauri-apps_api@2.10.1", "licenseFolder": "licenses/@tauri-apps_api@2.11.0",
"sourcePackageDir": "apps/tauri/node_modules/@tauri-apps/api" "sourcePackageDir": "apps/tauri/node_modules/@tauri-apps/api"
}, },
{ {
"name": "@tauri-apps/cli", "name": "@tauri-apps/cli",
"version": "2.10.1", "version": "2.11.0",
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/tauri-apps/tauri#readme", "homepage": "https://github.com/tauri-apps/tauri#readme",
"repository": "git+https://github.com/tauri-apps/tauri.git", "repository": "git+https://github.com/tauri-apps/tauri.git",
@ -183,7 +183,7 @@
"LICENSE_MIT", "LICENSE_MIT",
"LICENSE_APACHE-2.0" "LICENSE_APACHE-2.0"
], ],
"licenseFolder": "licenses/@tauri-apps_cli@2.10.1", "licenseFolder": "licenses/@tauri-apps_cli@2.11.0",
"sourcePackageDir": "apps/tauri/node_modules/@tauri-apps/cli" "sourcePackageDir": "apps/tauri/node_modules/@tauri-apps/cli"
}, },
{ {
@ -227,7 +227,7 @@
}, },
{ {
"name": "@tensamin/ttp-core", "name": "@tensamin/ttp-core",
"version": "0.0.13", "version": "0.0.15",
"license": "UNKNOWN", "license": "UNKNOWN",
"homepage": null, "homepage": null,
"repository": null, "repository": null,
@ -235,7 +235,7 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/@tensamin_ttp-core@0.0.13", "licenseFolder": "licenses/@tensamin_ttp-core@0.0.15",
"sourcePackageDir": "packages/ttp/node_modules/@tensamin/ttp-core" "sourcePackageDir": "packages/ttp/node_modules/@tensamin/ttp-core"
}, },
{ {
@ -303,7 +303,7 @@
}, },
{ {
"name": "@typescript-eslint/parser", "name": "@typescript-eslint/parser",
"version": "8.58.1", "version": "8.59.1",
"license": "MIT", "license": "MIT",
"homepage": "https://typescript-eslint.io/packages/parser", "homepage": "https://typescript-eslint.io/packages/parser",
"repository": "https://github.com/typescript-eslint/typescript-eslint.git", "repository": "https://github.com/typescript-eslint/typescript-eslint.git",
@ -311,12 +311,12 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/@typescript-eslint_parser@8.58.1", "licenseFolder": "licenses/@typescript-eslint_parser@8.59.1",
"sourcePackageDir": "node_modules/@typescript-eslint/parser" "sourcePackageDir": "node_modules/@typescript-eslint/parser"
}, },
{ {
"name": "@vitejs/plugin-react", "name": "@vitejs/plugin-react",
"version": "5.2.0", "version": "6.0.1",
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme", "homepage": "https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme",
"repository": "git+https://github.com/vitejs/vite-plugin-react.git", "repository": "git+https://github.com/vitejs/vite-plugin-react.git",
@ -324,7 +324,7 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/@vitejs_plugin-react@5.2.0", "licenseFolder": "licenses/@vitejs_plugin-react@6.0.1",
"sourcePackageDir": "apps/web/node_modules/@vitejs/plugin-react" "sourcePackageDir": "apps/web/node_modules/@vitejs/plugin-react"
}, },
{ {
@ -382,7 +382,7 @@
}, },
{ {
"name": "eslint", "name": "eslint",
"version": "10.2.0", "version": "10.2.1",
"license": "MIT", "license": "MIT",
"homepage": "https://eslint.org", "homepage": "https://eslint.org",
"repository": "eslint/eslint", "repository": "eslint/eslint",
@ -390,12 +390,12 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/eslint@10.2.0", "licenseFolder": "licenses/eslint@10.2.1",
"sourcePackageDir": "apps/web/node_modules/eslint" "sourcePackageDir": "apps/web/node_modules/eslint"
}, },
{ {
"name": "eslint-plugin-react-hooks", "name": "eslint-plugin-react-hooks",
"version": "7.0.1", "version": "7.1.1",
"license": "MIT", "license": "MIT",
"homepage": "https://react.dev/", "homepage": "https://react.dev/",
"repository": "https://github.com/facebook/react.git", "repository": "https://github.com/facebook/react.git",
@ -403,7 +403,7 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/eslint-plugin-react-hooks@7.0.1", "licenseFolder": "licenses/eslint-plugin-react-hooks@7.1.1",
"sourcePackageDir": "node_modules/eslint-plugin-react-hooks" "sourcePackageDir": "node_modules/eslint-plugin-react-hooks"
}, },
{ {
@ -447,7 +447,7 @@
}, },
{ {
"name": "livekit-client", "name": "livekit-client",
"version": "2.18.1", "version": "2.18.8",
"license": "Apache-2.0", "license": "Apache-2.0",
"homepage": null, "homepage": null,
"repository": "git@github.com:livekit/client-sdk-js.git", "repository": "git@github.com:livekit/client-sdk-js.git",
@ -455,12 +455,12 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/livekit-client@2.18.1", "licenseFolder": "licenses/livekit-client@2.18.8",
"sourcePackageDir": "packages/call/node_modules/livekit-client" "sourcePackageDir": "packages/call/node_modules/livekit-client"
}, },
{ {
"name": "lucide-react", "name": "lucide-react",
"version": "1.8.0", "version": "1.14.0",
"license": "ISC", "license": "ISC",
"homepage": "https://lucide.dev", "homepage": "https://lucide.dev",
"repository": "https://github.com/lucide-icons/lucide.git", "repository": "https://github.com/lucide-icons/lucide.git",
@ -468,12 +468,12 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/lucide-react@1.8.0", "licenseFolder": "licenses/lucide-react@1.14.0",
"sourcePackageDir": "apps/tauri/node_modules/lucide-react" "sourcePackageDir": "apps/tauri/node_modules/lucide-react"
}, },
{ {
"name": "prettier", "name": "prettier",
"version": "3.8.2", "version": "3.8.3",
"license": "MIT", "license": "MIT",
"homepage": "https://prettier.io", "homepage": "https://prettier.io",
"repository": "prettier/prettier", "repository": "prettier/prettier",
@ -481,7 +481,7 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/prettier@3.8.2", "licenseFolder": "licenses/prettier@3.8.3",
"sourcePackageDir": "node_modules/prettier" "sourcePackageDir": "node_modules/prettier"
}, },
{ {
@ -538,7 +538,7 @@
}, },
{ {
"name": "shadcn", "name": "shadcn",
"version": "4.2.0", "version": "4.6.0",
"license": "MIT", "license": "MIT",
"homepage": null, "homepage": null,
"repository": "https://github.com/shadcn-ui/ui.git", "repository": "https://github.com/shadcn-ui/ui.git",
@ -546,12 +546,12 @@
"files": [ "files": [
"LICENSE.md" "LICENSE.md"
], ],
"licenseFolder": "licenses/shadcn@4.2.0", "licenseFolder": "licenses/shadcn@4.6.0",
"sourcePackageDir": "apps/web/node_modules/shadcn" "sourcePackageDir": "apps/web/node_modules/shadcn"
}, },
{ {
"name": "sonner", "name": "sonner",
"version": "1.7.4", "version": "2.0.7",
"license": "MIT", "license": "MIT",
"homepage": "https://sonner.emilkowal.ski/", "homepage": "https://sonner.emilkowal.ski/",
"repository": "git+https://github.com/emilkowalski/sonner.git", "repository": "git+https://github.com/emilkowalski/sonner.git",
@ -559,7 +559,7 @@
"files": [ "files": [
"LICENSE.md" "LICENSE.md"
], ],
"licenseFolder": "licenses/sonner@1.7.4", "licenseFolder": "licenses/sonner@2.0.7",
"sourcePackageDir": "apps/web/node_modules/sonner" "sourcePackageDir": "apps/web/node_modules/sonner"
}, },
{ {
@ -590,7 +590,7 @@
}, },
{ {
"name": "tailwindcss", "name": "tailwindcss",
"version": "4.2.2", "version": "4.2.4",
"license": "MIT", "license": "MIT",
"homepage": "https://tailwindcss.com", "homepage": "https://tailwindcss.com",
"repository": "https://github.com/tailwindlabs/tailwindcss.git", "repository": "https://github.com/tailwindlabs/tailwindcss.git",
@ -598,7 +598,7 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/tailwindcss@4.2.2", "licenseFolder": "licenses/tailwindcss@4.2.4",
"sourcePackageDir": "apps/web/node_modules/tailwindcss" "sourcePackageDir": "apps/web/node_modules/tailwindcss"
}, },
{ {
@ -616,7 +616,7 @@
}, },
{ {
"name": "typescript", "name": "typescript",
"version": "5.9.3", "version": "6.0.3",
"license": "Apache-2.0", "license": "Apache-2.0",
"homepage": "https://www.typescriptlang.org/", "homepage": "https://www.typescriptlang.org/",
"repository": "https://github.com/microsoft/TypeScript.git", "repository": "https://github.com/microsoft/TypeScript.git",
@ -624,12 +624,12 @@
"files": [ "files": [
"LICENSE.txt" "LICENSE.txt"
], ],
"licenseFolder": "licenses/typescript@5.9.3", "licenseFolder": "licenses/typescript@6.0.3",
"sourcePackageDir": "apps/web/node_modules/typescript" "sourcePackageDir": "apps/web/node_modules/typescript"
}, },
{ {
"name": "typescript-eslint", "name": "typescript-eslint",
"version": "8.58.1", "version": "8.59.1",
"license": "MIT", "license": "MIT",
"homepage": "https://typescript-eslint.io/packages/typescript-eslint", "homepage": "https://typescript-eslint.io/packages/typescript-eslint",
"repository": "https://github.com/typescript-eslint/typescript-eslint.git", "repository": "https://github.com/typescript-eslint/typescript-eslint.git",
@ -637,12 +637,12 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/typescript-eslint@8.58.1", "licenseFolder": "licenses/typescript-eslint@8.59.1",
"sourcePackageDir": "apps/web/node_modules/typescript-eslint" "sourcePackageDir": "apps/web/node_modules/typescript-eslint"
}, },
{ {
"name": "vite", "name": "vite",
"version": "7.3.2", "version": "8.0.10",
"license": "MIT", "license": "MIT",
"homepage": "https://vite.dev", "homepage": "https://vite.dev",
"repository": "git+https://github.com/vitejs/vite.git", "repository": "git+https://github.com/vitejs/vite.git",
@ -650,25 +650,12 @@
"files": [ "files": [
"LICENSE.md" "LICENSE.md"
], ],
"licenseFolder": "licenses/vite@7.3.2", "licenseFolder": "licenses/vite@8.0.10",
"sourcePackageDir": "apps/web/node_modules/vite" "sourcePackageDir": "apps/web/node_modules/vite"
}, },
{
"name": "vite-tsconfig-paths",
"version": "6.1.1",
"license": "MIT",
"homepage": null,
"repository": "aleclarson/vite-tsconfig-paths",
"description": "Vite resolver for TypeScript compilerOptions.paths",
"files": [
"LICENSE"
],
"licenseFolder": "licenses/vite-tsconfig-paths@6.1.1",
"sourcePackageDir": "apps/web/node_modules/vite-tsconfig-paths"
},
{ {
"name": "zod", "name": "zod",
"version": "4.3.6", "version": "4.4.1",
"license": "MIT", "license": "MIT",
"homepage": "https://zod.dev", "homepage": "https://zod.dev",
"repository": "git+https://github.com/colinhacks/zod.git", "repository": "git+https://github.com/colinhacks/zod.git",
@ -676,7 +663,7 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/zod@4.3.6", "licenseFolder": "licenses/zod@4.4.1",
"sourcePackageDir": "apps/web/node_modules/zod" "sourcePackageDir": "apps/web/node_modules/zod"
}, },
{ {

View file

@ -25,7 +25,7 @@ SOFTWARE.
# Licenses of bundled dependencies # Licenses of bundled dependencies
The published Vite artifact additionally contains code with the following licenses: The published Vite artifact additionally contains code with the following licenses:
BSD-2-Clause, CC0-1.0, ISC, MIT Apache-2.0, BSD-2-Clause, CC0-1.0, ISC, MIT
# Bundled dependencies: # Bundled dependencies:
## @jridgewell/gen-mapping, @jridgewell/remapping, @jridgewell/sourcemap-codec, @jridgewell/trace-mapping ## @jridgewell/gen-mapping, @jridgewell/remapping, @jridgewell/sourcemap-codec, @jridgewell/trace-mapping
@ -95,47 +95,11 @@ Repository: https://github.com/lukeed/polka
--------------------------------------- ---------------------------------------
## @rolldown/pluginutils ## @rollup/plugin-alias, @rollup/plugin-dynamic-import-vars, @rollup/pluginutils
License: MIT
Repository: https://github.com/rolldown/rolldown
> MIT License
>
> Copyright (c) 2024-present VoidZero Inc. & Contributors
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> SOFTWARE.
>
> end of terms and conditions
>
> The licenses of externally maintained libraries from which parts of the Software is derived are listed [here](https://github.com/rolldown/rolldown/blob/main/THIRD-PARTY-LICENSE).
---------------------------------------
## @rollup/plugin-alias, @rollup/plugin-commonjs, @rollup/plugin-dynamic-import-vars, @rollup/pluginutils
License: MIT License: MIT
By: Johannes Stein By: Johannes Stein
Repository: https://github.com/rollup/plugins Repository: https://github.com/rollup/plugins
License: MIT
By: Rich Harris
Repository: https://github.com/rollup/plugins
License: MIT License: MIT
By: LarsDenBakker By: LarsDenBakker
Repository: https://github.com/rollup/plugins Repository: https://github.com/rollup/plugins
@ -168,6 +132,243 @@ Repository: https://github.com/rollup/plugins
--------------------------------------- ---------------------------------------
## @vercel/detect-agent
License: Apache-2.0
By: Vercel
Repository: https://github.com/vercel/vercel
> Apache License
> Version 2.0, January 2004
> http://www.apache.org/licenses/
>
> TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
>
> 1. Definitions.
>
> "License" shall mean the terms and conditions for use, reproduction,
> and distribution as defined by Sections 1 through 9 of this document.
>
> "Licensor" shall mean the copyright owner or entity authorized by
> the copyright owner that is granting the License.
>
> "Legal Entity" shall mean the union of the acting entity and all
> other entities that control, are controlled by, or are under common
> control with that entity. For the purposes of this definition,
> "control" means (i) the power, direct or indirect, to cause the
> direction or management of such entity, whether by contract or
> otherwise, or (ii) ownership of fifty percent (50%) or more of the
> outstanding shares, or (iii) beneficial ownership of such entity.
>
> "You" (or "Your") shall mean an individual or Legal Entity
> exercising permissions granted by this License.
>
> "Source" form shall mean the preferred form for making modifications,
> including but not limited to software source code, documentation
> source, and configuration files.
>
> "Object" form shall mean any form resulting from mechanical
> transformation or translation of a Source form, including but
> not limited to compiled object code, generated documentation,
> and conversions to other media types.
>
> "Work" shall mean the work of authorship, whether in Source or
> Object form, made available under the License, as indicated by a
> copyright notice that is included in or attached to the work
> (an example is provided in the Appendix below).
>
> "Derivative Works" shall mean any work, whether in Source or Object
> form, that is based on (or derived from) the Work and for which the
> editorial revisions, annotations, elaborations, or other modifications
> represent, as a whole, an original work of authorship. For the purposes
> of this License, Derivative Works shall not include works that remain
> separable from, or merely link (or bind by name) to the interfaces of,
> the Work and Derivative Works thereof.
>
> "Contribution" shall mean any work of authorship, including
> the original version of the Work and any modifications or additions
> to that Work or Derivative Works thereof, that is intentionally
> submitted to Licensor for inclusion in the Work by the copyright owner
> or by an individual or Legal Entity authorized to submit on behalf of
> the copyright owner. For the purposes of this definition, "submitted"
> means any form of electronic, verbal, or written communication sent
> to the Licensor or its representatives, including but not limited to
> communication on electronic mailing lists, source code control systems,
> and issue tracking systems that are managed by, or on behalf of, the
> Licensor for the purpose of discussing and improving the Work, but
> excluding communication that is conspicuously marked or otherwise
> designated in writing by the copyright owner as "Not a Contribution."
>
> "Contributor" shall mean Licensor and any individual or Legal Entity
> on behalf of whom a Contribution has been received by Licensor and
> subsequently incorporated within the Work.
>
> 2. Grant of Copyright License. Subject to the terms and conditions of
> this License, each Contributor hereby grants to You a perpetual,
> worldwide, non-exclusive, no-charge, royalty-free, irrevocable
> copyright license to reproduce, prepare Derivative Works of,
> publicly display, publicly perform, sublicense, and distribute the
> Work and such Derivative Works in Source or Object form.
>
> 3. Grant of Patent License. Subject to the terms and conditions of
> this License, each Contributor hereby grants to You a perpetual,
> worldwide, non-exclusive, no-charge, royalty-free, irrevocable
> (except as stated in this section) patent license to make, have made,
> use, offer to sell, sell, import, and otherwise transfer the Work,
> where such license applies only to those patent claims licensable
> by such Contributor that are necessarily infringed by their
> Contribution(s) alone or by combination of their Contribution(s)
> with the Work to which such Contribution(s) was submitted. If You
> institute patent litigation against any entity (including a
> cross-claim or counterclaim in a lawsuit) alleging that the Work
> or a Contribution incorporated within the Work constitutes direct
> or contributory patent infringement, then any patent licenses
> granted to You under this License for that Work shall terminate
> as of the date such litigation is filed.
>
> 4. Redistribution. You may reproduce and distribute copies of the
> Work or Derivative Works thereof in any medium, with or without
> modifications, and in Source or Object form, provided that You
> meet the following conditions:
>
> (a) You must give any other recipients of the Work or
> Derivative Works a copy of this License; and
>
> (b) You must cause any modified files to carry prominent notices
> stating that You changed the files; and
>
> (c) You must retain, in the Source form of any Derivative Works
> that You distribute, all copyright, patent, trademark, and
> attribution notices from the Source form of the Work,
> excluding those notices that do not pertain to any part of
> the Derivative Works; and
>
> (d) If the Work includes a "NOTICE" text file as part of its
> distribution, then any Derivative Works that You distribute must
> include a readable copy of the attribution notices contained
> within such NOTICE file, excluding those notices that do not
> pertain to any part of the Derivative Works, in at least one
> of the following places: within a NOTICE text file distributed
> as part of the Derivative Works; within the Source form or
> documentation, if provided along with the Derivative Works; or,
> within a display generated by the Derivative Works, if and
> wherever such third-party notices normally appear. The contents
> of the NOTICE file are for informational purposes only and
> do not modify the License. You may add Your own attribution
> notices within Derivative Works that You distribute, alongside
> or as an addendum to the NOTICE text from the Work, provided
> that such additional attribution notices cannot be construed
> as modifying the License.
>
> You may add Your own copyright statement to Your modifications and
> may provide additional or different license terms and conditions
> for use, reproduction, or distribution of Your modifications, or
> for any such Derivative Works as a whole, provided Your use,
> reproduction, and distribution of the Work otherwise complies with
> the conditions stated in this License.
>
> 5. Submission of Contributions. Unless You explicitly state otherwise,
> any Contribution intentionally submitted for inclusion in the Work
> by You to the Licensor shall be under the terms and conditions of
> this License, without any additional terms or conditions.
> Notwithstanding the above, nothing herein shall supersede or modify
> the terms of any separate license agreement you may have executed
> with Licensor regarding such Contributions.
>
> 6. Trademarks. This License does not grant permission to use the trade
> names, trademarks, service marks, or product names of the Licensor,
> except as required for reasonable and customary use in describing the
> origin of the Work and reproducing the content of the NOTICE file.
>
> 7. Disclaimer of Warranty. Unless required by applicable law or
> agreed to in writing, Licensor provides the Work (and each
> Contributor provides its Contributions) on an "AS IS" BASIS,
> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied, including, without limitation, any warranties or conditions
> of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
> PARTICULAR PURPOSE. You are solely responsible for determining the
> appropriateness of using or redistributing the Work and assume any
> risks associated with Your exercise of permissions under this License.
>
> 8. Limitation of Liability. In no event and under no legal theory,
> whether in tort (including negligence), contract, or otherwise,
> unless required by applicable law (such as deliberate and grossly
> negligent acts) or agreed to in writing, shall any Contributor be
> liable to You for damages, including any direct, indirect, special,
> incidental, or consequential damages of any character arising as a
> result of this License or out of the use or inability to use the
> Work (including but not limited to damages for loss of goodwill,
> work stoppage, computer failure or malfunction, or any and all
> other commercial damages or losses), even if such Contributor
> has been advised of the possibility of such damages.
>
> 9. Accepting Warranty or Additional Liability. While redistributing
> the Work or Derivative Works thereof, You may choose to offer,
> and charge a fee for, acceptance of support, warranty, indemnity,
> or other liability obligations and/or rights consistent with this
> License. However, in accepting such obligations, You may act only
> on Your own behalf and on Your sole responsibility, not on behalf
> of any other Contributor, and only if You agree to indemnify,
> defend, and hold each Contributor harmless for any liability
> incurred by, or claims asserted against, such Contributor by reason
> of your accepting any such warranty or additional liability.
>
> END OF TERMS AND CONDITIONS
>
> APPENDIX: How to apply the Apache License to your work.
>
> To apply the Apache License to your work, attach the following
> boilerplate notice, with the fields enclosed by brackets "[]"
> replaced with your own identifying information. (Don't include
> the brackets!) The text should be enclosed in the appropriate
> comment syntax for the file format. We also recommend that a
> file or class name and description of purpose be included on the
> same "printed page" as the copyright notice for easier
> identification within third-party archives.
>
> Copyright 2017 Vercel, Inc.
>
> Licensed under the Apache License, Version 2.0 (the "License");
> you may not use this file except in compliance with the License.
> You may obtain a copy of the License at
>
> http://www.apache.org/licenses/LICENSE-2.0
>
> Unless required by applicable law or agreed to in writing, software
> distributed under the License is distributed on an "AS IS" BASIS,
> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> See the License for the specific language governing permissions and
> limitations under the License.
---------------------------------------
## @vitest/utils
License: MIT
Repository: https://github.com/vitest-dev/vitest
> MIT License
>
> Copyright (c) 2021-Present VoidZero Inc. and Vitest contributors
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> SOFTWARE.
---------------------------------------
## anymatch ## anymatch
License: ISC License: ISC
By: Elan Shanker By: Elan Shanker
@ -296,7 +497,7 @@ Repositories: https://github.com/sindresorhus/bundle-name, https://github.com/si
## cac ## cac
License: MIT License: MIT
By: egoist By: egoist
Repository: https://github.com/egoist/cac Repository: https://github.com/cacjs/cac
> The MIT License (MIT) > The MIT License (MIT)
> >
@ -351,38 +552,6 @@ Repository: https://github.com/paulmillr/chokidar
--------------------------------------- ---------------------------------------
## commondir, shell-quote
License: MIT
By: James Halliday
Repositories: http://github.com/substack/node-commondir, http://github.com/ljharb/shell-quote
> The MIT License
>
> Copyright (c) 2013 James Halliday (mail@substack.net)
>
> Permission is hereby granted, free of charge,
> to any person obtaining a copy of this software and
> associated documentation files (the "Software"), to
> deal in the Software without restriction, including
> without limitation the rights to use, copy, modify,
> merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom
> the Software is furnished to do so,
> subject to the following conditions:
>
> The above copyright notice and this permission notice
> shall be included in all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
> OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
> ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---------------------------------------
## connect ## connect
License: MIT License: MIT
By: TJ Holowaychuk, Douglas Christopher Wilson, Jonathan Ong, Tim Caswell By: TJ Holowaychuk, Douglas Christopher Wilson, Jonathan Ong, Tim Caswell
@ -534,36 +703,6 @@ Repository: https://github.com/mathiasbynens/cssesc
--------------------------------------- ---------------------------------------
## dotenv
License: BSD-2-Clause
Repository: https://github.com/motdotla/dotenv
> Copyright (c) 2015, Scott Motte
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
>
> * Redistributions of source code must retain the above copyright notice, this
> list of conditions and the following disclaimer.
>
> * Redistributions in binary form must reproduce the above copyright notice,
> this list of conditions and the following disclaimer in the documentation
> and/or other materials provided with the distribution.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---------------------------------------
## dotenv-expand ## dotenv-expand
License: BSD-2-Clause License: BSD-2-Clause
By: motdotla By: motdotla
@ -1023,13 +1162,6 @@ Repository: https://github.com/micromatch/is-glob
--------------------------------------- ---------------------------------------
## is-reference
License: MIT
By: Rich Harris
Repository: https://github.com/Rich-Harris/is-reference
---------------------------------------
## isexe, which ## isexe, which
License: ISC License: ISC
By: Isaac Z. Schlueter By: Isaac Z. Schlueter
@ -1417,7 +1549,7 @@ Repository: https://github.com/jshttp/on-finished
## parse5 ## parse5
License: MIT License: MIT
By: Ivan Nikulin, https://github.com/inikulin/parse5/graphs/contributors By: Ivan Nikulin, James Garbutt, Felix Boehm, Titus
Repository: https://github.com/inikulin/parse5 Repository: https://github.com/inikulin/parse5
> Copyright (c) 2013-2019 Ivan Nikulin (ifaaan@gmail.com, https://github.com/inikulin) > Copyright (c) 2013-2019 Ivan Nikulin (ifaaan@gmail.com, https://github.com/inikulin)
@ -1817,6 +1949,38 @@ Repository: https://github.com/kevva/shebang-command
--------------------------------------- ---------------------------------------
## shell-quote
License: MIT
By: James Halliday
Repository: http://github.com/ljharb/shell-quote
> The MIT License
>
> Copyright (c) 2013 James Halliday (mail@substack.net)
>
> Permission is hereby granted, free of charge,
> to any person obtaining a copy of this software and
> associated documentation files (the "Software"), to
> deal in the Software without restriction, including
> without limitation the rights to use, copy, modify,
> merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom
> the Software is furnished to do so,
> subject to the following conditions:
>
> The above copyright notice and this permission notice
> shall be included in all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
> OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
> ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---------------------------------------
## sirv ## sirv
License: MIT License: MIT
By: Luke Edwards By: Luke Edwards
@ -1919,60 +2083,6 @@ Repository: https://github.com/micromatch/to-regex-range
--------------------------------------- ---------------------------------------
## tsconfck
License: MIT
By: dominikg
Repository: https://github.com/dominikg/tsconfck
> MIT License
>
> Copyright (c) 2021-present dominikg and [contributors](https://github.com/dominikg/tsconfck/graphs/contributors)
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> SOFTWARE.
>
> -- Licenses for 3rd-party code included in tsconfck --
>
> # strip-bom and strip-json-comments
> MIT License
>
> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> SOFTWARE.
---------------------------------------
## unpipe ## unpipe
License: MIT License: MIT
By: Douglas Christopher Wilson By: Douglas Christopher Wilson

View file

@ -15,6 +15,7 @@
"pre-build": "bun run lint && bun run build:packages", "pre-build": "bun run lint && bun run build:packages",
"build:apps": "bun run copy-licenses && bun run pre-build && bun run build:mobile && bun run build:desktop && bun --bun run copy-releases", "build:apps": "bun run copy-licenses && bun run pre-build && bun run build:mobile && bun run build:desktop && bun --bun run copy-releases",
"build:packages": "bun scripts/build-packages.ts", "build:packages": "bun scripts/build-packages.ts",
"update:packages": "bun scripts/update-packages.ts",
"dev:web": "cd apps/web && bun dev", "dev:web": "cd apps/web && bun dev",
"build:web": "cd apps/web && bun run build", "build:web": "cd apps/web && bun run build",
"preview:web": "cd apps/web && bun run preview", "preview:web": "cd apps/web && bun run preview",
@ -28,18 +29,18 @@
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@typescript-eslint/parser": "^8.58.1", "@typescript-eslint/parser": "^8.59.1",
"eslint": "^10.2.0", "eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.1.1",
"globals": "^17.4.0", "globals": "^17.5.0",
"prettier": "^3.8.2", "prettier": "^3.8.3",
"typescript": "^6.0.2", "typescript": "^6.0.3",
"typescript-eslint": "^8.58.1", "typescript-eslint": "^8.59.1",
"jsonc-parser": "^3.3.1" "jsonc-parser": "^3.3.1"
}, },
"overrides": { "overrides": {
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.30.tar.gz", "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.30.tar.gz",
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.13.tar.gz" "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.15.tar.gz"
}, },
"dependencies": { "dependencies": {
"@tensamin/ttp-core": "*", "@tensamin/ttp-core": "*",

View file

@ -16,9 +16,9 @@
}, },
"dependencies": { "dependencies": {
"@livekit/components-react": "^2.9.20", "@livekit/components-react": "^2.9.20",
"@tanstack/react-query": "^5.0.0", "@tanstack/react-query": "^5.100.7",
"@tanstack/react-router": "^1.0.0", "@tanstack/react-router": "^1.169.1",
"@tanstack/react-virtual": "^3.0.0", "@tanstack/react-virtual": "^3.13.24",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
"@tensamin/markdown": "workspace:*", "@tensamin/markdown": "workspace:*",
@ -29,8 +29,8 @@
"@tensamin/ui": "*", "@tensamin/ui": "*",
"@tensamin/user": "workspace:*", "@tensamin/user": "workspace:*",
"deepfilternet3-noise-filter": "^1.2.1", "deepfilternet3-noise-filter": "^1.2.1",
"livekit-client": "^2.18.1", "livekit-client": "^2.18.8",
"lucide-react": "^0.564.0", "lucide-react": "^1.14.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"recharts": "^3.8.1", "recharts": "^3.8.1",

View file

@ -90,6 +90,7 @@ export default function ScreenShareDialog({
let active = true; let active = true;
// eslint-disable-next-line
setLoading(true); setLoading(true);
setSelectedSourceId(null); setSelectedSourceId(null);
setSelectedAudioOutputId(NONE_AUDIO_OUTPUT); setSelectedAudioOutputId(NONE_AUDIO_OUTPUT);

View file

@ -25,7 +25,7 @@
"@tensamin/markdown": "workspace:*", "@tensamin/markdown": "workspace:*",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",
"@tensamin/ui": "*", "@tensamin/ui": "*",
"lucide-react": "^0.564.0", "lucide-react": "^1.14.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"zod": "^4.3.6" "zod": "^4.3.6"

Some files were not shown because too many files have changed in this diff Show more