diff --git a/baml_language/.cargo/size-gate.toml b/baml_language/.cargo/size-gate.toml index 4fa22b87dab..530e72f4e8f 100644 --- a/baml_language/.cargo/size-gate.toml +++ b/baml_language/.cargo/size-gate.toml @@ -42,15 +42,15 @@ policy.max_delta_pct = 3.0 # Baselines re-recorded after merging canary (~+4%). Reclaiming the `syn` # cost would mean dropping prettyplease (raw, unformatted generated SDK # output). -# macOS arm64: baseline 18.90 MB file (18_900_160), ceiling = baseline x1.03. +# macOS arm64: baseline 19.55 MB file (19_545_088), ceiling = baseline x1.03. [artifacts.baml-cli.platform.aarch64-apple-darwin] -max_file_bytes = 19_467_165 -# Linux x86_64: baseline 24.51 MB file (24_507_664), ceiling = baseline x1.03. +max_file_bytes = 20_131_441 +# Linux x86_64: baseline 25.27 MB file (25_272_400), ceiling = baseline x1.03. [artifacts.baml-cli.platform.x86_64-unknown-linux-gnu] -max_file_bytes = 25_242_894 -# Windows x86_64: baseline 20.43 MB file (20_426_752), ceiling = baseline x1.03. +max_file_bytes = 26_030_572 +# Windows x86_64: baseline 21.08 MB file (21_075_968), ceiling = baseline x1.03. [artifacts.baml-cli.platform.x86_64-pc-windows-msvc] -max_file_bytes = 21_039_555 +max_file_bytes = 21_708_248 [artifacts.packed-program] kind = "pack" @@ -61,10 +61,10 @@ policy.max_delta_pct = 3.0 max_file_bytes = 13_602_990 # Linux x86_64: baseline 17.03 MB file. [artifacts.packed-program.platform.x86_64-unknown-linux-gnu] -max_file_bytes = 17_542_251 +max_file_bytes = 17_538_033 # Windows x86_64: baseline 14.14 MB file. [artifacts.packed-program.platform.x86_64-pc-windows-msvc] -max_file_bytes = 14_567_300 +max_file_bytes = 14_566_773 [artifacts.packed-program.pack] cli_package = "baml_cli" @@ -86,6 +86,6 @@ no_default_features = true wasm = true policy.max_delta_pct = 3.0 -# Single platform; baseline 4.27 MB gzip. +# Single platform; baseline 4.40 MB gzip. [artifacts.bridge_wasm.platform.wasm32-unknown-unknown] -max_gzip_bytes = 4_400_547 +max_gzip_bytes = 4_534_972 diff --git a/baml_language/.ci/size-gate/aarch64-apple-darwin.toml b/baml_language/.ci/size-gate/aarch64-apple-darwin.toml index 460105fef06..378b2a198af 100644 --- a/baml_language/.ci/size-gate/aarch64-apple-darwin.toml +++ b/baml_language/.ci/size-gate/aarch64-apple-darwin.toml @@ -1,12 +1,12 @@ version = 1 -recorded_at = "2026-07-17T00:35:21Z" -git_sha = "b01ee9c671a00b44a7a9a3832e149e3c2ec93c13" +recorded_at = "2026-07-20T07:51:49Z" +git_sha = "fe3304335ff13eb6355233b1f96690b6ede7ae09" [artifacts.baml-cli] -file_bytes = 18900160 -stripped_bytes = 18900208 -gzip_bytes = 8955790 +file_bytes = 19545088 +stripped_bytes = 19545136 +gzip_bytes = 9331881 [artifacts.packed-program] file_bytes = 13206786 -gzip_bytes = 6154161 +gzip_bytes = 6154377 diff --git a/baml_language/.ci/size-gate/wasm32-unknown-unknown.toml b/baml_language/.ci/size-gate/wasm32-unknown-unknown.toml index acd8ff2b902..d815de5d1ae 100644 --- a/baml_language/.ci/size-gate/wasm32-unknown-unknown.toml +++ b/baml_language/.ci/size-gate/wasm32-unknown-unknown.toml @@ -1,7 +1,7 @@ version = 1 -recorded_at = "2026-07-16T10:00:08Z" -git_sha = "79aa0f44e323e356a172e16e6b0f4a42e569959d" +recorded_at = "2026-07-20T07:51:49Z" +git_sha = "fe3304335ff13eb6355233b1f96690b6ede7ae09" [artifacts.bridge_wasm] -file_bytes = 15157305 -gzip_bytes = 4272375 +file_bytes = 16160508 +gzip_bytes = 4402885 diff --git a/baml_language/.ci/size-gate/x86_64-pc-windows-msvc.toml b/baml_language/.ci/size-gate/x86_64-pc-windows-msvc.toml index c49191d5455..5c472735ac7 100644 --- a/baml_language/.ci/size-gate/x86_64-pc-windows-msvc.toml +++ b/baml_language/.ci/size-gate/x86_64-pc-windows-msvc.toml @@ -1,12 +1,12 @@ version = 1 -recorded_at = "2026-07-17T00:35:21Z" -git_sha = "b01ee9c671a00b44a7a9a3832e149e3c2ec93c13" +recorded_at = "2026-07-20T07:51:49Z" +git_sha = "fe3304335ff13eb6355233b1f96690b6ede7ae09" [artifacts.baml-cli] -file_bytes = 20426752 -stripped_bytes = 20426752 -gzip_bytes = 9169338 +file_bytes = 21075968 +stripped_bytes = 21075968 +gzip_bytes = 9542344 [artifacts.packed-program] -file_bytes = 14143010 -gzip_bytes = 6248500 +file_bytes = 14142498 +gzip_bytes = 6248883 diff --git a/baml_language/.ci/size-gate/x86_64-unknown-linux-gnu.toml b/baml_language/.ci/size-gate/x86_64-unknown-linux-gnu.toml index 06df65b9f78..0ddc5985a73 100644 --- a/baml_language/.ci/size-gate/x86_64-unknown-linux-gnu.toml +++ b/baml_language/.ci/size-gate/x86_64-unknown-linux-gnu.toml @@ -1,12 +1,12 @@ version = 1 -recorded_at = "2026-07-17T00:35:21Z" -git_sha = "b01ee9c671a00b44a7a9a3832e149e3c2ec93c13" +recorded_at = "2026-07-20T07:51:49Z" +git_sha = "fe3304335ff13eb6355233b1f96690b6ede7ae09" [artifacts.baml-cli] -file_bytes = 24507664 -stripped_bytes = 24507656 -gzip_bytes = 10325653 +file_bytes = 25272400 +stripped_bytes = 25272392 +gzip_bytes = 10732594 [artifacts.packed-program] -file_bytes = 17031312 -gzip_bytes = 7039119 +file_bytes = 17027216 +gzip_bytes = 7039234 diff --git a/baml_language/Cargo.lock b/baml_language/Cargo.lock index 93567bfc2d8..9a1abdecedc 100644 --- a/baml_language/Cargo.lock +++ b/baml_language/Cargo.lock @@ -1673,7 +1673,13 @@ dependencies = [ name = "bridge_typescript_web" version = "0.0.0-beta" dependencies = [ + "baml_version", + "bex_project", "bridge_cffi", + "bridge_ctypes", + "js-sys", + "serde-wasm-bindgen", + "sys_wasm", "wasm-bindgen", "wasm-bindgen-futures", ] @@ -7423,6 +7429,25 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "sys_wasm" +version = "0.0.0-beta" +dependencies = [ + "baml_type", + "bex_project", + "bridge_ctypes", + "futures", + "indexmap", + "js-sys", + "log", + "num-bigint", + "num-traits", + "prost", + "sys_ops", + "sys_types", + "wasm-bindgen", +] + [[package]] name = "taplo" version = "0.13.2" diff --git a/baml_language/Cargo.toml b/baml_language/Cargo.toml index e38e756cb3e..71bc2f06805 100644 --- a/baml_language/Cargo.toml +++ b/baml_language/Cargo.toml @@ -109,6 +109,7 @@ bex_heap = { path = "crates/bex_heap" } bex_sap = { path = "crates/bex_sap" } bridge_ctypes = { path = "crates/bridge_ctypes", default-features = false } bridge_cffi = { path = "crates/bridge_cffi", default-features = false } +sys_wasm = { path = "crates/sys_wasm" } baml_exec = { path = "crates/baml_exec" } bex_events = { path = "crates/bex_events" } bex_resource_types = { path = "crates/bex_resource_types" } diff --git a/baml_language/crates/bridge_cffi/Cargo.toml b/baml_language/crates/bridge_cffi/Cargo.toml index d7d777cd939..ebe8afc0279 100644 --- a/baml_language/crates/bridge_cffi/Cargo.toml +++ b/baml_language/crates/bridge_cffi/Cargo.toml @@ -40,6 +40,9 @@ serde_json = { workspace = true } tokio = { workspace = true, features = [ "rt", "rt-multi-thread" ] } vfs = { workspace = true } +[target.'cfg(target_arch = "wasm32")'.dependencies] +vfs = { workspace = true } + [target.'cfg(not(target_os = "windows"))'.dev-dependencies] cbindgen = { workspace = true } diff --git a/baml_language/crates/bridge_cffi/src/handle.rs b/baml_language/crates/bridge_cffi/src/handle.rs new file mode 100644 index 00000000000..0c925d989eb --- /dev/null +++ b/baml_language/crates/bridge_cffi/src/handle.rs @@ -0,0 +1,156 @@ +//! Safe, target-neutral ordinary handle and media operations. + +use std::sync::Arc; + +use bex_project::{BexExternalAdt, MediaKind, MediaValue}; +use bridge_ctypes::{CffiHandleTableEntry, HANDLE_TABLE, baml_bridge::cffi::BamlHandleType}; + +/// An owned handle-table key and its protocol type tag. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct HandleParts { + pub key: u64, + pub handle_type: i32, +} + +/// Failure from a safe ordinary handle or media operation. +#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)] +pub enum HandleError { + #[error("invalid handle")] + InvalidHandle, + #[error("handle type mismatch")] + TypeMismatch, + #[error("unsupported handle type")] + UnsupportedHandleType, + #[error("{0}")] + InvalidInput(String), +} + +fn insert_entry(entry: CffiHandleTableEntry) -> HandleParts { + let handle_type = entry.handle_type() as i32; + let key = HANDLE_TABLE.insert(entry); + HandleParts { key, handle_type } +} + +fn validate_input(value: &str, field: &str) -> Result<(), HandleError> { + if value.contains('\0') { + return Err(HandleError::InvalidInput(format!( + "{field} contains an embedded NUL byte" + ))); + } + Ok(()) +} + +fn validate_media_input(source: &str, mime_type: Option<&str>) -> Result<(), HandleError> { + validate_input(source, "media source")?; + if let Some(mime_type) = mime_type { + validate_input(mime_type, "media MIME type")?; + } + Ok(()) +} + +fn resolve_media(key: u64, handle_type: i32) -> Result, HandleError> { + let entry = HANDLE_TABLE + .resolve(key) + .ok_or(HandleError::InvalidHandle)?; + + if handle_type != BamlHandleType::HandleUnspecified as i32 + && handle_type != entry.handle_type() as i32 + { + return Err(HandleError::TypeMismatch); + } + + match &*entry { + CffiHandleTableEntry::Adt(BexExternalAdt::Media(media)) => Ok(media.clone()), + _ => Err(HandleError::UnsupportedHandleType), + } +} + +/// Clone one live ordinary handle-table row into a distinct owned key. +pub fn clone_handle(key: u64) -> Result { + HANDLE_TABLE + .clone_handle(key) + .ok_or(HandleError::InvalidHandle) +} + +/// Release one owned ordinary handle-table key. +pub fn release_handle(key: u64) -> Result<(), HandleError> { + if HANDLE_TABLE.release(key) { + Ok(()) + } else { + Err(HandleError::InvalidHandle) + } +} + +/// Return the number of currently owned ordinary handle-table keys. +/// +/// This is exposed by target adapters only as focused test instrumentation; +/// it is not part of the generated SDK surface. +pub fn live_handle_count() -> usize { + HANDLE_TABLE.len() +} + +/// Seed a function-reference handle for focused bridge tests. +pub fn seed_function_ref_handle(global_index: u64) -> HandleParts { + insert_entry(CffiHandleTableEntry::FunctionRef { + global_index: global_index as usize, + }) +} + +/// Seed a generic-media handle for focused bridge tests. +pub fn seed_generic_media_handle() -> HandleParts { + insert_entry(CffiHandleTableEntry::Adt(BexExternalAdt::Media( + MediaValue::from_url(MediaKind::Generic, "https://example.com/", None), + ))) +} + +/// Construct an owned media handle from a URL descriptor. +pub fn media_from_url( + kind: MediaKind, + url: &str, + mime_type: Option<&str>, +) -> Result { + validate_media_input(url, mime_type)?; + Ok(insert_entry(CffiHandleTableEntry::Adt( + BexExternalAdt::Media(MediaValue::from_url(kind, url, mime_type)), + ))) +} + +/// Construct an owned media handle from a file-path descriptor. +pub fn media_from_file( + kind: MediaKind, + file: &str, + mime_type: Option<&str>, +) -> Result { + validate_media_input(file, mime_type)?; + Ok(insert_entry(CffiHandleTableEntry::Adt( + BexExternalAdt::Media(MediaValue::from_file(kind, file, mime_type)), + ))) +} + +/// Construct an owned media handle from base64 text. +pub fn media_from_base64( + kind: MediaKind, + base64: &str, + mime_type: Option<&str>, +) -> Result { + validate_media_input(base64, mime_type)?; + Ok(insert_entry(CffiHandleTableEntry::Adt( + BexExternalAdt::Media(MediaValue::from_base64(kind, base64, mime_type)), + ))) +} + +pub fn media_url(key: u64, handle_type: i32) -> Result, HandleError> { + Ok(resolve_media(key, handle_type)?.url()) +} + +pub fn media_file(key: u64, handle_type: i32) -> Result, HandleError> { + Ok(resolve_media(key, handle_type)?.file()) +} + +pub fn media_base64(key: u64, handle_type: i32) -> Result { + Ok(resolve_media(key, handle_type)?.base64()) +} + +pub fn media_mime_type(key: u64, handle_type: i32) -> Result, HandleError> { + Ok(resolve_media(key, handle_type)?.mime_type()) +} diff --git a/baml_language/crates/bridge_cffi/src/lib.rs b/baml_language/crates/bridge_cffi/src/lib.rs index ff321f581cd..757a4a21220 100644 --- a/baml_language/crates/bridge_cffi/src/lib.rs +++ b/baml_language/crates/bridge_cffi/src/lib.rs @@ -14,13 +14,81 @@ mod platform; #[path = "lib_wasm.rs"] mod platform; +#[cfg(target_arch = "wasm32")] +use std::collections::HashMap; use std::sync::Arc; use bex_project::Bex; +#[cfg(target_arch = "wasm32")] +use vfs::error::VfsErrorKind; + +#[cfg(target_arch = "wasm32")] +#[derive(Debug)] +struct WasmSourceRootFs; + +#[cfg(target_arch = "wasm32")] +impl vfs::FileSystem for WasmSourceRootFs { + fn read_dir(&self, path: &str) -> vfs::VfsResult + Send>> { + if path.is_empty() { + Ok(Box::new(std::iter::empty())) + } else { + Err(VfsErrorKind::FileNotFound.into()) + } + } + + fn create_dir(&self, _path: &str) -> vfs::VfsResult<()> { + Err(VfsErrorKind::NotSupported.into()) + } + + fn open_file(&self, _path: &str) -> vfs::VfsResult> { + Err(VfsErrorKind::FileNotFound.into()) + } + + fn create_file(&self, _path: &str) -> vfs::VfsResult> { + Err(VfsErrorKind::NotSupported.into()) + } + + fn append_file(&self, _path: &str) -> vfs::VfsResult> { + Err(VfsErrorKind::NotSupported.into()) + } + + fn metadata(&self, path: &str) -> vfs::VfsResult { + if path.is_empty() { + Ok(vfs::VfsMetadata { + file_type: vfs::VfsFileType::Directory, + len: 0, + created: None, + modified: None, + accessed: None, + }) + } else { + Err(VfsErrorKind::FileNotFound.into()) + } + } + + fn exists(&self, path: &str) -> vfs::VfsResult { + Ok(path.is_empty()) + } + + fn remove_file(&self, _path: &str) -> vfs::VfsResult<()> { + Err(VfsErrorKind::NotSupported.into()) + } + + fn remove_dir(&self, _path: &str) -> vfs::VfsResult<()> { + Err(VfsErrorKind::NotSupported.into()) + } +} + +#[cfg(target_arch = "wasm32")] +fn source_vfs_root() -> vfs::VfsPath { + vfs::VfsPath::new(WasmSourceRootFs) +} pub mod baml_to_host; pub mod buffer; pub mod error; +#[cfg(target_arch = "wasm32")] +pub mod handle; pub use baml_to_host::{call_and_encode, error_to_outbound, result_to_outbound}; pub use bridge_ctypes::baml_bridge; @@ -48,6 +116,55 @@ pub fn initialize_runtime_from_bytecode_with_sys_ops( Ok(runtime) } +/// Initialize the WebAssembly runtime from an in-memory BAML source map. +#[cfg(target_arch = "wasm32")] +pub fn initialize_runtime_from_files_with_sys_ops( + root_path: &str, + src_files: HashMap, + sys_ops: sys_ops::SysOps, +) -> Result, BridgeError> { + let project_root = source_vfs_root().join(root_path).map_err(|error| { + bex_project::RuntimeError::InvalidArgument { + name: format!("root_path: {error}"), + } + })?; + + let mut files = HashMap::with_capacity(src_files.len()); + for (name, contents) in src_files { + if name.is_empty() { + return Err(bex_project::RuntimeError::InvalidArgument { + name: "source filename must not be empty".to_string(), + } + .into()); + } + let validated_path = project_root.join(&name).map_err(|error| { + bex_project::RuntimeError::InvalidArgument { + name: format!("source filename {name:?}: {error}"), + } + })?; + let root = project_root.as_str(); + let inside_root = if root.is_empty() { + validated_path.as_str().starts_with('/') + } else { + validated_path + .as_str() + .strip_prefix(root) + .is_some_and(|suffix| suffix.starts_with('/')) + }; + if !inside_root { + return Err(bex_project::RuntimeError::InvalidArgument { + name: format!("source filename {name:?} resolves outside the project root"), + } + .into()); + } + files.insert(bex_project::FsPath::from_str(name), contents); + } + + let runtime: Arc = bex_project::new(project_root, sys_ops, files)?; + platform::replace_runtime(runtime.clone())?; + Ok(runtime) +} + /// Initialize the native process-global runtime from serialized BAML bytecode. #[cfg(not(target_arch = "wasm32"))] pub fn initialize_runtime_from_bytecode(bytecode: &[u8]) -> Result, BridgeError> { diff --git a/baml_language/crates/bridge_ctypes/src/handle_table.rs b/baml_language/crates/bridge_ctypes/src/handle_table.rs index 59e442dc098..a48d7a5986e 100644 --- a/baml_language/crates/bridge_ctypes/src/handle_table.rs +++ b/baml_language/crates/bridge_ctypes/src/handle_table.rs @@ -191,6 +191,21 @@ impl CffiHandleTable { .is_some() } + /// Return the number of currently owned handle-table keys in a WebAssembly runtime. + #[cfg(target_arch = "wasm32")] + pub fn len(&self) -> usize { + self.entries + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .len() + } + + /// Return whether the WebAssembly handle table currently owns no keys. + #[cfg(target_arch = "wasm32")] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Atomically resolve and remove. Returns the entry or None if the /// key was already absent. pub fn drain(&self, key: u64) -> Option> { diff --git a/baml_language/crates/sys_wasm/Cargo.toml b/baml_language/crates/sys_wasm/Cargo.toml new file mode 100644 index 00000000000..bc2dab9ca8a --- /dev/null +++ b/baml_language/crates/sys_wasm/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "sys_wasm" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +description = "Browser implementations of BAML system operations" + +[dependencies] +baml_type = { workspace = true } +bex_project = { workspace = true } +bridge_ctypes = { workspace = true } +sys_ops = { workspace = true } +sys_types = { workspace = true } +futures = { workspace = true } +indexmap = { workspace = true } +js-sys = { workspace = true } +log = { workspace = true } +num-bigint = { workspace = true } +num-traits = { workspace = true } +prost = { workspace = true } +wasm-bindgen = { workspace = true } + +[lints] +workspace = true + +[package.metadata.ci] +wasm_support = true diff --git a/baml_language/crates/sys_wasm/src/host_value.rs b/baml_language/crates/sys_wasm/src/host_value.rs new file mode 100644 index 00000000000..3b79e638bc1 --- /dev/null +++ b/baml_language/crates/sys_wasm/src/host_value.rs @@ -0,0 +1,1140 @@ +//! WASM host-value registry and `IoNamespaceHost` impl. +//! +//! When JavaScript passes a callable as an argument to a BAML function, the +//! JS encoder calls [`register_host_callable`] (exposed via wasm-bindgen +//! as `registerHostCallable`) with the user's [`js_sys::Function`]. The +//! registry stores it under a freshly-allocated `u64` key and the encoder +//! emits an `InboundValue::Handle { key, handleType: HOST_VALUE_CALLABLE }`. +//! +//! When BAML invokes the host value, the [`WasmHost`] impl of +//! [`IoNamespaceHost::call_host_value`] encodes the call arguments as a +//! protobuf-encoded `BamlOutboundValue` list, allocates a call id, installs +//! a `CompletionHandle` in the in-flight call table, and fires *this +//! runtime's* JS-supplied `host_dispatch` callback with `(key, callId, +//! argsBytes)`. +//! +//! The JS dispatch wrapper decodes args, invokes the user function (await-ing +//! a returned Promise if any), encodes the result, and calls the wasm-exported +//! [`complete_host_call`] which resolves the `CompletionHandle`. +//! +//! When the engine drops the last Rust clone of the corresponding +//! `HostValueArc`, the global `host_release_dispatch` fires +//! [`host_release_callback`] which removes the registry entry — the user's +//! JS callable becomes GC-eligible. +//! +//! ## Per-runtime dispatch, global registry +//! +//! Dispatch is per-runtime: the JS `host_dispatch` callback — supplied *per* +//! [`BamlWasmRuntime`] at `create` time — lives on the per-runtime [`WasmHost`] +//! (one instance per `BamlWasmRuntime`, wired into its `SysOps`). +//! `IoNamespaceHost::call_host_value` runs on the `WasmHost` that owns the +//! originating runtime, so the dispatch always fires through *that* runtime's +//! wrapper. This is the engine→host path and it is unambiguous because the impl +//! already knows its own runtime; no global routing is needed in this +//! direction. Storing the callback globally instead would let a second runtime +//! clobber the first's wrapper, so a host call from runtime A would dispatch +//! through runtime B's JS wrapper (wrong closure / wrong `completeHostCall` +//! wiring) — which is why it lives on `WasmHost` rather than a `thread_local`. +//! +//! The `callables` map and the `in_flight` call table stay **process-global** +//! and are routed purely by id (mirroring the Node bridge's process-global +//! registry): +//! +//! * The `callables` map only keeps the JS `Function` alive and is consulted +//! solely to *drop* it on release. Keys are minted by a process-global +//! atomic and are therefore **globally unique**, so releasing by key drops +//! exactly the right entry regardless of which runtime registered it. There +//! is no per-runtime ambiguity to resolve. (In WASM, `registerHostCallable` +//! is a free function the JS encoder calls *while building the proto args*, +//! before a `RunStore` start command is invoked — there is no runtime receiver to +//! attribute the registration to. A global registry sidesteps that entirely.) +//! * The `in_flight` table holds only a `CompletionHandle` (a oneshot sender). +//! Resolving it is fully self-contained — it needs no runtime context — and +//! call ids are globally unique, so `complete_host_call(callId)` from runtime +//! A can never resolve runtime B's pending call. +//! +//! `Function` / the table state are `!Send`, so they live in `thread_local!`; +//! `wasm32-unknown-unknown` is single-threaded. +//! +//! ## In-flight lifetime: RAII eviction on cancel, no timeout +//! +//! An `IN_FLIGHT` entry is removed by exactly one of: the JS wrapper calling +//! `completeHostCall(callId, ...)` (normal completion), or cancellation of the +//! BAML call. On cancel the engine drops the async future returned by +//! [`drain_pending`], which drops the [`WasmInflightGuard`] moved into it; the +//! guard removes the dangling entry so it does not leak. There is **no +//! wall-clock timeout** — a host that never completes a call and is never +//! cancelled leaves the entry pending forever (matching the native bridge; see +//! `sys_native::host_dispatch`). + +use std::{ + cell::{Cell, RefCell}, + collections::HashMap, + sync::Arc, +}; + +use bex_project::{HostValueArc, HostValueKind, host_release_dispatch, validate_host_return}; +use bridge_ctypes::{ + CffiHandleTableOptions, HANDLE_TABLE, baml_bridge::cffi::InboundValue, inbound_to_external, +}; +use indexmap::IndexMap; +use js_sys::Function; +use prost::Message; +use sys_ops::io::{ + self, BexExternalValue, CallId, OpError, SysOpContext, SysOpOutput, SysOpResult, VmBamlError, + VmRustFnError, +}; +use sys_types::{BexHeap, CompletionHandle, SysOp}; +use wasm_bindgen::prelude::*; + +use crate::send_wrapper::SendWrapper; + +// ============================================================================ +// Process-global, id-routed WASM host-value state +// ============================================================================ +// +// Keyed by globally-unique ids (see `next_key` / `next_call_id`), so neither +// table needs a per-runtime split to route correctly: a key/call-id maps to at +// most one entry across the whole process. Only the *dispatch callback* is +// per-runtime (see `WasmHost`). + +thread_local! { + /// `u64 → user JS callable`. Populated by [`register_host_callable`] from + /// the JS encoder; removed by [`host_release_callback`] when the Rust + /// runtime drops the last clone of the corresponding `HostValueArc`. Keys + /// are globally unique, so release-by-key is unambiguous across runtimes. + static CALLABLES: RefCell>> = RefCell::new(HashMap::new()); + + /// Optional SDK callback used to release host-owned opaque values kept in + /// a JS-side registry. Playground consumers do not need to install one. + static HOST_VALUE_RELEASE_CALLBACK: RefCell>> = const { RefCell::new(None) }; + + /// `call_id → CompletionHandle`. Populated by [`WasmHost::call_host_value`] + /// before firing `host_dispatch`; removed by [`complete_host_call`] when + /// the JS dispatch wrapper completes the invocation. Call ids are globally + /// unique, so a completion can never resolve a different runtime's call. + static IN_FLIGHT: RefCell> = RefCell::new(HashMap::new()); + + /// Process-global key minter. Globally unique so release-by-key routing is + /// unambiguous. Zero is reserved as "invalid"; wrap-around skips 0. + static NEXT_KEY: RefCell = const { RefCell::new(1) }; + + /// Process-global call-id minter. Globally unique so completion routing is + /// unambiguous. Zero is reserved as "invalid"; wrap-around skips 0. + static NEXT_CALL_ID: RefCell = const { RefCell::new(1) }; + + /// Nesting depth for the bounded Web synchronous-call policy. + static WEB_SYNC_MODE_DEPTH: Cell = const { Cell::new(0) }; +} + +struct WebSyncModeGuard; + +impl Drop for WebSyncModeGuard { + fn drop(&mut self) { + WEB_SYNC_MODE_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1))); + } +} + +/// Run `operation` under the bounded Web synchronous-call policy. +/// +/// The guard is nesting-safe and restores state during panic unwinding. Host +/// operations consult this mode before dispatching into JavaScript. +pub fn with_web_sync_mode(operation: impl FnOnce() -> R) -> R { + WEB_SYNC_MODE_DEPTH.with(|depth| depth.set(depth.get().saturating_add(1))); + let _guard = WebSyncModeGuard; + operation() +} + +fn web_sync_mode_active() -> bool { + WEB_SYNC_MODE_DEPTH.with(|depth| depth.get() > 0) +} + +fn web_sync_failure(operation: SysOp, reason: &str) -> sys_types::VmInternalError { + sys_types::VmInternalError::BridgeFailure { + message: format!( + "{operation:?} cannot complete through callFunctionSync in Web {reason}; use the async API callFunction instead" + ), + } +} + +struct InFlightHostCall { + operation: SysOp, + completion: CompletionHandle, +} + +fn next_key() -> u64 { + NEXT_KEY.with(|cell| { + let mut next = cell.borrow_mut(); + loop { + let k = *next; + *next = next.wrapping_add(1); + if k != 0 { + return k; + } + } + }) +} + +fn next_call_id() -> u32 { + NEXT_CALL_ID.with(|cell| { + let mut next = cell.borrow_mut(); + loop { + let id = *next; + *next = next.wrapping_add(1); + if id != 0 { + return id; + } + } + }) +} + +/// Register a `CompletionHandle` for an in-flight host call. +/// +/// `call_id` must be freshly minted by [`next_call_id`] and not already +/// present. The table is keyed by a *wrapping* `u32`; with RAII eviction of +/// cancelled calls (see [`WasmInflightGuard`]) entries no longer leak, so the +/// live set stays bounded and a wrap-around collision is effectively +/// impossible. +/// +/// We never silently overwrite a live entry (that would strand the previous +/// call's `CompletionHandle` and let a late completion resolve the wrong call). +/// A collision is caught at runtime by refusing the insert and completing the +/// new call with a bridge failure. +/// +/// Returns `true` when `completion` was inserted, `false` on collision. On +/// `false` the caller **must not** fire the JS dispatch (the call has already +/// failed) and **must not** build a [`WasmInflightGuard`] for `call_id` — the +/// live entry under that id belongs to the *other* call, so a guard drop would +/// evict it. +#[must_use] +fn insert_in_flight(call_id: u32, operation: SysOp, completion: CompletionHandle) -> bool { + let collision = IN_FLIGHT.with(|cell| cell.borrow().contains_key(&call_id)); + if collision { + log::error!( + "host-call id {call_id} collided with a live in-flight entry; \ + refusing to overwrite and failing the new call" + ); + completion.complete(Err(OpError::new( + operation, + sys_types::VmInternalError::BridgeFailure { + message: format!("host-call id {call_id} collided with a live in-flight call"), + }, + ))); + return false; + } + IN_FLIGHT.with(|cell| { + cell.borrow_mut().insert( + call_id, + InFlightHostCall { + operation, + completion, + }, + ); + }); + true +} + +/// RAII guard that evicts an in-flight call's `IN_FLIGHT` entry when dropped. +/// +/// Owned by the async future returned by [`drain_pending`]. Carries only the +/// `Copy` `call_id`, never the `CompletionHandle` (which lives in the table). +/// On drop it removes the entry if still present: +/// +/// * **After normal completion** this is a no-op — `complete_host_call` already +/// removed and fired the handle. +/// * **On cancellation** the future is dropped before completion, so the guard +/// removes the dangling entry. Dropping the removed `CompletionHandle` closes +/// the oneshot, so a later `completeHostCall` for that id hits the benign +/// unknown-id path. No leak. +struct WasmInflightGuard { + call_id: u32, +} + +impl Drop for WasmInflightGuard { + fn drop(&mut self) { + let _ = IN_FLIGHT.with(|cell| cell.borrow_mut().remove(&self.call_id)); + } +} + +// ============================================================================ +// JS-facing wasm-bindgen exports +// ============================================================================ + +/// Register a JS callable in the WASM host-value table and return its key. +/// +/// Exposed to JS as `registerHostCallable(fn) -> bigint`. The key is then +/// embedded in `InboundValue::Handle { key, handleType: HOST_VALUE_CALLABLE }` +/// by the JS encoder. The returned value is a `BigInt` because `u64` does not +/// fit into JS's safe-integer range. +#[wasm_bindgen(js_name = registerHostCallable)] +pub fn register_host_callable(callable: Function) -> u64 { + let key = next_key(); + CALLABLES.with(|cell| { + cell.borrow_mut().insert(key, SendWrapper::new(callable)); + }); + key +} + +/// Retain an existing registered callable as an engine host value. +#[allow(dead_code)] +pub(crate) fn retain_host_callable(key: u64) -> Result, String> { + let registered = CALLABLES.with(|cell| cell.borrow().contains_key(&key)); + if !registered { + return Err(format!("host callable key {key} is not registered")); + } + ensure_release_dispatch_installed(); + Ok(HostValueArc::intern(key, HostValueKind::Callable)) +} + +/// Mint a key for a JS-owned opaque value from the same keyspace used by +/// callable host values. Both kinds share one engine handle table. +#[wasm_bindgen(js_name = mintHostValueKey)] +pub fn mint_host_value_key() -> u64 { + next_key() +} + +/// Register the JS callback that releases opaque values from the SDK's local +/// registry when the engine drops its last corresponding `HostValueArc`. +#[wasm_bindgen(js_name = registerHostValueReleaseCallback)] +pub fn register_host_value_release_callback(callback: Function) -> bool { + HOST_VALUE_RELEASE_CALLBACK.with(|cell| { + let mut callback_slot = cell.borrow_mut(); + if callback_slot.is_some() { + false + } else { + callback_slot.replace(SendWrapper::new(callback)); + true + } + }) +} + +/// Remove a callable that was registered but never transferred to the engine. +#[wasm_bindgen(js_name = releaseHostCallable)] +pub fn release_host_callable(key: u64) { + CALLABLES.with(|cell| { + cell.borrow_mut().remove(&key); + }); +} + +#[doc(hidden)] +pub fn test_host_callable_count() -> u32 { + CALLABLES.with(|cell| cell.borrow().len().try_into().unwrap_or(u32::MAX)) +} + +#[doc(hidden)] +pub fn test_in_flight_host_call_count() -> u32 { + IN_FLIGHT.with(|cell| cell.borrow().len().try_into().unwrap_or(u32::MAX)) +} + +#[doc(hidden)] +pub fn test_host_release_callback_installed() -> bool { + HOST_VALUE_RELEASE_CALLBACK.with(|cell| cell.borrow().is_some()) +} + +#[doc(hidden)] +pub fn test_fire_host_release(key: u64) { + host_release_callback(key); +} + +#[doc(hidden)] +pub async fn test_missing_host_callable_error(key: u64) -> String { + let host = WasmHost::direct(); + let callable = HostValueArc::new(key, HostValueKind::Callable); + let output = host.call_registered_callable( + SysOp::BamlHostCallHostValue, + callable.as_ref(), + &[], + &IndexMap::new(), + ); + match output { + SysOpOutput::Ready(Err(error)) => error.to_string(), + SysOpOutput::Async(future) => match future.await { + Err(error) => error.to_string(), + Ok(value) => format!("unexpected success: {value:?}"), + }, + SysOpOutput::Ready(Ok(value)) => format!("unexpected success: {value:?}"), + } +} + +#[doc(hidden)] +pub fn test_sync_pending_host_callable_error(key: u64) -> String { + with_web_sync_mode(|| { + let host = WasmHost::direct(); + let callable = HostValueArc::new(key, HostValueKind::Callable); + let output = host.call_registered_callable( + SysOp::BamlFsRead, + callable.as_ref(), + &[], + &IndexMap::new(), + ); + match output { + SysOpOutput::Ready(Err(error)) => error.to_string(), + SysOpOutput::Async(future) => match futures::executor::block_on(future) { + Err(error) => error.to_string(), + Ok(value) => format!("unexpected success: {value:?}"), + }, + SysOpOutput::Ready(Ok(value)) => format!("unexpected success: {value:?}"), + } + }) +} + +/// Complete an in-flight host call from JS. +/// +/// Exposed to JS as `completeHostCall(callId, isError, content)`. +/// +/// On success (`is_error == 0`), `content` is a protobuf-encoded `InboundValue` +/// (host→engine direction, no type metadata — engine re-validates against the +/// declared return type). +/// +/// On error (`is_error != 0`), `content` is a protobuf-encoded `InboundValue` +/// representing the thrown value. The host bridge SDK wraps native exceptions +/// in a synthetic `Instance` of class `baml.errors.HostCallable` carrying +/// `message` / `class_name` / `language` / `traceback` fields; codegenned +/// BAML errors flow through as their own `Instance` shape. The engine's +/// `materialize_host_throw` runs the declared-throws contract check on the +/// decoded value and either re-injects it as a catchable throw or escalates +/// to a `HostContractViolation` panic. +/// +/// `call_id` is globally unique, so it resolves the originating runtime's +/// pending call unambiguously. Returns `true` when a live entry was removed and +/// completed. An unknown ID returns `false` after a bounded warning, making a +/// Promise settlement racing with cancellation an observable benign stale +/// completion. +#[wasm_bindgen(js_name = completeHostCall)] +pub fn complete_host_call(call_id: u32, is_error: i32, content: &[u8]) -> bool { + let completion = IN_FLIGHT.with(|cell| cell.borrow_mut().remove(&call_id)); + let Some(in_flight) = completion else { + log::warn!("completeHostCall for unknown call id {call_id}"); + return false; + }; + let InFlightHostCall { + operation, + completion, + } = in_flight; + + // Strict 0/1 contract: any other value is a bridge wire-protocol bug + // (an `i32` could carry uninitialised memory, a forgotten cast, or + // someone repurposing the flag) — surface it as `BridgeFailure` so the + // bug is loud, instead of silently aliasing into the throw branch. + if is_error != 0 && is_error != 1 { + completion.complete(Err(OpError::new( + operation, + sys_types::VmInternalError::BridgeFailure { + message: format!( + "completeHostCall: invalid isError value {is_error}; \ + expected 0 (success) or 1 (error)" + ), + }, + ))); + return true; + } + + if is_error == 0 { + // Success: decode InboundValue → BexExternalValue. + if content.is_empty() { + // No payload → Null return. + completion.complete(Ok(BexExternalValue::Null)); + return true; + } + let inbound = match InboundValue::decode(content) { + Ok(v) => v, + Err(e) => { + completion.complete(Err(OpError::new( + operation, + sys_types::VmInternalError::BridgeFailure { + message: format!("completeHostCall decode failure: {e}"), + }, + ))); + return true; + } + }; + match inbound_to_external(inbound, &HANDLE_TABLE) { + Ok(v) => completion.complete(Ok(v)), + Err(e) => completion.complete(Err(OpError::new( + operation, + sys_types::VmInternalError::BridgeFailure { + message: format!("completeHostCall decode failure: {e}"), + }, + ))), + } + } else { + // Throw: decode `InboundValue` → `BexExternalValue` → engine. The + // engine's `materialize_host_throw` runs the declared-throws + // contract check on the decoded value. + let mapped = if content.is_empty() { + // An empty throw payload is a host bridge bug, not a user + // contract violation: `is_error == 1` requires a protobuf- + // encoded `InboundValue`, and only the bridge itself decides + // what to send on the wire. A misbehaving bridge is an + // infrastructure fault — surface it as `BridgeFailure` (which + // codegens to `baml.panics.SdkPanic` on the host side), not as + // `HostContractViolation` (which would falsely accuse the + // user's callable of returning the wrong shape). + OpError::new( + operation, + sys_types::VmInternalError::BridgeFailure { + message: "host bridge called completeHostCall(isError=1) \ + with no payload; expected a protobuf-encoded \ + InboundValue describing the thrown value" + .to_string(), + }, + ) + } else { + match InboundValue::decode(content) { + Ok(inbound) => match inbound_to_external(inbound, &HANDLE_TABLE) { + Ok(v) => OpError::host_thrown_value(operation, v), + Err(e) => OpError::new( + operation, + sys_types::VmInternalError::BridgeFailure { + message: format!("completeHostCall throw-payload decode failure: {e}"), + }, + ), + }, + Err(e) => OpError::new( + operation, + sys_types::VmInternalError::BridgeFailure { + message: format!("completeHostCall throw-payload decode failure: {e}"), + }, + ), + } + }; + completion.complete(Err(mapped)); + } + true +} + +// ============================================================================ +// host_release_dispatch wiring (drop of last HostValueArc clone) +// ============================================================================ + +/// `HostReleaseFn` C-ABI handler installed via [`host_release_dispatch::install`]. +/// +/// Fires (via the engine's `host_release_dispatch::drain()` at a safepoint) +/// when the last Rust clone of the corresponding `HostValueArc` is dropped — +/// see `bex_project::host_release_dispatch`. Dropping the +/// [`SendWrapper`] releases the underlying `js_sys::Function`, +/// allowing the user's JS callable to become GC-eligible. +/// +/// Routes by the bare `key` against the process-global [`CALLABLES`] table. +/// Keys are globally unique, so this removes exactly the right entry regardless +/// of which runtime registered the callable. +extern "C" fn host_release_callback(key: u64) { + CALLABLES.with(|cell| { + cell.borrow_mut().remove(&key); + }); + HOST_VALUE_RELEASE_CALLBACK.with(|cell| { + let callback = cell + .borrow() + .as_ref() + .map(|callback| callback.inner().clone()); + if let Some(callback) = callback { + if let Err(error) = callback.call1(&JsValue::NULL, &JsValue::from(key)) { + log::warn!("host-value release callback threw: {error:?}"); + } + } + }); +} + +/// Install the global release-dispatch callback. Idempotent (first call wins). +/// +/// Called from [`WasmHost::new`] so the wiring is present whenever a runtime +/// that supports host callables is active. +fn ensure_release_dispatch_installed() { + // First-call-wins: subsequent installs return `Err(AlreadyInstalled)` + // which we ignore (the same fn pointer is being installed every time). + let _ = host_release_dispatch::install(host_release_callback); +} + +// ============================================================================ +// WasmHost: IoNamespaceHost impl +// ============================================================================ + +/// WASM host-value dispatch sysop impl. +/// +/// One instance per `BamlWasmRuntime`, wired into its `SysOps`. Holds *this* +/// runtime's JS `host_dispatch` callback (supplied at `BamlWasmRuntime::create`). +/// Implements [`IoNamespaceHost::call_host_value`] by encoding the inbound BAML +/// args to a `BamlOutboundValue` list, allocating a fresh (globally-unique) +/// call id, installing a `CompletionHandle`, and firing *this runtime's* JS +/// dispatch callback. Because the impl runs on the `WasmHost` that owns the +/// originating runtime, the dispatch always reaches the correct wrapper — no +/// global routing is needed for the engine→host direction. +#[doc(hidden)] +pub struct WasmHost { + dispatch: WasmHostDispatch, +} + +enum WasmHostDispatch { + DirectRegistry, + RuntimeCallback(SendWrapper), +} + +impl WasmHost { + /// Construct a new `WasmHost` bound to this runtime's `host_dispatch` + /// callback, and install the global release callback (idempotent — only the + /// first installation wins; the same fn pointer is installed each time). + pub fn new(host_dispatch: Function, dispatch_registered_directly: bool) -> Self { + ensure_release_dispatch_installed(); + let dispatch = if dispatch_registered_directly { + WasmHostDispatch::DirectRegistry + } else { + WasmHostDispatch::RuntimeCallback(SendWrapper::new(host_dispatch)) + }; + Self { dispatch } + } + + /// Construct an SDK host that dispatches only callables in [`CALLABLES`]. + #[allow(dead_code)] + pub(crate) fn direct() -> Self { + ensure_release_dispatch_installed(); + Self { + dispatch: WasmHostDispatch::DirectRegistry, + } + } + + /// Invoke a registered callable through the existing host-value wire and completion path. + pub(crate) fn call_registered_callable( + &self, + originating_sysop: SysOp, + callable: &HostValueArc, + positional: &[BexExternalValue], + optional: &IndexMap, + ) -> SysOpOutput { + let sync_mode = web_sync_mode_active(); + if sync_mode && !matches!(originating_sysop, SysOp::BamlFsRead) { + return SysOpOutput::err(web_sync_failure( + originating_sysop, + "because it requires asynchronous JavaScript work", + )); + } + + let options = CffiHandleTableOptions::for_wire(); + let encoded = match bridge_ctypes::build_to_host_call(positional, optional, &options) { + Ok(to_host_call) => to_host_call.encode_to_vec(), + Err(error) => { + return SysOpOutput::err(sys_types::VmInternalError::BridgeFailure { + message: format!("failed to encode host-call arguments: {error}"), + }); + } + }; + + let call_id = next_call_id(); + let (result, completion) = SysOpResult::pending(originating_sysop); + let inserted = insert_in_flight(call_id, originating_sysop, completion); + if inserted { + let call_id_js = JsValue::from_f64(f64::from(call_id)); + let args_js = js_sys::Uint8Array::new_with_length( + encoded + .len() + .try_into() + .expect("host-call args payload exceeds u32::MAX"), + ); + args_js.copy_from(&encoded); + + let dispatched = match &self.dispatch { + WasmHostDispatch::DirectRegistry => { + let registered = CALLABLES.with(|cell| { + cell.borrow() + .get(&callable.key) + .map(|callable| callable.inner().clone()) + }); + match registered { + Some(registered) => { + registered.call2(&JsValue::NULL, &call_id_js, &args_js.into()) + } + None => Err(JsValue::from_str(&format!( + "host callable key {} is not registered", + callable.key + ))), + } + } + WasmHostDispatch::RuntimeCallback(host_dispatch) => { + let key_js = JsValue::from(callable.key); + host_dispatch.call3(&JsValue::NULL, &key_js, &call_id_js, &args_js.into()) + } + }; + + if let Err(error) = dispatched { + let popped = IN_FLIGHT.with(|cell| cell.borrow_mut().remove(&call_id)); + if let Some(in_flight) = popped { + let message = error.as_string().unwrap_or_else(|| format!("{error:?}")); + in_flight.completion.complete(Err(OpError::new( + in_flight.operation, + sys_types::VmInternalError::BridgeFailure { + message: format!("host_dispatch JS callback threw: {message}"), + }, + ))); + } + } + + if sync_mode { + let pending = IN_FLIGHT.with(|cell| cell.borrow_mut().remove(&call_id)); + if let Some(in_flight) = pending { + in_flight.completion.complete(Err(OpError::new( + in_flight.operation, + web_sync_failure( + in_flight.operation, + "because its JavaScript adapter did not complete reentrantly", + ), + ))); + } + } + } + + drain_pending_unvalidated(result, call_id, inserted) + } +} + +impl io::IoNamespaceHost for WasmHost { + fn call_host_value( + &self, + _heap: &Arc, + _call_id: CallId, + handle: BexExternalValue, + args: Vec, + type_arg_0: baml_type::RuntimeTy, + _type_arg_1: baml_type::RuntimeTy, + _ctx: &SysOpContext, + ) -> SysOpOutput { + // Extract the HostValueArc from the incoming handle. + let host_arc = match handle { + BexExternalValue::HostValue(arc) => arc, + other => { + return SysOpOutput::err(VmBamlError::InvalidArgument { + message: format!("expected HostValue, got {other:?}"), + }); + } + }; + + // The VM split the call's args by the callable's declared params and + // packed them as `[positional_array, optional_map]` (see + // `host_closure_call_sysop`). Unpack and encode them into the + // `BamlToHostCall`'s flat `args` list (mirrors + // `sys_native::host_impls::call_host_value`): required args first, then + // the supplied optionals (tagged + keyed by name). The JS dispatch + // wrapper applies its calling convention. + let mut pack = args.into_iter(); + let positional = match pack.next() { + Some(BexExternalValue::Array { items, .. }) => items, + other => { + return SysOpOutput::err(sys_types::VmInternalError::BridgeFailure { + message: format!( + "host-call args pack[0] must be the positional array, got {other:?}" + ), + }); + } + }; + let optional = match pack.next() { + Some(BexExternalValue::Map { entries, .. }) => entries, + other => { + return SysOpOutput::err(sys_types::VmInternalError::BridgeFailure { + message: format!( + "host-call args pack[1] must be the optional map, got {other:?}" + ), + }); + } + }; + validate_host_output( + self.call_registered_callable( + SysOp::BamlHostCallHostValue, + host_arc.as_ref(), + &positional, + &optional, + ), + type_arg_0, + ) + } +} + +/// Convert a `SysOpResult` from `SysOpResult::pending` into a `SysOpOutput` +/// suitable for the trait return type. The host's result lands as a +/// `BexExternalValue`; on error we strip the `SysOp` wrapper to expose only +/// the kind (the trait's contract). +/// +/// On success the host's returned value is strictly validated against the +/// declared return type `expected` via the shared [`validate_host_return`] +/// guard — the same check the native bridge performs — so a host that returns +/// a value violating the declared `T` surfaces as a catchable +/// `root.errors.HostCallable` rather than corrupting the VM. Class *field +/// types* are validated engine-side where the resolved schema is available; +/// this guard covers scalar discrimination (`int` ≠ `float`), container +/// recursion, enum identity, and class-name identity. +/// +/// When `install_guard` is true, `call_id` owns the in-flight entry installed +/// by the caller, and a [`WasmInflightGuard`] for it is moved into the async +/// future so that cancellation (the future being dropped before completion) +/// evicts the dangling entry. `install_guard` is `false` on the id-collision +/// path, where the live entry under `call_id` belongs to another call — guarding +/// it would evict that entry, so no guard is built and `result` simply resolves +/// to the collision error. The `Ready` arms drop the guard inline — also +/// correct, just not reached in practice since `SysOpResult::pending` always +/// yields `Async`. +fn drain_pending_unvalidated( + result: SysOpResult, + call_id: u32, + install_guard: bool, +) -> SysOpOutput { + let guard = install_guard.then_some(WasmInflightGuard { call_id }); + match result { + SysOpResult::Ready(Ok(value)) => SysOpOutput::ok(value), + SysOpResult::Ready(Err(err)) => match err.payload { + sys_types::OpErrorPayload::Vm(kind) => SysOpOutput::err(kind), + // `SysOpResult::pending` always yields `Async`, so a Ready(Err) + // here can't actually originate from the host-throw path. + sys_types::OpErrorPayload::HostThrown(_) => { + unreachable!("Ready(Err) is never produced for the host-callable sysop") + } + }, + SysOpResult::Async(fut) => { + SysOpOutput::Async(Box::pin(crate::send_wrapper::SendFuture(async move { + // Move the guard into the future so cancellation (future drop) + // evicts the in-flight entry. `None` on the collision path — + // there is no entry of ours to evict and `fut` resolves to the + // collision error. + let _guard = guard; + // `?` propagates `OpError → VmRustFnError` via the `From` + // impl in `sys_types` while preserving host-thrown payloads. + Ok(fut.await?) + }))) + } + } +} + +fn validate_host_output( + output: SysOpOutput, + expected: baml_type::RuntimeTy, +) -> SysOpOutput { + match output { + SysOpOutput::Ready(Ok(value)) => match validate_host_return_value(&value, &expected) { + Ok(()) => SysOpOutput::ok(value), + Err(error) => SysOpOutput::err(error), + }, + SysOpOutput::Ready(Err(error)) => SysOpOutput::Ready(Err(error)), + SysOpOutput::Async(future) => { + SysOpOutput::Async(Box::pin(crate::send_wrapper::SendFuture(async move { + let value = future.await?; + validate_host_return_value(&value, &expected)?; + Ok(value) + }))) + } + } +} + +/// Strictly validate a host-returned value against the declared return type. +/// A mismatch is a `baml.panics.HostContractViolation` panic — the host has +/// violated its typed contract, so the call cannot be reasonably continued. +/// Mirrors the native bridge's `validate_return_value` in +/// `sys_native::host_impls`. +fn validate_host_return_value( + value: &BexExternalValue, + expected: &baml_type::RuntimeTy, +) -> Result<(), VmRustFnError> { + validate_host_return(value, expected).map_err(|err| { + sys_types::VmPanic::HostContractViolation { + message: format!( + "host callable returned a value of the wrong type: {err} (expected {expected})" + ), + class_name: None, + language: None, + } + .into() + }) +} + +#[cfg(test)] +mod tests { + //! Host-target unit tests for the id-routing layer. + //! + //! The full BAML→host round trip is exercised by the `wasm_bindgen_test` + //! suite in `tests/host_callable.rs` (including the two-runtime + //! `two_runtimes_dispatch_through_their_own_wrapper` test), which needs a + //! wasm runtime (`wasm-pack test --node`). These plain `#[test]`s + //! cover what the routing layer guarantees *without* JS: globally-unique id + //! minting and `complete_host_call`'s by-id resolution against the shared + //! in-flight table — the property that lets two runtimes share one global + //! table yet never resolve each other's pending calls. + + use std::collections::HashSet; + + use bridge_ctypes::baml_bridge::cffi::inbound_value::Value as InboundVariant; + + use super::*; + + fn insert_pending_call() -> (u32, SysOpResult) { + let call_id = next_call_id(); + let (result, completion) = SysOpResult::pending(SysOp::BamlHostCallHostValue); + assert!(insert_in_flight( + call_id, + SysOp::BamlHostCallHostValue, + completion + )); + (call_id, result) + } + + /// Minted keys must be unique, monotonic, and never 0 (0 is the reserved + /// "invalid" sentinel). Globally-unique keys are what make release-by-key + /// routing unambiguous across runtimes. + #[test] + fn next_key_is_unique_and_nonzero() { + let mut seen = HashSet::new(); + for _ in 0..1000 { + let k = next_key(); + assert_ne!(k, 0, "minted key must never be the reserved 0"); + assert!(seen.insert(k), "minted key {k} was handed out twice"); + } + } + + /// Same invariants for call ids. + #[test] + fn next_call_id_is_unique_and_nonzero() { + let mut seen = HashSet::new(); + for _ in 0..1000 { + let id = next_call_id(); + assert_ne!(id, 0, "minted call id must never be the reserved 0"); + assert!(seen.insert(id), "minted call id {id} was handed out twice"); + } + } + + /// `complete_host_call` must resolve *only* the in-flight entry whose id + /// matches, leaving other runtimes' entries untouched. This is the + /// table-level guarantee behind two-runtime independence: both runtimes + /// share the one global `IN_FLIGHT` table but mint disjoint globally-unique + /// ids, so a completion for runtime A's id can never resolve runtime B's + /// pending call. + #[test] + fn complete_host_call_resolves_only_the_matching_id() { + // Two pending calls standing in for two different runtimes. + let id_a = next_call_id(); + let id_b = next_call_id(); + let (result_a, completion_a) = SysOpResult::pending(SysOp::BamlHostCallHostValue); + let (result_b, completion_b) = SysOpResult::pending(SysOp::BamlHostCallHostValue); + IN_FLIGHT.with(|cell| { + let mut t = cell.borrow_mut(); + t.insert( + id_a, + InFlightHostCall { + operation: SysOp::BamlHostCallHostValue, + completion: completion_a, + }, + ); + t.insert( + id_b, + InFlightHostCall { + operation: SysOp::BamlHostCallHostValue, + completion: completion_b, + }, + ); + }); + + let SysOpResult::Async(fut_a) = result_a else { + panic!("pending() must produce an async result"); + }; + let SysOpResult::Async(mut fut_b) = result_b else { + panic!("pending() must produce an async result"); + }; + + // Complete A with an empty success payload (→ Null). B must stay pending. + complete_host_call(id_a, 0, &[]); + + // A's future is now resolved with Null; B's table entry is still present. + let a = futures::executor::block_on(fut_a).expect("A completes successfully"); + assert!(matches!(a, BexExternalValue::Null), "A resolved with Null"); + IN_FLIGHT.with(|cell| { + assert!( + cell.borrow().contains_key(&id_b), + "completing A must not touch B's in-flight entry" + ); + assert!( + !cell.borrow().contains_key(&id_a), + "A's entry must be removed once completed" + ); + }); + + // An unknown id is a silent no-op (stale completion / dropped runtime). + complete_host_call(u32::MAX, 0, &[]); + + // B's future must still be pending (not yet polled to completion). + let waker = futures::task::noop_waker(); + let mut cx = std::task::Context::from_waker(&waker); + assert!( + std::future::Future::poll(std::pin::Pin::new(&mut fut_b), &mut cx).is_pending(), + "B must remain pending until its own id is completed" + ); + + // Now complete B and confirm it resolves independently. + complete_host_call(id_b, 0, &[]); + let b = futures::executor::block_on(fut_b).expect("B completes successfully"); + assert!(matches!(b, BexExternalValue::Null), "B resolved with Null"); + } + + /// Cancellation: the async future returned by `drain_pending` carries a + /// [`WasmInflightGuard`]. Dropping the future before completion (the engine's + /// cancel arm) must evict the in-flight entry so it does not leak. + #[test] + fn guard_drop_evicts_in_flight_entry_on_cancel() { + let call_id = next_call_id(); + let (result, completion) = SysOpResult::pending(SysOp::BamlHostCallHostValue); + assert!( + insert_in_flight(call_id, SysOp::BamlHostCallHostValue, completion), + "fresh id must insert cleanly" + ); + assert!( + IN_FLIGHT.with(|c| c.borrow().contains_key(&call_id)), + "entry must be present after insert" + ); + + // `drain_pending_unvalidated` wraps the result + guard into the returned future. + let SysOpOutput::Async(fut) = drain_pending_unvalidated(result, call_id, true) else { + panic!("pending() must yield an async output"); + }; + + // Model the engine dropping the cancelled future. + drop(fut); + + assert!( + !IN_FLIGHT.with(|c| c.borrow().contains_key(&call_id)), + "guard drop on cancel must evict the in-flight entry (no leak)" + ); + } + + /// Normal completion removes the entry; a later guard drop is a benign + /// no-op (no double-remove, no panic). + #[test] + fn guard_drop_after_normal_completion_is_noop() { + let call_id = next_call_id(); + let (result, completion) = SysOpResult::pending(SysOp::BamlHostCallHostValue); + assert!( + insert_in_flight(call_id, SysOp::BamlHostCallHostValue, completion), + "fresh id must insert cleanly" + ); + + let SysOpOutput::Async(fut) = drain_pending_unvalidated(result, call_id, true) else { + panic!("pending() must yield an async output"); + }; + + // Host completes normally — this removes-and-fires the handle. + complete_host_call(call_id, 0, &[]); + assert!( + !IN_FLIGHT.with(|c| c.borrow().contains_key(&call_id)), + "completion must remove the in-flight entry" + ); + + // The future resolves (Null) and its guard drop is a no-op. + let value = futures::executor::block_on(fut).expect("completes successfully"); + assert!(matches!(value, BexExternalValue::Null)); + assert!( + !IN_FLIGHT.with(|c| c.borrow().contains_key(&call_id)), + "entry stays removed after the future (and its guard) is dropped" + ); + } + + #[test] + fn malformed_completion_is_bridge_failure_and_removes_entry() { + let (call_id, result) = insert_pending_call(); + complete_host_call(call_id, 0, &[0xff]); + assert!(!IN_FLIGHT.with(|cell| cell.borrow().contains_key(&call_id))); + + let SysOpResult::Async(future) = result else { + panic!("pending() must produce an async result"); + }; + let error = futures::executor::block_on(future).expect_err("malformed bytes must fail"); + assert!(matches!( + error.payload, + sys_types::OpErrorPayload::Vm(sys_types::VmRustFnError::InternalError( + sys_types::VmInternalError::BridgeFailure { .. } + )) + )); + } + + #[test] + fn invalid_is_error_is_bridge_failure_and_removes_entry() { + let (call_id, result) = insert_pending_call(); + assert!(complete_host_call(call_id, 2, &[])); + assert!(!IN_FLIGHT.with(|cell| cell.borrow().contains_key(&call_id))); + + let SysOpResult::Async(future) = result else { + panic!("pending() must produce an async result"); + }; + let error = futures::executor::block_on(future).expect_err("invalid isError must fail"); + assert!(matches!( + error.payload, + sys_types::OpErrorPayload::Vm(sys_types::VmRustFnError::InternalError( + sys_types::VmInternalError::BridgeFailure { .. } + )) + )); + } + + #[test] + fn colliding_call_id_is_bridge_failure_without_replacing_live_call() { + let call_id = next_call_id(); + let (first_result, first_completion) = SysOpResult::pending(SysOp::BamlHostCallHostValue); + assert!(insert_in_flight( + call_id, + SysOp::BamlHostCallHostValue, + first_completion + )); + + let (second_result, second_completion) = SysOpResult::pending(SysOp::BamlHostCallHostValue); + assert!(!insert_in_flight( + call_id, + SysOp::BamlHostCallHostValue, + second_completion + )); + assert!(IN_FLIGHT.with(|cell| cell.borrow().contains_key(&call_id))); + + let SysOpResult::Async(second_future) = second_result else { + panic!("pending() must produce an async result"); + }; + let error = futures::executor::block_on(second_future) + .expect_err("colliding call id must fail the new call"); + assert!(matches!( + error.payload, + sys_types::OpErrorPayload::Vm(sys_types::VmRustFnError::InternalError( + sys_types::VmInternalError::BridgeFailure { .. } + )) + )); + + assert!(complete_host_call(call_id, 0, &[])); + let SysOpResult::Async(first_future) = first_result else { + panic!("pending() must produce an async result"); + }; + let value = futures::executor::block_on(first_future) + .expect("the original live call must remain completable"); + assert!(matches!(value, BexExternalValue::Null)); + } + + #[test] + fn structured_user_error_removes_entry() { + let (call_id, result) = insert_pending_call(); + let payload = InboundValue { + value: Some(InboundVariant::StringValue("boom".to_string())), + } + .encode_to_vec(); + complete_host_call(call_id, 1, &payload); + assert!(!IN_FLIGHT.with(|cell| cell.borrow().contains_key(&call_id))); + + let SysOpResult::Async(future) = result else { + panic!("pending() must produce an async result"); + }; + let error = futures::executor::block_on(future).expect_err("throw must fail the sysop"); + assert!(matches!( + error.payload, + sys_types::OpErrorPayload::HostThrown(ref value) + if matches!(value.as_ref(), BexExternalValue::String(value) if value.as_str() == "boom") + )); + } + + #[test] + fn duplicate_completion_is_a_noop_after_first_removal() { + let (call_id, result) = insert_pending_call(); + complete_host_call(call_id, 0, &[]); + complete_host_call(call_id, 0, &[0xff]); + assert!(!IN_FLIGHT.with(|cell| cell.borrow().contains_key(&call_id))); + + let SysOpResult::Async(future) = result else { + panic!("pending() must produce an async result"); + }; + let value = futures::executor::block_on(future).expect("first completion wins"); + assert!(matches!(value, BexExternalValue::Null)); + } +} diff --git a/baml_language/crates/sys_wasm/src/lib.rs b/baml_language/crates/sys_wasm/src/lib.rs new file mode 100644 index 00000000000..226f665f385 --- /dev/null +++ b/baml_language/crates/sys_wasm/src/lib.rs @@ -0,0 +1,62 @@ +//! Narrow Web implementations of the BAML `SysOps` table. +//! +//! JavaScript platform calls reuse the same `HOST_VALUE_CALLABLE` registry, +//! protobuf wire format, and completion table as user-provided host callables. + +use std::{cell::RefCell, sync::Arc}; + +mod host_value; +mod send_wrapper; +mod web_sysops; + +use bex_project::HostValueArc; +use host_value::retain_host_callable; +pub use host_value::{ + WasmHost, complete_host_call, mint_host_value_key, register_host_callable, + register_host_value_release_callback, release_host_callable, test_fire_host_release, + test_host_callable_count, test_host_release_callback_installed, test_in_flight_host_call_count, + test_missing_host_callable_error, test_sync_pending_host_callable_error, with_web_sync_mode, +}; +#[doc(hidden)] +pub use send_wrapper::{SendFuture, SendWrapper}; +use web_sysops::{WebFs, WebHttp}; + +#[derive(Clone)] +struct WebSysopConfig { + fetch: Arc, + read_file_sync: Arc, +} + +thread_local! { + static WEB_SYSOP_CONFIG: RefCell> = const { RefCell::new(None) }; +} + +/// Configure the bridge-owned callables used by the Web `SysOp` table. +pub fn configure_web_sysops(fetch_key: u64, read_file_sync_key: u64) -> Result<(), String> { + let config = WebSysopConfig { + fetch: retain_host_callable(fetch_key)?, + read_file_sync: retain_host_callable(read_file_sync_key)?, + }; + WEB_SYSOP_CONFIG.with(|slot| { + slot.borrow_mut().replace(config); + }); + Ok(()) +} + +/// Build the exact Web capability surface: fetch, readFileSync, and user host callables. +pub fn build() -> Result { + let config = WEB_SYSOP_CONFIG + .with(|slot| slot.borrow().clone()) + .ok_or_else(|| { + "configure_web_sysops must be called before initializing the Web runtime".to_string() + })?; + let host = Arc::new(WasmHost::direct()); + let http = Arc::new(WebHttp::new(host.clone(), config.fetch)); + let fs = Arc::new(WebFs::new(host.clone(), config.read_file_sync)); + + Ok(sys_ops::SysOpsBuilder::new() + .with_http_instance(http) + .with_fs_instance(fs) + .with_host_instance(host) + .build()) +} diff --git a/baml_language/crates/sys_wasm/src/send_wrapper.rs b/baml_language/crates/sys_wasm/src/send_wrapper.rs new file mode 100644 index 00000000000..9e74d962702 --- /dev/null +++ b/baml_language/crates/sys_wasm/src/send_wrapper.rs @@ -0,0 +1,70 @@ +//! `SendFuture` and `SendWrapper` for WASM async compatibility. +//! +//! `SysOpFn` requires `Send + Sync` and `OpFuture` requires `Send`. But +//! `js_sys::Function` and `JsFuture` are `!Send`. On wasm32-unknown-unknown +//! (single-threaded), this is safe to bypass. +#![allow(unsafe_code)] + +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +/// A future wrapper that unsafely implements Send on WASM. +/// +/// # Safety +/// +/// wasm32-unknown-unknown is single-threaded, so this is safe. +/// This wrapper should only be used on WASM targets. +pub struct SendFuture(pub F); + +// SAFETY: wasm32-unknown-unknown is single-threaded +unsafe impl Send for SendFuture {} + +impl Future for SendFuture { + type Output = F::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + // SAFETY: We're not moving F out of the Pin, just projecting to it + unsafe { self.map_unchecked_mut(|s| &mut s.0).poll(cx) } + } +} + +/// Wrapper for values that need Send+Sync on WASM. +/// +/// # Safety +/// +/// wasm32-unknown-unknown is single-threaded, so this is safe. +/// This wrapper should only be used on WASM targets. +#[derive(Clone)] +pub struct SendWrapper(pub T); + +// SAFETY: wasm32-unknown-unknown is single-threaded +unsafe impl Send for SendWrapper {} +unsafe impl Sync for SendWrapper {} + +impl SendWrapper { + /// Create a new `SendWrapper`. + pub fn new(value: T) -> Self { + Self(value) + } + + /// Get a reference to the inner value. + pub fn inner(&self) -> &T { + self + } + + /// Take the inner value (for use when the wrapper is stored in a container). + pub fn into_inner(self) -> T { + self.0 + } +} + +impl std::ops::Deref for SendWrapper { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/baml_language/crates/sys_wasm/src/web_sysops.rs b/baml_language/crates/sys_wasm/src/web_sysops.rs new file mode 100644 index 00000000000..199cf67fb87 --- /dev/null +++ b/baml_language/crates/sys_wasm/src/web_sysops.rs @@ -0,0 +1,701 @@ +use std::sync::{Arc, Mutex}; + +use bex_project::{BexExternalValue, Handle, HostValueArc}; +use indexmap::{IndexMap, indexmap}; +use num_traits::ToPrimitive as _; +use sys_ops::io::{self, IoClassHttpResponse, IoNamespaceFs, IoNamespaceHttp}; +use sys_types::{ + BexHeap, CallId, OpErrorBody, SysOp, SysOpContext, SysOpOutput, VmBamlError, VmInternalError, + VmRustFnError, +}; + +use crate::host_value::WasmHost; + +const UNSUPPORTED: &str = "Operation not supported by the Web runtime"; + +pub(crate) struct WebHttp { + host: Arc, + fetch: Arc, +} + +impl WebHttp { + pub(crate) fn new(host: Arc, fetch: Arc) -> Self { + Self { host, fetch } + } + + fn send( + &self, + operation: SysOp, + request: io::owned::http::Request, + timeout_nanos: &num_bigint::BigInt, + ) -> SysOpOutput { + if timeout_nanos.sign() == num_bigint::Sign::Minus { + return SysOpOutput::err(VmBamlError::InvalidArgument { + message: "HTTP timeout must be non-negative".to_string(), + }); + } + + let headers = request + .headers + .into_iter() + .map(|(name, value)| { + array(vec![ + BexExternalValue::String(name.into()), + BexExternalValue::String(value.into()), + ]) + }) + .collect(); + let request_value = map(indexmap! { + "method".to_string() => BexExternalValue::String(request.method.into()), + "url".to_string() => BexExternalValue::String(request.url.into()), + "headers".to_string() => array(headers), + "body".to_string() => BexExternalValue::Uint8Array(request.body.into_bytes()), + "timeoutNanos".to_string() => BexExternalValue::Bigint(timeout_nanos.clone()), + }); + let duration_ms = timeout_duration_ms(timeout_nanos); + map_output( + self.host.call_registered_callable( + operation, + self.fetch.as_ref(), + &[request_value], + &IndexMap::new(), + ), + move |value| parse_fetch_result(value, duration_ms), + ) + } +} + +#[derive(Debug)] +struct BufferedResponseBody(Mutex>>); + +fn take_response_body(response: &io::owned::http::Response) -> Result, VmRustFnError> { + let body = response + ._body + .downcast_ref::() + .ok_or_else(|| VmInternalError::BridgeFailure { + message: "Web HTTP response contains an invalid body resource".to_string(), + })?; + body.0 + .lock() + .map_err(|_| VmInternalError::BridgeFailure { + message: "Web HTTP response body lock was poisoned".to_string(), + })? + .take() + .ok_or_else(|| { + VmBamlError::Io { + message: "response body has already been consumed".to_string(), + } + .into() + }) +} + +impl IoClassHttpResponse for WebHttp { + fn text( + &self, + _heap: &Arc, + _call_id: CallId, + response: io::owned::http::Response, + _ctx: &SysOpContext, + ) -> SysOpOutput { + match take_response_body(&response).and_then(|bytes| { + String::from_utf8(bytes).map_err(|error| { + VmBamlError::Io { + message: format!("HTTP response body is not valid UTF-8: {error}"), + } + .into() + }) + }) { + Ok(text) => SysOpOutput::ok(text), + Err(error) => SysOpOutput::err(error), + } + } + + fn bytes( + &self, + _heap: &Arc, + _call_id: CallId, + response: io::owned::http::Response, + _ctx: &SysOpContext, + ) -> SysOpOutput> { + match take_response_body(&response) { + Ok(bytes) => SysOpOutput::ok(bytes), + Err(error) => SysOpOutput::err(error), + } + } + + fn new( + &self, + _heap: &Arc, + _call_id: CallId, + _status_code: i64, + _headers: IndexMap, + _body: Vec, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } + + fn new_streaming( + &self, + _heap: &Arc, + _call_id: CallId, + _status_code: i64, + _headers: IndexMap, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } + + fn write( + &self, + _heap: &Arc, + _call_id: CallId, + _response: io::owned::http::Response, + _data: Vec, + _ctx: &SysOpContext, + ) -> SysOpOutput<()> { + unsupported() + } + + fn end( + &self, + _heap: &Arc, + _call_id: CallId, + _response: io::owned::http::Response, + _ctx: &SysOpContext, + ) -> SysOpOutput<()> { + unsupported() + } +} + +impl io::IoClassHttpTlsConfig for WebHttp { + fn _new( + &self, + _heap: &Arc, + _call_id: CallId, + _cert_pem: Vec, + _key_pem: Vec, + _allow_tls1_2: bool, + _handshake_timeout_nanos: Arc, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } +} + +impl io::IoClassHttpServer for WebHttp { + fn bind( + &self, + _heap: &Arc, + _call_id: CallId, + _addr: String, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } + + fn _serve( + &self, + _heap: &Arc, + _call_id: CallId, + _server: io::owned::http::Server, + _handler: Handle, + _tls_config: Option, + _allow_http1: bool, + _allow_http2: bool, + _max_body_size: i64, + _max_connections: i64, + _header_read_timeout_nanos: Arc, + _ctx: &SysOpContext, + ) -> SysOpOutput<()> { + unsupported() + } +} + +impl io::IoClassHttpSseStream for WebHttp { + fn next( + &self, + _heap: &Arc, + _call_id: CallId, + _stream: io::owned::http::SseStream, + _ctx: &SysOpContext, + ) -> SysOpOutput> { + unsupported() + } + + fn close( + &self, + _heap: &Arc, + _call_id: CallId, + _stream: io::owned::http::SseStream, + _ctx: &SysOpContext, + ) -> SysOpOutput<()> { + unsupported() + } +} + +impl IoNamespaceHttp for WebHttp { + fn _fetch( + &self, + _heap: &Arc, + _call_id: CallId, + url: String, + timeout_nanos: Arc, + _ctx: &SysOpContext, + ) -> SysOpOutput { + self.send( + SysOp::BamlHttpFetch, + io::owned::http::Request { + method: "GET".to_string(), + url, + headers: IndexMap::new(), + body: String::new(), + }, + timeout_nanos.as_ref(), + ) + } + + fn _send( + &self, + _heap: &Arc, + _call_id: CallId, + request: io::owned::http::Request, + timeout_nanos: Arc, + _ctx: &SysOpContext, + ) -> SysOpOutput { + self.send(SysOp::BamlHttpSend, request, timeout_nanos.as_ref()) + } + + fn fetch_sse( + &self, + _heap: &Arc, + _call_id: CallId, + _request: io::owned::http::Request, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } +} + +pub(crate) struct WebFs { + host: Arc, + read_file_sync: Arc, +} + +impl WebFs { + pub(crate) fn new(host: Arc, read_file_sync: Arc) -> Self { + Self { + host, + read_file_sync, + } + } +} + +impl io::IoClassFsFile for WebFs { + fn text( + &self, + _h: &Arc, + _c: CallId, + _f: io::owned::fs::File, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } + fn bytes( + &self, + _h: &Arc, + _c: CallId, + _f: io::owned::fs::File, + _ctx: &SysOpContext, + ) -> SysOpOutput> { + unsupported() + } + fn read( + &self, + _h: &Arc, + _c: CallId, + _f: io::owned::fs::File, + _n: i64, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } + fn read_bytes( + &self, + _h: &Arc, + _c: CallId, + _f: io::owned::fs::File, + _n: i64, + _ctx: &SysOpContext, + ) -> SysOpOutput> { + unsupported() + } + fn close( + &self, + _h: &Arc, + _c: CallId, + _f: io::owned::fs::File, + _ctx: &SysOpContext, + ) -> SysOpOutput<()> { + unsupported() + } + fn seek_from( + &self, + _h: &Arc, + _c: CallId, + _f: io::owned::fs::File, + _whence: BexExternalValue, + _offset: i64, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } + fn write( + &self, + _h: &Arc, + _c: CallId, + _f: io::owned::fs::File, + _data: String, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } + fn write_bytes( + &self, + _h: &Arc, + _c: CallId, + _f: io::owned::fs::File, + _data: Vec, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } +} + +impl IoNamespaceFs for WebFs { + fn open( + &self, + _h: &Arc, + _c: CallId, + _path: String, + _mode: BexExternalValue, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } + fn exists( + &self, + _h: &Arc, + _c: CallId, + _path: String, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } + fn remove( + &self, + _h: &Arc, + _c: CallId, + _path: String, + _ctx: &SysOpContext, + ) -> SysOpOutput<()> { + unsupported() + } + fn remove_dir( + &self, + _h: &Arc, + _c: CallId, + _path: String, + _ctx: &SysOpContext, + ) -> SysOpOutput<()> { + unsupported() + } + fn remove_dir_all( + &self, + _h: &Arc, + _c: CallId, + _path: String, + _ctx: &SysOpContext, + ) -> SysOpOutput<()> { + unsupported() + } + fn size( + &self, + _h: &Arc, + _c: CallId, + _path: String, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } + + fn read( + &self, + _heap: &Arc, + _call_id: CallId, + path: String, + _ctx: &SysOpContext, + ) -> SysOpOutput { + map_output( + self.host.call_registered_callable( + SysOp::BamlFsRead, + self.read_file_sync.as_ref(), + &[BexExternalValue::String(path.into())], + &IndexMap::new(), + ), + parse_read_file_result, + ) + } + + fn write( + &self, + _h: &Arc, + _c: CallId, + _path: String, + _content: String, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } + fn write_bytes( + &self, + _h: &Arc, + _c: CallId, + _path: String, + _content: Vec, + _ctx: &SysOpContext, + ) -> SysOpOutput { + unsupported() + } + fn read_dir( + &self, + _h: &Arc, + _c: CallId, + _path: String, + _ctx: &SysOpContext, + ) -> SysOpOutput> { + unsupported() + } + fn mkdir( + &self, + _h: &Arc, + _c: CallId, + _path: String, + _options: io::owned::fs::MkdirOptions, + _ctx: &SysOpContext, + ) -> SysOpOutput<()> { + unsupported() + } +} + +fn parse_fetch_result( + value: BexExternalValue, + duration_ms: Option, +) -> Result { + let mut result = expect_map(value, "fetch result")?; + match take_string(&mut result, "kind", "fetch result")?.as_str() { + "ok" => { + let status_code = take_int(&mut result, "statusCode", "fetch result")?; + if !(100..=999).contains(&status_code) { + return Err(bridge_failure(format!( + "fetch result statusCode must be between 100 and 999, got {status_code}" + ))); + } + let url = take_string(&mut result, "url", "fetch result")?; + let headers = take_headers(&mut result)?; + let body = take_bytes(&mut result, "body", "fetch result")?; + Ok(io::owned::http::Response { + status_code, + headers, + url, + _body: Arc::new(BufferedResponseBody(Mutex::new(Some(body)))), + }) + } + "io" => Err(VmBamlError::Io { + message: take_string(&mut result, "message", "fetch error")?, + } + .into()), + "timeout" => Err(VmBamlError::Timeout { + message: take_string(&mut result, "message", "fetch timeout")?, + duration_ms, + } + .into()), + kind => Err(bridge_failure(format!( + "fetch result has unknown kind {kind:?}" + ))), + } +} + +fn parse_read_file_result(value: BexExternalValue) -> Result { + let mut result = expect_map(value, "readFileSync result")?; + match take_string(&mut result, "kind", "readFileSync result")?.as_str() { + "ok" => { + let bytes = take_bytes(&mut result, "bytes", "readFileSync result")?; + String::from_utf8(bytes).map_err(|error| { + VmBamlError::ParseError { + message: format!("file is not valid UTF-8: {error}"), + } + .into() + }) + } + "io" => Err(VmBamlError::Io { + message: take_string(&mut result, "message", "readFileSync error")?, + } + .into()), + "unavailable" => Err(VmBamlError::Unsupported { + message: take_string(&mut result, "message", "readFileSync unavailable")?, + } + .into()), + kind => Err(bridge_failure(format!( + "readFileSync result has unknown kind {kind:?}" + ))), + } +} + +fn take_headers( + result: &mut IndexMap, +) -> Result, VmRustFnError> { + let value = result + .swap_remove("headers") + .ok_or_else(|| bridge_failure("fetch result is missing headers"))?; + let BexExternalValue::Array { items, .. } = value else { + return Err(bridge_failure("fetch result headers must be an array")); + }; + items + .into_iter() + .map(|pair| { + let BexExternalValue::Array { mut items, .. } = pair else { + return Err(bridge_failure("fetch result header must be a pair")); + }; + if items.len() != 2 { + return Err(bridge_failure("fetch result header must contain two items")); + } + let value = expect_string(items.pop().expect("length checked"), "header value")?; + let name = expect_string(items.pop().expect("length checked"), "header name")?; + Ok((name, value)) + }) + .collect() +} + +fn expect_map( + value: BexExternalValue, + context: &str, +) -> Result, VmRustFnError> { + match value { + BexExternalValue::Map { entries, .. } => Ok(entries), + other => Err(bridge_failure(format!( + "{context} must be a map, got {}", + other.type_name() + ))), + } +} + +fn take_string( + values: &mut IndexMap, + key: &str, + context: &str, +) -> Result { + let value = values + .swap_remove(key) + .ok_or_else(|| bridge_failure(format!("{context} is missing {key}")))?; + expect_string(value, key) +} + +fn expect_string(value: BexExternalValue, context: &str) -> Result { + match value { + BexExternalValue::String(value) => Ok(value.to_string()), + other => Err(bridge_failure(format!( + "{context} must be a string, got {}", + other.type_name() + ))), + } +} + +fn take_int( + values: &mut IndexMap, + key: &str, + context: &str, +) -> Result { + match values + .swap_remove(key) + .ok_or_else(|| bridge_failure(format!("{context} is missing {key}")))? + { + BexExternalValue::Int(value) => Ok(value), + other => Err(bridge_failure(format!( + "{context}.{key} must be an integer, got {}", + other.type_name() + ))), + } +} + +fn take_bytes( + values: &mut IndexMap, + key: &str, + context: &str, +) -> Result, VmRustFnError> { + match values + .swap_remove(key) + .ok_or_else(|| bridge_failure(format!("{context} is missing {key}")))? + { + BexExternalValue::Uint8Array(value) => Ok(value), + other => Err(bridge_failure(format!( + "{context}.{key} must be bytes, got {}", + other.type_name() + ))), + } +} + +fn timeout_duration_ms(timeout_nanos: &num_bigint::BigInt) -> Option { + if timeout_nanos.sign() == num_bigint::Sign::NoSign { + return None; + } + ((timeout_nanos + num_bigint::BigInt::from(999_999_u64)) + / num_bigint::BigInt::from(1_000_000_u64)) + .to_i64() +} + +fn map(entries: IndexMap) -> BexExternalValue { + BexExternalValue::Map { + key_type: baml_type::RuntimeTy::string(), + value_type: baml_type::RuntimeTy::unknown(), + entries, + } +} + +fn array(items: Vec) -> BexExternalValue { + BexExternalValue::Array { + element_type: baml_type::RuntimeTy::unknown(), + items, + } +} + +fn map_output( + output: SysOpOutput, + map: impl FnOnce(T) -> Result + Send + 'static, +) -> SysOpOutput +where + T: Send + 'static, + U: Send + 'static, +{ + match output { + SysOpOutput::Ready(Ok(value)) => SysOpOutput::Ready(map(value)), + SysOpOutput::Ready(Err(error)) => SysOpOutput::Ready(Err(error)), + SysOpOutput::Async(future) => SysOpOutput::Async(Box::pin(async move { + let value = future.await?; + map(value).map_err(OpErrorBody::from) + })), + } +} + +fn unsupported() -> SysOpOutput { + SysOpOutput::err(VmBamlError::Unsupported { + message: UNSUPPORTED.to_string(), + }) +} + +fn bridge_failure(message: impl Into) -> VmRustFnError { + VmInternalError::BridgeFailure { + message: message.into(), + } + .into() +} diff --git a/baml_language/sdk_tests/crates/typescript_web/setup.ps1 b/baml_language/sdk_tests/crates/typescript_web/setup.ps1 index 9d9b1f54102..5bc0f51a049 100644 --- a/baml_language/sdk_tests/crates/typescript_web/setup.ps1 +++ b/baml_language/sdk_tests/crates/typescript_web/setup.ps1 @@ -12,7 +12,7 @@ New-Item -ItemType Directory -Force -Path $env:npm_config_store_dir | Out-Null Write-Host '==> pnpm install in sdks/typescript/bridge_typescript_web' Push-Location $BridgeTypescriptWeb try { - pnpm install --ignore-workspace --ignore-scripts + pnpm install if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } pnpm build:debug if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/baml_language/sdk_tests/crates/typescript_web/setup.sh b/baml_language/sdk_tests/crates/typescript_web/setup.sh index 1b7371a4d42..3157146e097 100755 --- a/baml_language/sdk_tests/crates/typescript_web/setup.sh +++ b/baml_language/sdk_tests/crates/typescript_web/setup.sh @@ -14,7 +14,7 @@ export npm_config_store_dir="$WORKSPACE_ROOT/target/pnpm-store" mkdir -p "$npm_config_store_dir" echo "==> pnpm install in sdks/typescript/bridge_typescript_web" -(cd "$BRIDGE_TYPESCRIPT_WEB" && pnpm install --ignore-workspace --ignore-scripts) +(cd "$BRIDGE_TYPESCRIPT_WEB" && pnpm install) echo "==> pnpm build:debug in sdks/typescript/bridge_typescript_web" (cd "$BRIDGE_TYPESCRIPT_WEB" && pnpm build:debug) diff --git a/baml_language/sdks/typescript/bridge_typescript_web/Cargo.toml b/baml_language/sdks/typescript/bridge_typescript_web/Cargo.toml index 049379dc24f..68d1a8f9d73 100644 --- a/baml_language/sdks/typescript/bridge_typescript_web/Cargo.toml +++ b/baml_language/sdks/typescript/bridge_typescript_web/Cargo.toml @@ -9,7 +9,13 @@ license.workspace = true crate-type = [ "cdylib", "rlib" ] [dependencies] +baml_version = { workspace = true } +bex_project = { workspace = true } bridge_cffi = { workspace = true, default-features = false } +bridge_ctypes = { workspace = true } +sys_wasm = { workspace = true } +js-sys = { workspace = true } +serde-wasm-bindgen = { workspace = true } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } diff --git a/baml_language/sdks/typescript/bridge_typescript_web/package.json b/baml_language/sdks/typescript/bridge_typescript_web/package.json index 4844c0d9099..c3186310a42 100644 --- a/baml_language/sdks/typescript/bridge_typescript_web/package.json +++ b/baml_language/sdks/typescript/bridge_typescript_web/package.json @@ -23,18 +23,26 @@ }, "files": ["dist"], "scripts": { - "build:wasm": "wasm-pack build . --target web --dev --out-dir dist/wasm --out-name bridge_web_core && wasm-pack build . --target bundler --dev --out-dir dist/workerd-wasm --out-name bridge_web_core && node scripts/prepare-wasm-package.mjs", - "build:wasm:release": "node scripts/build-wasm-release.mjs && node scripts/prepare-wasm-package.mjs", + "build:wasm": "wasm-pack build . --target web --dev --out-dir dist/wasm --out-name bridge_web_core && wasm-pack build . --target bundler --dev --out-dir dist/workerd-wasm --out-name bridge_web_core && node scripts/sync-wasm-declarations.mjs && node scripts/prepare-wasm-package.mjs", + "build:wasm:release": "node scripts/build-wasm-release.mjs && node scripts/sync-wasm-declarations.mjs && node scripts/prepare-wasm-package.mjs", + "check:wasm-declarations": "node scripts/sync-wasm-declarations.mjs --check", "build:ts": "node scripts/sync-node-runtime.mjs && tsc -p tsconfig.json && node scripts/prepare-workerd-package.mjs", "build": "pnpm build:wasm:release && pnpm build:ts", - "build:debug": "pnpm build:wasm && pnpm build:ts" + "build:debug": "pnpm build:wasm && pnpm build:ts", + "test:web": "vitest run --config vitest.web.config.ts", + "test:workers": "vitest run --config vitest.workers.config.ts" }, "dependencies": { "protobufjs": "^7.6.2" }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "0.18.4", "@types/node": "^20.0.0", + "@vitest/browser-playwright": "^4.1.0", "esbuild": "^0.25.0", - "typescript": "^5.9.3" + "playwright": "^1.53.1", + "typescript": "^5.9.3", + "vitest": "^4.1.0", + "wrangler": "4.110.0" } } diff --git a/baml_language/sdks/typescript/bridge_typescript_web/pnpm-lock.yaml b/baml_language/sdks/typescript/bridge_typescript_web/pnpm-lock.yaml index 8f6eb797e26..2bd6a91e1f3 100644 --- a/baml_language/sdks/typescript/bridge_typescript_web/pnpm-lock.yaml +++ b/baml_language/sdks/typescript/bridge_typescript_web/pnpm-lock.yaml @@ -12,304 +12,1552 @@ importers: specifier: ^7.6.2 version: 7.6.5 devDependencies: + '@cloudflare/vitest-pool-workers': + specifier: 0.18.4 + version: 0.18.4(@vitest/runner@4.1.10)(@vitest/snapshot@4.1.10)(vitest@4.1.10) '@types/node': specifier: ^20.0.0 version: 20.19.43 + '@vitest/browser-playwright': + specifier: ^4.1.0 + version: 4.1.10(playwright@1.61.1)(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12))(vitest@4.1.10) esbuild: specifier: ^0.25.0 version: 0.25.12 + playwright: + specifier: ^1.53.1 + version: 1.61.1 typescript: specifier: ^5.9.3 version: 5.9.3 + vitest: + specifier: ^4.1.0 + version: 4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12)) + wrangler: + specifier: 4.110.0 + version: 4.110.0 packages: + '@blazediff/core@1.9.1': + resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/vitest-pool-workers@0.18.4': + resolution: {integrity: sha512-jOXvyLoR2b8jFvo3tX+mWxXXwcZ+PbId3d7vGFtZsuKakmDjKSeIq4o/sQjkqNv6q3XacIKSCAvfM8TfkPyrEw==} + peerDependencies: + '@vitest/runner': ^4.1.0 + '@vitest/snapshot': ^4.1.0 + vitest: ^4.1.0 + + '@cloudflare/workerd-darwin-64@1.20260708.1': + resolution: {integrity: sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260708.1': + resolution: {integrity: sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260708.1': + resolution: {integrity: sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260708.1': + resolution: {integrity: sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260708.1': + resolution: {integrity: sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] - '@protobufjs/eventemitter@1.1.1': - resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] - '@protobufjs/fetch@1.1.1': - resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] - '@protobufjs/utf8@1.1.2': - resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] - '@types/node@20.19.43': - resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} - engines: {node: '>=18'} - hasBin: true + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] - protobufjs@7.6.5: - resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} - engines: {node: '>=12.0.0'} + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] -snapshots: + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] - '@esbuild/aix-ppc64@0.25.12': - optional: true + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] - '@esbuild/android-arm64@0.25.12': - optional: true + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] - '@esbuild/android-arm@0.25.12': - optional: true + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] - '@esbuild/android-x64@0.25.12': - optional: true + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] - '@esbuild/darwin-arm64@0.25.12': - optional: true + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] - '@esbuild/darwin-x64@0.25.12': - optional: true + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] - '@esbuild/freebsd-arm64@0.25.12': - optional: true + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] - '@esbuild/freebsd-x64@0.25.12': - optional: true + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] - '@esbuild/linux-arm64@0.25.12': - optional: true + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] - '@esbuild/linux-arm@0.25.12': - optional: true + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] - '@esbuild/linux-ia32@0.25.12': - optional: true + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} - '@esbuild/linux-loong64@0.25.12': - optional: true + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@esbuild/linux-mips64el@0.25.12': - optional: true + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@esbuild/linux-ppc64@0.25.12': - optional: true + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 - '@esbuild/linux-riscv64@0.25.12': - optional: true + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} - '@esbuild/linux-s390x@0.25.12': - optional: true + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@esbuild/linux-x64@0.25.12': - optional: true + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} - '@esbuild/netbsd-arm64@0.25.12': - optional: true + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} - '@esbuild/netbsd-x64@0.25.12': - optional: true + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - '@esbuild/openbsd-arm64@0.25.12': - optional: true + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - '@esbuild/openbsd-x64@0.25.12': - optional: true + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - '@esbuild/openharmony-arm64@0.25.12': - optional: true + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.17': + resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + + '@vitest/browser-playwright@4.1.10': + resolution: {integrity: sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==} + peerDependencies: + playwright: '*' + vitest: 4.1.10 + + '@vitest/browser@4.1.10': + resolution: {integrity: sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==} + peerDependencies: + vitest: 4.1.10 + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + miniflare@4.20260708.1: + resolution: {integrity: sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA==} + engines: {node: '>=22.0.0'} + hasBin: true + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + postcss@8.5.19: + resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} + engines: {node: ^10 || ^12 || >=14} + + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.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 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + workerd@1.20260708.1: + resolution: {integrity: sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.110.0: + resolution: {integrity: sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^5.20260708.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@blazediff/core@1.9.1': {} + + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260708.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260708.1 + + '@cloudflare/vitest-pool-workers@0.18.4(@vitest/runner@4.1.10)(@vitest/snapshot@4.1.10)(vitest@4.1.10)': + dependencies: + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + cjs-module-lexer: 1.2.3 + esbuild: 0.28.1 + miniflare: 4.20260708.1 + vitest: 4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12)) + wrangler: 4.110.0 + zod: 3.25.76 + transitivePeerDependencies: + - '@cloudflare/workers-types' + - bufferutil + - utf-8-validate + + '@cloudflare/workerd-darwin-64@1.20260708.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260708.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260708.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260708.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260708.1': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.28.1': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.28.1': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.28.1': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true + '@esbuild/win32-x64@0.28.1': + optional: true + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.11.2 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.139.0': {} + + '@polka/url@1.0.0-next.29': {} + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -330,10 +1578,170 @@ snapshots: '@protobufjs/utf8@1.1.2': {} + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.17': {} + + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + '@types/node@20.19.43': dependencies: undici-types: 6.21.0 + '@vitest/browser-playwright@4.1.10(playwright@1.61.1)(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12))(vitest@4.1.10)': + dependencies: + '@vitest/browser': 4.1.10(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12))(vitest@4.1.10) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12)) + playwright: 1.61.1 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.1.10(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12))(vitest@4.1.10)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12)) + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12)) + ws: 8.21.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.5(@types/node@20.19.43)(esbuild@0.25.12) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + assertion-error@2.0.1: {} + + blake3-wasm@2.1.5: {} + + chai@6.2.2: {} + + cjs-module-lexer@1.2.3: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + detect-libc@2.1.2: {} + + error-stack-parser-es@1.0.5: {} + + es-module-lexer@2.3.1: {} + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -363,8 +1771,150 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + kleur@4.1.5: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + long@5.3.2: {} + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + miniflare@4.20260708.1: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.28.0 + workerd: 1.20260708.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + mrmime@2.0.1: {} + + nanoid@3.3.16: {} + + obug@2.1.4: {} + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + + pngjs@7.0.0: {} + + postcss@8.5.19: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + protobufjs@7.6.5: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -379,6 +1929,186 @@ snapshots: '@types/node': 20.19.43 long: 5.3.2 + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + semver@7.8.5: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + siginfo@2.0.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + supports-color@10.2.2: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.0: {} + + totalist@3.0.1: {} + + tslib@2.8.1: + optional: true + typescript@5.9.3: {} undici-types@6.21.0: {} + + undici@7.28.0: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.19 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 20.19.43 + esbuild: 0.25.12 + fsevents: 2.3.3 + + vitest@4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.5(@types/node@20.19.43)(esbuild@0.25.12) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.43 + '@vitest/browser-playwright': 4.1.10(playwright@1.61.1)(vite@8.1.5(@types/node@20.19.43)(esbuild@0.25.12))(vitest@4.1.10) + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + workerd@1.20260708.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260708.1 + '@cloudflare/workerd-darwin-arm64': 1.20260708.1 + '@cloudflare/workerd-linux-64': 1.20260708.1 + '@cloudflare/workerd-linux-arm64': 1.20260708.1 + '@cloudflare/workerd-windows-64': 1.20260708.1 + + wrangler@4.110.0: + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260708.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 4.20260708.1 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260708.1 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.21.0: {} + + ws@8.21.1: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.17 + cookie: 1.1.1 + youch-core: 0.3.3 + + zod@3.25.76: {} diff --git a/baml_language/sdks/typescript/bridge_typescript_web/pnpm-workspace.yaml b/baml_language/sdks/typescript/bridge_typescript_web/pnpm-workspace.yaml index fde02c3968b..68b3714328e 100644 --- a/baml_language/sdks/typescript/bridge_typescript_web/pnpm-workspace.yaml +++ b/baml_language/sdks/typescript/bridge_typescript_web/pnpm-workspace.yaml @@ -2,4 +2,7 @@ packages: - "." allowBuilds: + esbuild: true protobufjs: false + sharp: true + workerd: true diff --git a/baml_language/sdks/typescript/bridge_typescript_web/scripts/prepare-workerd-package.mjs b/baml_language/sdks/typescript/bridge_typescript_web/scripts/prepare-workerd-package.mjs index 57682e76a0c..798c4a83e81 100644 --- a/baml_language/sdks/typescript/bridge_typescript_web/scripts/prepare-workerd-package.mjs +++ b/baml_language/sdks/typescript/bridge_typescript_web/scripts/prepare-workerd-package.mjs @@ -26,6 +26,15 @@ if (workerdSafeProto === workerdProto) { } writeFileSync(workerdProtoPath, workerdSafeProto); +const workerdIndexPath = resolve(workerd, "index.js"); +const workerdIndex = readFileSync(workerdIndexPath, "utf8"); +writeFileSync(workerdIndexPath, `import { readFileSync } from "node:fs"; +import { installReadFileSync } from "./native.js"; + +installReadFileSync((path) => readFileSync(path)); + +${workerdIndex}`); + const browserCoreImport = 'from "./wasm/bridge_web_core.js";'; const workerdCoreImport = 'from "../workerd-wasm/bridge_web_core.js";'; const browserNative = readFileSync(resolve(dist, "native.js"), "utf8"); diff --git a/baml_language/sdks/typescript/bridge_typescript_web/scripts/sync-wasm-declarations.mjs b/baml_language/sdks/typescript/bridge_typescript_web/scripts/sync-wasm-declarations.mjs new file mode 100644 index 00000000000..423da77883f --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/scripts/sync-wasm-declarations.mjs @@ -0,0 +1,56 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import ts from "typescript"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const browserPath = resolve(packageRoot, "dist/wasm/bridge_web_core.d.ts"); +const workerdPath = resolve(packageRoot, "dist/workerd-wasm/bridge_web_core.d.ts"); +const sourcePath = resolve(packageRoot, "typescript_src/wasm/bridge_web_core.d.ts"); +const check = process.argv.includes("--check"); +const initializerNames = new Set(["InitInput", "InitOutput", "SyncInitInput", "initSync"]); + +function normalizedNamedExports(path, source) { + const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); + const printer = ts.createPrinter({ removeComments: true }); + const exports = new Map(); + + for (const statement of sourceFile.statements) { + const modifiers = statement.modifiers ?? []; + if (!modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue; + if (modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword)) continue; + + const name = statement.name?.text; + if (!name || initializerNames.has(name)) continue; + if (!ts.isFunctionDeclaration(statement) && !ts.isClassDeclaration(statement) && !ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement) && !ts.isEnumDeclaration(statement)) continue; + + const declaration = printer.printNode(ts.EmitHint.Unspecified, statement, sourceFile).replace(/\s+/g, " ").trim(); + exports.set(name, declaration); + } + + return exports; +} + +function diffExports(browser, workerd) { + const names = [...new Set([...browser.keys(), ...workerd.keys()])].sort(); + const differences = []; + for (const name of names) { + const browserDeclaration = browser.get(name); + const workerdDeclaration = workerd.get(name); + if (browserDeclaration === workerdDeclaration) continue; + differences.push(`${name}:\n browser: ${browserDeclaration ?? ""}\n workerd: ${workerdDeclaration ?? ""}`); + } + return differences; +} + +const browserSource = readFileSync(browserPath, "utf8"); +const workerdSource = readFileSync(workerdPath, "utf8"); +const differences = diffExports(normalizedNamedExports(browserPath, browserSource), normalizedNamedExports(workerdPath, workerdSource)); +if (differences.length > 0) throw new Error(`browser/workerd WASM declarations differ:\n${differences.join("\n")}`); + +if (check) { + const checkedInSource = readFileSync(sourcePath, "utf8"); + if (checkedInSource !== browserSource) throw new Error(`${sourcePath} is stale; run pnpm build:wasm`); +} else { + writeFileSync(sourcePath, browserSource); +} diff --git a/baml_language/sdks/typescript/bridge_typescript_web/src/errors.rs b/baml_language/sdks/typescript/bridge_typescript_web/src/errors.rs new file mode 100644 index 00000000000..1856eb6214f --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/src/errors.rs @@ -0,0 +1,61 @@ +//! JavaScript boundary-error adapters. + +use bridge_cffi::handle::HandleError; +use wasm_bindgen::{JsError, JsValue}; + +pub(crate) const INVALID_ARGUMENT: &str = "invalid_argument"; +pub(crate) const NOT_INITIALIZED: &str = "not_initialized"; +pub(crate) const COMPILATION: &str = "compilation"; +pub(crate) const CLIENT: &str = "client"; + +/// Build an Error-like object whose stable `code` survives the wasm-bindgen +/// exception boundary without relying on formatted Rust or N-API prefixes. +pub(crate) fn setup_error(code: &'static str, message: impl AsRef) -> JsValue { + let error = js_sys::Error::new(message.as_ref()); + error.set_name("BamlBridgeSetupError"); + let _ = js_sys::Reflect::set( + error.as_ref(), + &JsValue::from_str("code"), + &JsValue::from_str(code), + ); + error.into() +} + +pub(crate) fn bridge_error(error: &bridge_cffi::BridgeError) -> JsValue { + use bex_project::RuntimeError; + use bridge_cffi::BridgeError; + + let code = match &error { + BridgeError::NotInitialized | BridgeError::ProjectNotInitialized => NOT_INITIALIZED, + BridgeError::Ctypes(_) + | BridgeError::NullFunctionName + | BridgeError::InvalidFunctionName(_) + | BridgeError::MissingArgument { .. } + | BridgeError::InvalidCallId => INVALID_ARGUMENT, + BridgeError::Runtime(RuntimeError::InvalidArgument { .. }) => INVALID_ARGUMENT, + BridgeError::Runtime(RuntimeError::Compilation { .. }) => COMPILATION, + BridgeError::Runtime( + RuntimeError::Other(_) | RuntimeError::Engine(_) | RuntimeError::Access(_), + ) + | BridgeError::LockPoisoned + | BridgeError::FunctionNotFound { .. } + | BridgeError::NotImplemented(_) + | BridgeError::DuplicateCallId(_) + | BridgeError::Internal(_) => CLIENT, + }; + setup_error(code, error.to_string()) +} + +pub(crate) fn handle_error(operation: &'static str, error: &HandleError) -> JsError { + JsError::new(&format!("{operation}: {error}")) +} + +pub(crate) fn unexpected_handle_type( + operation: &'static str, + expected: i32, + actual: i32, +) -> JsError { + JsError::new(&format!( + "{operation}: shared handle type mismatch (expected {expected}, got {actual})" + )) +} diff --git a/baml_language/sdks/typescript/bridge_typescript_web/src/handle.rs b/baml_language/sdks/typescript/bridge_typescript_web/src/handle.rs new file mode 100644 index 00000000000..f659ff65315 --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/src/handle.rs @@ -0,0 +1,61 @@ +//! Raw ordinary handle-table bindings. + +use bridge_cffi::handle::{self as handle_core, HandleError, HandleParts}; +use bridge_ctypes::baml_bridge::cffi::BamlHandleType; +use wasm_bindgen::prelude::*; + +use crate::errors::{handle_error, unexpected_handle_type}; + +fn key_with_expected_type( + operation: &'static str, + parts: HandleParts, + expected: BamlHandleType, +) -> Result { + let expected = expected as i32; + if parts.handle_type != expected { + return Err(unexpected_handle_type( + operation, + expected, + parts.handle_type, + )); + } + Ok(parts.key) +} + +#[wasm_bindgen(js_name = cloneHandle)] +pub fn clone_handle(key: u64) -> Result { + handle_core::clone_handle(key).map_err(|error| handle_error("cloneHandle", &error)) +} + +#[wasm_bindgen(js_name = releaseHandle)] +pub fn release_handle(key: u64) -> bool { + match handle_core::release_handle(key) { + Ok(()) => true, + Err(HandleError::InvalidHandle) => false, + Err(_) => false, + } +} + +#[wasm_bindgen(js_name = _testHandleTableEntryCount)] +pub fn test_handle_table_entry_count() -> Result { + u32::try_from(handle_core::live_handle_count()) + .map_err(|_| JsError::new("handle table entry count exceeds uint32")) +} + +#[wasm_bindgen(js_name = seedFunctionRefHandle)] +pub fn seed_function_ref_handle(global_index: u32) -> Result { + key_with_expected_type( + "seedFunctionRefHandle", + handle_core::seed_function_ref_handle(u64::from(global_index)), + BamlHandleType::FunctionRef, + ) +} + +#[wasm_bindgen(js_name = seedGenericMediaHandle)] +pub fn seed_generic_media_handle() -> Result { + key_with_expected_type( + "seedGenericMediaHandle", + handle_core::seed_generic_media_handle(), + BamlHandleType::AdtMediaGeneric, + ) +} diff --git a/baml_language/sdks/typescript/bridge_typescript_web/src/host_value.rs b/baml_language/sdks/typescript/bridge_typescript_web/src/host_value.rs new file mode 100644 index 00000000000..34fb8281cd1 --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/src/host_value.rs @@ -0,0 +1,64 @@ +//! Raw Web host-value bindings. + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = configureWebSysops)] +pub fn configure_web_sysops(fetch_key: u64, read_file_sync_key: u64) -> Result<(), JsError> { + sys_wasm::configure_web_sysops(fetch_key, read_file_sync_key) + .map_err(|error| JsError::new(&error)) +} + +#[wasm_bindgen(js_name = registerWebHostCallable)] +pub fn register_web_host_callable(callable: js_sys::Function) -> u64 { + sys_wasm::register_host_callable(callable) +} + +#[wasm_bindgen(js_name = mintWebHostValueKey)] +pub fn mint_web_host_value_key() -> u64 { + sys_wasm::mint_host_value_key() +} + +#[wasm_bindgen(js_name = registerWebHostValueReleaseCallback)] +pub fn register_web_host_value_release_callback(callback: js_sys::Function) -> bool { + sys_wasm::register_host_value_release_callback(callback) +} + +#[wasm_bindgen(js_name = releaseWebHostCallable)] +pub fn release_web_host_callable(key: u64) { + sys_wasm::release_host_callable(key); +} + +#[wasm_bindgen(js_name = completeWebHostCall)] +pub fn complete_web_host_call(call_id: u32, is_error: i32, content: &[u8]) -> bool { + sys_wasm::complete_host_call(call_id, is_error, content) +} + +#[wasm_bindgen(js_name = _testWebHostCallableCount)] +pub fn test_web_host_callable_count() -> u32 { + sys_wasm::test_host_callable_count() +} + +#[wasm_bindgen(js_name = _testWebInFlightHostCallCount)] +pub fn test_web_in_flight_host_call_count() -> u32 { + sys_wasm::test_in_flight_host_call_count() +} + +#[wasm_bindgen(js_name = _testWebHostReleaseCallbackInstalled)] +pub fn test_web_host_release_callback_installed() -> bool { + sys_wasm::test_host_release_callback_installed() +} + +#[wasm_bindgen(js_name = _testWebFireHostRelease)] +pub fn test_web_fire_host_release(key: u64) { + sys_wasm::test_fire_host_release(key); +} + +#[wasm_bindgen(js_name = _testWebMissingHostCallableError)] +pub async fn test_web_missing_host_callable_error(key: u64) -> String { + sys_wasm::test_missing_host_callable_error(key).await +} + +#[wasm_bindgen(js_name = _testWebSyncPendingHostCallableError)] +pub fn test_web_sync_pending_host_callable_error(key: u64) -> String { + sys_wasm::test_sync_pending_host_callable_error(key) +} diff --git a/baml_language/sdks/typescript/bridge_typescript_web/src/lib.rs b/baml_language/sdks/typescript/bridge_typescript_web/src/lib.rs index 7541867e30e..a5c21e2429c 100644 --- a/baml_language/sdks/typescript/bridge_typescript_web/src/lib.rs +++ b/baml_language/sdks/typescript/bridge_typescript_web/src/lib.rs @@ -7,24 +7,15 @@ use wasm_bindgen::prelude::*; #[cfg(target_arch = "wasm32")] -#[wasm_bindgen(js_name = stageRuntimeBytecode)] -pub fn stage_runtime_bytecode(_bytecode: &[u8]) -> Result<(), JsError> { - Err(JsError::new( - "@boundaryml/baml-bridge-web runtime support is not implemented yet", - )) -} - +mod errors; #[cfg(target_arch = "wasm32")] -#[wasm_bindgen(js_name = callFunctionSync)] -pub fn call_function_sync(function_name: &str, encoded_args: &[u8]) -> Vec { - bridge_cffi::call_function_in_wasm_sync(function_name, encoded_args) -} - +pub mod handle; #[cfg(target_arch = "wasm32")] -#[wasm_bindgen(js_name = callFunction)] -pub async fn call_function(function_name: &str, encoded_args: &[u8]) -> Vec { - bridge_cffi::call_function_in_wasm(function_name, encoded_args).await -} +pub mod host_value; +#[cfg(target_arch = "wasm32")] +pub mod media; +#[cfg(target_arch = "wasm32")] +pub mod runtime; #[cfg(target_arch = "wasm32")] #[wasm_bindgen(js_name = newFunctionCall)] @@ -37,3 +28,13 @@ pub fn new_function_call() -> u64 { pub fn cancel_function_call(call_id: u64) -> bool { bridge_cffi::cancel_function_call_by_id(call_id) } + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen(js_name = getVersion)] +pub fn get_version() -> String { + baml_version::CANONICAL_VERSION.to_string() +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen(js_name = flushEvents)] +pub fn flush_events() {} diff --git a/baml_language/sdks/typescript/bridge_typescript_web/src/media.rs b/baml_language/sdks/typescript/bridge_typescript_web/src/media.rs new file mode 100644 index 00000000000..66f29a1cd2f --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/src/media.rs @@ -0,0 +1,105 @@ +//! Raw media handle bindings. + +use bex_project::MediaKind; +use bridge_cffi::handle::{self as handle_core, HandleError, HandleParts}; +use bridge_ctypes::baml_bridge::cffi::{BamlHandleType, MediaTypeEnum}; +use wasm_bindgen::prelude::*; + +use crate::errors::{handle_error, unexpected_handle_type}; + +fn media_kind(media_kind: i32, operation: &'static str) -> Result { + let media_kind = MediaTypeEnum::try_from(media_kind) + .map_err(|_| handle_error(operation, &HandleError::UnsupportedHandleType))?; + match media_kind { + MediaTypeEnum::Image => Ok(MediaKind::Image), + MediaTypeEnum::Audio => Ok(MediaKind::Audio), + MediaTypeEnum::Pdf => Ok(MediaKind::Pdf), + MediaTypeEnum::Video => Ok(MediaKind::Video), + MediaTypeEnum::Other => Ok(MediaKind::Generic), + MediaTypeEnum::MediaTypeUnspecified => { + Err(handle_error(operation, &HandleError::UnsupportedHandleType)) + } + } +} + +fn expected_handle_type(kind: MediaKind) -> BamlHandleType { + match kind { + MediaKind::Image => BamlHandleType::AdtMediaImage, + MediaKind::Audio => BamlHandleType::AdtMediaAudio, + MediaKind::Video => BamlHandleType::AdtMediaVideo, + MediaKind::Pdf => BamlHandleType::AdtMediaPdf, + MediaKind::Generic => BamlHandleType::AdtMediaGeneric, + } +} + +fn media_key(operation: &'static str, kind: MediaKind, parts: HandleParts) -> Result { + let expected = expected_handle_type(kind) as i32; + if parts.handle_type != expected { + return Err(unexpected_handle_type( + operation, + expected, + parts.handle_type, + )); + } + Ok(parts.key) +} + +#[wasm_bindgen(js_name = mediaFromUrl)] +#[allow(clippy::needless_pass_by_value)] +pub fn media_from_url( + media_kind_value: i32, + url: &str, + mime_type: Option, +) -> Result { + let kind = media_kind(media_kind_value, "mediaFromUrl")?; + let parts = handle_core::media_from_url(kind, url, mime_type.as_deref()) + .map_err(|error| handle_error("mediaFromUrl", &error))?; + media_key("mediaFromUrl", kind, parts) +} + +#[wasm_bindgen(js_name = mediaFromFile)] +#[allow(clippy::needless_pass_by_value)] +pub fn media_from_file( + media_kind_value: i32, + file: &str, + mime_type: Option, +) -> Result { + let kind = media_kind(media_kind_value, "mediaFromFile")?; + let parts = handle_core::media_from_file(kind, file, mime_type.as_deref()) + .map_err(|error| handle_error("mediaFromFile", &error))?; + media_key("mediaFromFile", kind, parts) +} + +#[wasm_bindgen(js_name = mediaFromBase64)] +#[allow(clippy::needless_pass_by_value)] +pub fn media_from_base64( + media_kind_value: i32, + base64: &str, + mime_type: Option, +) -> Result { + let kind = media_kind(media_kind_value, "mediaFromBase64")?; + let parts = handle_core::media_from_base64(kind, base64, mime_type.as_deref()) + .map_err(|error| handle_error("mediaFromBase64", &error))?; + media_key("mediaFromBase64", kind, parts) +} + +#[wasm_bindgen(js_name = mediaUrl)] +pub fn media_url(key: u64, handle_type: i32) -> Result, JsError> { + handle_core::media_url(key, handle_type).map_err(|error| handle_error("mediaUrl", &error)) +} + +#[wasm_bindgen(js_name = mediaFile)] +pub fn media_file(key: u64, handle_type: i32) -> Result, JsError> { + handle_core::media_file(key, handle_type).map_err(|error| handle_error("mediaFile", &error)) +} + +#[wasm_bindgen(js_name = mediaBase64)] +pub fn media_base64(key: u64, handle_type: i32) -> Result { + handle_core::media_base64(key, handle_type).map_err(|error| handle_error("mediaBase64", &error)) +} + +#[wasm_bindgen(js_name = mediaMimeType)] +pub fn media_mime_type(key: u64, handle_type: i32) -> Result, JsError> { + handle_core::media_mime_type(key, handle_type) + .map_err(|error| handle_error("mediaMimeType", &error)) +} diff --git a/baml_language/sdks/typescript/bridge_typescript_web/src/runtime.rs b/baml_language/sdks/typescript/bridge_typescript_web/src/runtime.rs new file mode 100644 index 00000000000..188d4fb109b --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/src/runtime.rs @@ -0,0 +1,42 @@ +//! Raw runtime bindings. + +use std::collections::HashMap; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = stageRuntimeBytecode)] +pub fn stage_runtime_bytecode(bytecode: &[u8]) -> Result<(), JsValue> { + let sys_ops = sys_wasm::build() + .map_err(|error| crate::errors::setup_error(crate::errors::CLIENT, error))?; + bridge_cffi::initialize_runtime_from_bytecode_with_sys_ops(bytecode, sys_ops) + .map(|_| ()) + .map_err(|error| crate::errors::bridge_error(&error)) +} + +#[wasm_bindgen(js_name = stageRuntimeSources)] +pub fn stage_runtime_sources(root_path: &str, files: JsValue) -> Result<(), JsValue> { + let files: HashMap = + serde_wasm_bindgen::from_value(files).map_err(|error| { + crate::errors::setup_error( + crate::errors::INVALID_ARGUMENT, + format!("source files must be a record of string contents: {error}"), + ) + })?; + let sys_ops = sys_wasm::build() + .map_err(|error| crate::errors::setup_error(crate::errors::CLIENT, error))?; + bridge_cffi::initialize_runtime_from_files_with_sys_ops(root_path, files, sys_ops) + .map(|_| ()) + .map_err(|error| crate::errors::bridge_error(&error)) +} + +#[wasm_bindgen(js_name = callFunctionSync)] +pub fn call_function_sync(function_name: &str, encoded_args: &[u8]) -> Vec { + sys_wasm::with_web_sync_mode(|| { + bridge_cffi::call_function_in_wasm_sync(function_name, encoded_args) + }) +} + +#[wasm_bindgen(js_name = callFunction)] +pub async fn call_function(function_name: &str, encoded_args: &[u8]) -> Vec { + bridge_cffi::call_function_in_wasm(function_name, encoded_args).await +} diff --git a/baml_language/sdks/typescript/bridge_typescript_web/tests/bridge-web-core.d.ts b/baml_language/sdks/typescript/bridge_typescript_web/tests/bridge-web-core.d.ts new file mode 100644 index 00000000000..71ec842c55a --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/tests/bridge-web-core.d.ts @@ -0,0 +1,4 @@ +declare module "#bridge-web-core" { + export { default } from "../typescript_src/wasm/bridge_web_core.js"; + export * from "../typescript_src/wasm/bridge_web_core.js"; +} diff --git a/baml_language/sdks/typescript/bridge_typescript_web/tests/host_value_registry.test.ts b/baml_language/sdks/typescript/bridge_typescript_web/tests/host_value_registry.test.ts new file mode 100644 index 00000000000..c61c4820544 --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/tests/host_value_registry.test.ts @@ -0,0 +1,64 @@ +import initWasm, * as raw from "#bridge-web-core"; +import { beforeAll, describe, expect, it } from "vitest"; + +beforeAll(async () => { + await initWasm(); +}); + +describe("raw Web host-value registry", () => { + it("uses one unique keyspace for callables and opaque values", () => { + const before = raw._testWebHostCallableCount(); + const callableOne = raw.registerWebHostCallable(() => undefined); + const opaque = raw.mintWebHostValueKey(); + const callableTwo = raw.registerWebHostCallable(() => undefined); + + expect(new Set([callableOne, opaque, callableTwo]).size).toBe(3); + expect(raw._testWebHostCallableCount()).toBe(before + 2); + raw.releaseWebHostCallable(callableOne); + raw.releaseWebHostCallable(callableTwo); + expect(raw._testWebHostCallableCount()).toBe(before); + }); + + it("installs the release callback once and keeps the first callback", () => { + const first: bigint[] = []; + const second: bigint[] = []; + expect(raw._testWebHostReleaseCallbackInstalled()).toBe(false); + expect(raw.registerWebHostValueReleaseCallback((key: bigint) => first.push(key))).toBe(true); + expect(raw._testWebHostReleaseCallbackInstalled()).toBe(true); + expect(raw.registerWebHostValueReleaseCallback((key: bigint) => second.push(key))).toBe(false); + + const key = raw.mintWebHostValueKey(); + raw._testWebFireHostRelease(key); + expect(first).toEqual([key]); + expect(second).toEqual([]); + }); + + it("rolls back callable registrations when a later value cannot encode", async () => { + const { encodeCallArgs } = await import("@boundaryml/baml-bridge-web"); + const before = raw._testWebHostCallableCount(); + expect(() => encodeCallArgs( + { value: [() => "registered first", Symbol("cannot encode")] }, + { callId: 1n }, + )).toThrow(/Cannot encode value/); + expect(raw._testWebHostCallableCount()).toBe(before); + }); + + it("starts with no abandoned in-flight completions", () => { + expect(raw._testWebInFlightHostCallCount()).toBe(0); + expect(raw.completeWebHostCall(0xffff_ffff, 0, new Uint8Array())).toBe(false); + expect(raw._testWebInFlightHostCallCount()).toBe(0); + }); + + it("classifies missing callable dispatch and removes its completion", async () => { + const key = raw.mintWebHostValueKey(); + await expect(raw._testWebMissingHostCallableError(key)).resolves.toMatch(/not registered/); + expect(raw._testWebInFlightHostCallCount()).toBe(0); + }); + + it("fails an allowed sync dispatch that does not complete reentrantly", { timeout: 2_000 }, () => { + const key = raw.registerWebHostCallable(() => undefined); + expect(raw._testWebSyncPendingHostCallableError(key)).toMatch(/did not complete reentrantly|async API/); + expect(raw._testWebInFlightHostCallCount()).toBe(0); + raw.releaseWebHostCallable(key); + }); +}); diff --git a/baml_language/sdks/typescript/bridge_typescript_web/tests/wasm_exports.test.ts b/baml_language/sdks/typescript/bridge_typescript_web/tests/wasm_exports.test.ts new file mode 100644 index 00000000000..645282acf4b --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/tests/wasm_exports.test.ts @@ -0,0 +1,76 @@ +import initWasm, * as raw from "#bridge-web-core"; +import { beforeAll, describe, expect, it } from "vitest"; + +const rawHandleMediaExports = [ + "cloneHandle", + "mediaBase64", + "mediaFile", + "mediaFromBase64", + "mediaFromFile", + "mediaFromUrl", + "mediaMimeType", + "mediaUrl", + "releaseHandle", + "seedFunctionRefHandle", + "seedGenericMediaHandle", + "_testHandleTableEntryCount", +] as const; + +beforeAll(async () => { + await initWasm(); +}); + +describe("raw WASM handle and media exports", () => { + it("exposes the same named handle/media contract", () => { + for (const name of rawHandleMediaExports) expect(raw[name]).toBeTypeOf("function"); + }); + + it("keeps handle keys lossless and release idempotent", () => { + const initialCount = raw._testHandleTableEntryCount(); + const original = raw.seedFunctionRefHandle(0xffff_ffff); + expect(raw._testHandleTableEntryCount()).toBe(initialCount + 1); + const clone = raw.cloneHandle(original); + expect(raw._testHandleTableEntryCount()).toBe(initialCount + 2); + + expect(original).toBeTypeOf("bigint"); + expect(clone).toBeTypeOf("bigint"); + expect(clone).not.toBe(original); + expect(raw.releaseHandle(original)).toBe(true); + expect(raw._testHandleTableEntryCount()).toBe(initialCount + 1); + expect(raw.releaseHandle(original)).toBe(false); + expect(raw._testHandleTableEntryCount()).toBe(initialCount + 1); + expect(raw.releaseHandle(clone)).toBe(true); + expect(raw._testHandleTableEntryCount()).toBe(initialCount); + expect(() => raw.cloneHandle(0n)).toThrow(/cloneHandle: invalid handle/); + }); + + it("constructs and inspects every media source form", () => { + const url = raw.mediaFromUrl(1, "https://example.com/image.png", "image/png"); + expect(url).toBeTypeOf("bigint"); + expect(raw.mediaUrl(url, 6)).toBe("https://example.com/image.png"); + expect(raw.mediaFile(url, 6)).toBeUndefined(); + expect(raw.mediaBase64(url, 6)).toBe(""); + expect(raw.mediaMimeType(url, 6)).toBe("image/png"); + + const file = raw.mediaFromFile(2, "/tmp/audio.wav"); + expect(raw.mediaFile(file, 7)).toBe("/tmp/audio.wav"); + expect(raw.mediaUrl(file, 7)).toBeUndefined(); + expect(raw.mediaMimeType(file, 7)).toBeUndefined(); + + const base64 = raw.mediaFromBase64(4, "dm9pY2U=", "video/mp4"); + expect(raw.mediaBase64(base64, 8)).toBe("dm9pY2U="); + expect(raw.mediaMimeType(base64, 8)).toBe("video/mp4"); + + expect(raw.releaseHandle(url)).toBe(true); + expect(raw.releaseHandle(file)).toBe(true); + expect(raw.releaseHandle(base64)).toBe(true); + }); + + it("seeds generic media and validates explicit handle tags", () => { + const generic = raw.seedGenericMediaHandle(); + expect(generic).toBeTypeOf("bigint"); + expect(raw.mediaUrl(generic, 10)).toBe("https://example.com/"); + expect(() => raw.mediaUrl(generic, 6)).toThrow(/mediaUrl: handle type mismatch/); + expect(raw.releaseHandle(generic)).toBe(true); + }); +}); diff --git a/baml_language/sdks/typescript/bridge_typescript_web/tests/worker.js b/baml_language/sdks/typescript/bridge_typescript_web/tests/worker.js new file mode 100644 index 00000000000..ea37291d10b --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/tests/worker.js @@ -0,0 +1,5 @@ +export default { + fetch() { + return new Response("bridge-web-core-tests"); + }, +}; diff --git a/baml_language/sdks/typescript/bridge_typescript_web/typescript_src/native.ts b/baml_language/sdks/typescript/bridge_typescript_web/typescript_src/native.ts index 131c567310a..1c8dbd45565 100644 --- a/baml_language/sdks/typescript/bridge_typescript_web/typescript_src/native.ts +++ b/baml_language/sdks/typescript/bridge_typescript_web/typescript_src/native.ts @@ -2,15 +2,136 @@ import initWasm, { callFunction as callWasmFunction, callFunctionSync as callWasmFunctionSync, cancelFunctionCall as cancelWasmFunctionCall, + completeWebHostCall, + configureWebSysops, + mintWebHostValueKey, newFunctionCall as newWasmFunctionCall, + registerWebHostCallable, + registerWebHostValueReleaseCallback, + releaseWebHostCallable, stageRuntimeBytecode, } from "./wasm/bridge_web_core.js"; - await initWasm(); +interface WebFetchCall { + method: string; + url: string; + headers: Array<[string, string]>; + body: Uint8Array; + timeoutNanos: bigint; +} + +type WebFetchResult = + | { kind: "ok"; statusCode: number; url: string; headers: Array<[string, string]>; body: Uint8Array } + | { kind: "io"; message: string } + | { kind: "timeout"; message: string }; + +type WebReadFileResult = + | { kind: "ok"; bytes: Uint8Array } + | { kind: "io"; message: string } + | { kind: "unavailable"; message: string }; + +type ReadFileSync = (path: string) => Uint8Array; type HostCallableDispatchFactory = (callable: (...args: unknown[]) => unknown) => (callId: number, payload: Uint8Array) => void; -export function installHostCallableDispatchFactory(_factory: HostCallableDispatchFactory): void {} +let readFileSyncImpl: ReadFileSync | undefined; +let hostCallableDispatchFactory: HostCallableDispatchFactory | undefined; +let webSysopKeys: { fetch: bigint; readFileSync: bigint } | undefined; + +export function installReadFileSync(impl: ReadFileSync): void { + readFileSyncImpl = impl; +} + +export function installHostCallableDispatchFactory(factory: HostCallableDispatchFactory): void { + hostCallableDispatchFactory = factory; +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function parseFetchCall(value: unknown): WebFetchCall { + if (value === null || typeof value !== "object") throw new TypeError("fetch call must be an object"); + const call = value as Partial; + if (typeof call.method !== "string") throw new TypeError("fetch method must be a string"); + if (typeof call.url !== "string") throw new TypeError("fetch URL must be a string"); + if (!(call.body instanceof Uint8Array)) throw new TypeError("fetch body must be bytes"); + if (typeof call.timeoutNanos !== "bigint" || call.timeoutNanos < 0n) throw new TypeError("fetch timeoutNanos must be a non-negative bigint"); + if (!Array.isArray(call.headers) || !call.headers.every((pair) => Array.isArray(pair) && pair.length === 2 && typeof pair[0] === "string" && typeof pair[1] === "string")) { + throw new TypeError("fetch headers must be string pairs"); + } + return call as WebFetchCall; +} + +async function webFetch(value: unknown): Promise { + let call: WebFetchCall; + try { + call = parseFetchCall(value); + } catch (error) { + return { kind: "io", message: errorMessage(error) }; + } + + const controller = new AbortController(); + let timedOut = false; + let timer: ReturnType | undefined; + if (call.timeoutNanos > 0n) { + const timeoutMillis = (call.timeoutNanos + 999_999n) / 1_000_000n; + const maximumTimerMillis = 2_147_483_647n; + timer = setTimeout(() => { + timedOut = true; + controller.abort(); + }, Number(timeoutMillis > maximumTimerMillis ? maximumTimerMillis : timeoutMillis)); + } + + try { + const method = call.method.toUpperCase(); + const requestBody = method === "GET" || method === "HEAD" || call.body.length === 0 ? undefined : new Uint8Array(call.body); + const response = await globalThis.fetch(call.url, { + method, + headers: new Headers(call.headers), + body: requestBody, + signal: controller.signal, + }); + const body = new Uint8Array(await response.arrayBuffer()); + return { + kind: "ok", + statusCode: response.status, + url: response.url || call.url, + headers: Array.from(response.headers.entries()), + body, + }; + } catch (error) { + return timedOut + ? { kind: "timeout", message: `HTTP request timed out: ${errorMessage(error)}` } + : { kind: "io", message: `HTTP request failed: ${errorMessage(error)}` }; + } finally { + if (timer !== undefined) clearTimeout(timer); + } +} + +function webReadFileSync(path: unknown): WebReadFileResult { + if (typeof path !== "string") return { kind: "io", message: "readFileSync path must be a string" }; + if (!readFileSyncImpl) return { kind: "unavailable", message: "fs.readFileSync is not available in this JavaScript runtime" }; + try { + const value = readFileSyncImpl(path); + const bytes = new Uint8Array(value.byteLength); + bytes.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)); + return { kind: "ok", bytes }; + } catch (error) { + return { kind: "io", message: `readFileSync failed for ${path}: ${errorMessage(error)}` }; + } +} + +function ensureWebSysopsConfigured(): void { + if (!hostCallableDispatchFactory) throw new Error("Web HOST_VALUE_CALLABLE dispatch is not installed"); + if (!webSysopKeys) { + webSysopKeys = { + fetch: registerWebHostCallable(hostCallableDispatchFactory(webFetch)), + readFileSync: registerWebHostCallable(hostCallableDispatchFactory(webReadFileSync)), + }; + } + configureWebSysops(webSysopKeys.fetch, webSysopKeys.readFileSync); +} export interface HandleKey { low: number; high: number; } @@ -78,6 +199,7 @@ export class Collector { export class BamlRuntime { static initializeRuntimeFromBytecode(bytecode: Uint8Array): BamlRuntime { + ensureWebSysopsConfigured(); stageRuntimeBytecode(bytecode); runtime = new BamlRuntime(); return runtime; @@ -95,7 +217,6 @@ export class BamlRuntime { let runtime: BamlRuntime | undefined; let hostRelease: ((key: HandleKey) => void) | undefined; -let nextHostValueKey = 1n; export function getRuntime(): BamlRuntime { if (!runtime) throw new Error("BAML runtime has not been initialized"); @@ -105,10 +226,17 @@ export function newFunctionCall(): bigint { return newWasmFunctionCall(); } export function cancelFunctionCall(callId: bigint | string): boolean { return cancelWasmFunctionCall(BigInt(callId)); } export function flushEvents(): void {} export function getVersion(): string { return "0.0.0-web"; } -export function mintHostValueKey(): HandleKey { return keyFromBigint(nextHostValueKey++); } -export function registerHostValueReleaseCallback(callback: (key: HandleKey) => void): void { hostRelease = callback; } -export function registerHostCallable(_callback: (callId: number, args: Uint8Array) => void): HandleKey { return mintHostValueKey(); } -export function releaseHostCallable(key: HandleKey): void { hostRelease?.(key); } -export function completeHostCall(_callId: number, _isError: number, _content: Uint8Array): void {} +export function mintHostValueKey(): HandleKey { return keyFromBigint(mintWebHostValueKey()); } +export function registerHostValueReleaseCallback(callback: (key: HandleKey) => void): void { + hostRelease = callback; + registerWebHostValueReleaseCallback((key: bigint) => hostRelease?.(keyFromBigint(key))); +} +export function registerHostCallable(callback: (callId: number, args: Uint8Array) => void): HandleKey { return keyFromBigint(registerWebHostCallable(callback)); } +export function releaseHostCallable(key: HandleKey): void { + const value = BigInt.asUintN(64, BigInt(key.high) << 32n | BigInt.asUintN(32, BigInt(key.low))); + releaseWebHostCallable(value); + hostRelease?.(key); +} +export function completeHostCall(callId: number, isError: number, content: Uint8Array): void { completeWebHostCall(callId, isError, content); } export function _seedFunctionRefHandle(_globalIndex: number): [HandleKey, number] { throw new Error("test handle seeding is not implemented yet"); } export function _seedGenericMediaHandle(): [HandleKey, number] { throw new Error("test handle seeding is not implemented yet"); } diff --git a/baml_language/sdks/typescript/bridge_typescript_web/typescript_src/wasm/bridge_web_core.d.ts b/baml_language/sdks/typescript/bridge_typescript_web/typescript_src/wasm/bridge_web_core.d.ts index 56e99582148..548a06c8bee 100644 --- a/baml_language/sdks/typescript/bridge_typescript_web/typescript_src/wasm/bridge_web_core.d.ts +++ b/baml_language/sdks/typescript/bridge_typescript_web/typescript_src/wasm/bridge_web_core.d.ts @@ -1,6 +1,199 @@ -export default function init(moduleOrPath?: unknown): Promise; -export function stageRuntimeBytecode(bytecode: Uint8Array): void; -export function callFunctionSync(functionName: string, encodedArgs: Uint8Array): Uint8Array; -export function callFunction(functionName: string, encodedArgs: Uint8Array): Promise; +/* tslint:disable */ +/* eslint-disable */ + +export function _testHandleTableEntryCount(): number; + +export function _testWebFireHostRelease(key: bigint): void; + +export function _testWebHostCallableCount(): number; + +export function _testWebHostReleaseCallbackInstalled(): boolean; + +export function _testWebInFlightHostCallCount(): number; + +export function _testWebMissingHostCallableError(key: bigint): Promise; + +export function _testWebSyncPendingHostCallableError(key: bigint): string; + +export function callFunction(function_name: string, encoded_args: Uint8Array): Promise; + +export function callFunctionSync(function_name: string, encoded_args: Uint8Array): Uint8Array; + +export function cancelFunctionCall(call_id: bigint): boolean; + +export function cloneHandle(key: bigint): bigint; + +/** + * Complete an in-flight host call from JS. + * + * Exposed to JS as `completeHostCall(callId, isError, content)`. + * + * On success (`is_error == 0`), `content` is a protobuf-encoded `InboundValue` + * (host→engine direction, no type metadata — engine re-validates against the + * declared return type). + * + * On error (`is_error != 0`), `content` is a protobuf-encoded `InboundValue` + * representing the thrown value. The host bridge SDK wraps native exceptions + * in a synthetic `Instance` of class `baml.errors.HostCallable` carrying + * `message` / `class_name` / `language` / `traceback` fields; codegenned + * BAML errors flow through as their own `Instance` shape. The engine's + * `materialize_host_throw` runs the declared-throws contract check on the + * decoded value and either re-injects it as a catchable throw or escalates + * to a `HostContractViolation` panic. + * + * `call_id` is globally unique, so it resolves the originating runtime's + * pending call unambiguously. Returns `true` when a live entry was removed and + * completed. An unknown ID returns `false` after a bounded warning, making a + * Promise settlement racing with cancellation an observable benign stale + * completion. + */ +export function completeHostCall(call_id: number, is_error: number, content: Uint8Array): boolean; + +export function completeWebHostCall(call_id: number, is_error: number, content: Uint8Array): boolean; + +export function configureWebSysops(fetch_key: bigint, read_file_sync_key: bigint): void; + +export function flushEvents(): void; + +export function getVersion(): string; + +export function mediaBase64(key: bigint, handle_type: number): string; + +export function mediaFile(key: bigint, handle_type: number): string | undefined; + +export function mediaFromBase64(media_kind_value: number, base64: string, mime_type?: string | null): bigint; + +export function mediaFromFile(media_kind_value: number, file: string, mime_type?: string | null): bigint; + +export function mediaFromUrl(media_kind_value: number, url: string, mime_type?: string | null): bigint; + +export function mediaMimeType(key: bigint, handle_type: number): string | undefined; + +export function mediaUrl(key: bigint, handle_type: number): string | undefined; + +/** + * Mint a key for a JS-owned opaque value from the same keyspace used by + * callable host values. Both kinds share one engine handle table. + */ +export function mintHostValueKey(): bigint; + +export function mintWebHostValueKey(): bigint; + export function newFunctionCall(): bigint; -export function cancelFunctionCall(callId: bigint): boolean; + +/** + * Register a JS callable in the WASM host-value table and return its key. + * + * Exposed to JS as `registerHostCallable(fn) -> bigint`. The key is then + * embedded in `InboundValue::Handle { key, handleType: HOST_VALUE_CALLABLE }` + * by the JS encoder. The returned value is a `BigInt` because `u64` does not + * fit into JS's safe-integer range. + */ +export function registerHostCallable(callable: Function): bigint; + +/** + * Register the JS callback that releases opaque values from the SDK's local + * registry when the engine drops its last corresponding `HostValueArc`. + */ +export function registerHostValueReleaseCallback(callback: Function): boolean; + +export function registerWebHostCallable(callable: Function): bigint; + +export function registerWebHostValueReleaseCallback(callback: Function): boolean; + +export function releaseHandle(key: bigint): boolean; + +/** + * Remove a callable that was registered but never transferred to the engine. + */ +export function releaseHostCallable(key: bigint): void; + +export function releaseWebHostCallable(key: bigint): void; + +export function seedFunctionRefHandle(global_index: number): bigint; + +export function seedGenericMediaHandle(): bigint; + +export function stageRuntimeBytecode(bytecode: Uint8Array): void; + +export function stageRuntimeSources(root_path: string, files: any): void; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly mediaBase64: (a: bigint, b: number) => [number, number, number, number]; + readonly mediaFile: (a: bigint, b: number) => [number, number, number, number]; + readonly mediaFromBase64: (a: number, b: number, c: number, d: number, e: number) => [bigint, number, number]; + readonly mediaFromFile: (a: number, b: number, c: number, d: number, e: number) => [bigint, number, number]; + readonly mediaFromUrl: (a: number, b: number, c: number, d: number, e: number) => [bigint, number, number]; + readonly mediaMimeType: (a: bigint, b: number) => [number, number, number, number]; + readonly mediaUrl: (a: bigint, b: number) => [number, number, number, number]; + readonly _testHandleTableEntryCount: () => [number, number, number]; + readonly cloneHandle: (a: bigint) => [bigint, number, number]; + readonly releaseHandle: (a: bigint) => number; + readonly seedFunctionRefHandle: (a: number) => [bigint, number, number]; + readonly seedGenericMediaHandle: () => [bigint, number, number]; + readonly cancelFunctionCall: (a: bigint) => number; + readonly flushEvents: () => void; + readonly getVersion: () => [number, number]; + readonly newFunctionCall: () => bigint; + readonly callFunction: (a: number, b: number, c: number, d: number) => any; + readonly callFunctionSync: (a: number, b: number, c: number, d: number) => [number, number]; + readonly stageRuntimeBytecode: (a: number, b: number) => [number, number]; + readonly stageRuntimeSources: (a: number, b: number, c: any) => [number, number]; + readonly _testWebFireHostRelease: (a: bigint) => void; + readonly _testWebHostCallableCount: () => number; + readonly _testWebHostReleaseCallbackInstalled: () => number; + readonly _testWebInFlightHostCallCount: () => number; + readonly _testWebMissingHostCallableError: (a: bigint) => any; + readonly _testWebSyncPendingHostCallableError: (a: bigint) => [number, number]; + readonly completeWebHostCall: (a: number, b: number, c: number, d: number) => number; + readonly configureWebSysops: (a: bigint, b: bigint) => [number, number]; + readonly mintWebHostValueKey: () => bigint; + readonly registerWebHostCallable: (a: any) => bigint; + readonly registerWebHostValueReleaseCallback: (a: any) => number; + readonly releaseWebHostCallable: (a: bigint) => void; + readonly completeHostCall: (a: number, b: number, c: number, d: number) => number; + readonly mintHostValueKey: () => bigint; + readonly registerHostCallable: (a: any) => bigint; + readonly registerHostValueReleaseCallback: (a: any) => number; + readonly releaseHostCallable: (a: bigint) => void; + readonly free_buffer: (a: number) => void; + readonly cancel_function_call: (a: bigint) => number; + readonly new_function_call: () => bigint; + readonly wasm_bindgen__convert__closures_____invoke__h06cf218e7899498c: (a: number, b: number, c: any) => [number, number]; + readonly wasm_bindgen__convert__closures_____invoke__hc1aeb8686748a7da: (a: number, b: number, c: any, d: any) => void; + readonly wasm_bindgen__convert__closures_____invoke__h5b34bf2d90ad8f2b: (a: number, b: number) => number; + readonly __wbindgen_malloc_command_export: (a: number, b: number) => number; + readonly __wbindgen_realloc_command_export: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_exn_store_command_export: (a: number) => void; + readonly __externref_table_alloc_command_export: () => number; + readonly __wbindgen_externrefs: WebAssembly.Table; + readonly __wbindgen_destroy_closure_command_export: (a: number, b: number) => void; + readonly __wbindgen_free_command_export: (a: number, b: number, c: number) => void; + readonly __externref_table_dealloc_command_export: (a: number) => void; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; + +/** + * Instantiates the given `module`, which can either be bytes or + * a precompiled `WebAssembly.Module`. + * + * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. + * + * @returns {InitOutput} + */ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** + * If `module_or_path` is {RequestInfo} or {URL}, makes a request and + * for everything else, calls `WebAssembly.instantiate` directly. + * + * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. + * + * @returns {Promise} + */ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/baml_language/sdks/typescript/bridge_typescript_web/vitest.web.config.ts b/baml_language/sdks/typescript/bridge_typescript_web/vitest.web.config.ts new file mode 100644 index 00000000000..e468000f006 --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/vitest.web.config.ts @@ -0,0 +1,21 @@ +import { playwright } from "@vitest/browser-playwright"; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { + "#bridge-web-core": fileURLToPath(new URL("./dist/wasm/bridge_web_core.js", import.meta.url)), + "#bridge-web-native": fileURLToPath(new URL("./dist/native.js", import.meta.url)), + }, + }, + test: { + include: ["tests/*.test.ts"], + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: "chromium" }], + }, + }, +}); diff --git a/baml_language/sdks/typescript/bridge_typescript_web/vitest.workers.config.ts b/baml_language/sdks/typescript/bridge_typescript_web/vitest.workers.config.ts new file mode 100644 index 00000000000..ab5f484690f --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/vitest.workers.config.ts @@ -0,0 +1,20 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./wrangler.test.jsonc" }, + }), + ], + resolve: { + alias: { + "#bridge-web-core": fileURLToPath(new URL("./dist/workerd-wasm/bridge_web_core.js", import.meta.url)), + "#bridge-web-native": fileURLToPath(new URL("./dist/workerd/native.js", import.meta.url)), + }, + }, + test: { + include: ["tests/*.test.ts"], + }, +}); diff --git a/baml_language/sdks/typescript/bridge_typescript_web/wrangler.test.jsonc b/baml_language/sdks/typescript/bridge_typescript_web/wrangler.test.jsonc new file mode 100644 index 00000000000..fce043f4aed --- /dev/null +++ b/baml_language/sdks/typescript/bridge_typescript_web/wrangler.test.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "bridge-web-core-tests", + "main": "tests/worker.js", + "compatibility_date": "2026-07-15", + "compatibility_flags": ["nodejs_compat"] +}