From 9a7ec01bb90261ecfd833f43f0b986fda4b14949 Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 10:48:13 +0200 Subject: [PATCH 01/15] fix: merge correctness, strict config and CLI output format - respect output.format from config in the CLI (-f still overrides) - emit components in a stable order - never overwrite an existing component when renaming (b_2_Pet) - merge paths/webhooks per operation; identical definitions are shared - sanitise source names used in paths/component names; URL sources default to host - merge webhooks and top-level security; keep components.pathItems and extensions - warn when sources mix OpenAPI minor versions (aggregate_with_report) - reject unknown config keys and ambiguous sources with clear errors - expand ${VAR} in HTTP source url and headers - load sources concurrently with a shared HTTP client --- Cargo.lock | 1 + Cargo.toml | 3 +- README.md | 41 ++- config.example.yaml | 3 +- src/config.rs | 144 +++++++- src/lib.rs | 42 ++- src/main.rs | 14 +- src/merge.rs | 722 ++++++++++++++++++++++++++++++++------ src/source.rs | 145 +++++--- tests/integration_test.rs | 113 +++++- 10 files changed, 1042 insertions(+), 186 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05dd1d7..e1aad27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -708,6 +708,7 @@ name = "openapi-aggregator" version = "0.6.0" dependencies = [ "clap", + "futures-util", "mockito", "reqwest", "serde", diff --git a/Cargo.toml b/Cargo.toml index 7116682..8cff557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,12 +16,13 @@ path = "src/lib.rs" [dependencies] clap = { version = "4", features = ["derive"] } +futures-util = { version = "0.3", default-features = false, features = ["alloc"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } serde_yaml = "0.9" thiserror = "1" -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs"] } [dev-dependencies] mockito = "1" diff --git a/README.md b/README.md index 16f100d..8254bc6 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,23 @@ Sources are detected automatically by their fields: - If `url` is present → HTTP source - If `path` is present → file source (YAML or JSON auto-detected from content) +Exactly one of `url` / `path` must be set, and unknown keys anywhere in the config are rejected, so typos like `conflict_stratgy` fail loudly instead of being ignored. + +An HTTP source without a `name` is named after its URL host (e.g. `billing.example.com`). + +### Environment variables + +`${VAR}` placeholders in an HTTP source's `url` and `headers` values are replaced with environment variables at load time, so secrets don't have to live in the config file: + +```yaml + - name: billing + url: https://billing.example.com/openapi.json + headers: + Authorization: "Bearer ${BILLING_TOKEN}" +``` + +A referenced variable that is not set is an error. + ### Additional source blocks Each source can define `additional_blocks` as any YAML/JSON object. It is deep-merged into that source document before merge, so you can inject vendor extensions (for example API gateway related `x-...` blocks) or other custom OpenAPI fragments without adding provider-specific fields. @@ -159,15 +176,27 @@ async fn main() -> Result<(), Box> { ## Merge Behaviour -| Strategy | Duplicate paths | Duplicate components | -|-------------|----------------------------------------|------------------------------------------| -| `error` | Fail immediately | Fail immediately | -| `overwrite` | Last source wins | Last source wins | -| `rename` | Prefix path with `/{source_name}` | Rename to `{source_name}_{component}` | +Only real conflicts trigger the conflict strategy: + +- **Paths are merged per operation.** `GET /users` from one source and `POST /users` from another end up under the same path. A conflict is the same operation (or path-level field) defined differently. +- **Identical definitions are shared.** Two sources defining the same `bearerAuth` scheme or `Error` schema produce one copy, not a conflict. + +| Strategy | Conflicting paths / webhooks | Conflicting components | +|-------------|------------------------------------------|------------------------------------------| +| `error` | Fail immediately | Fail immediately | +| `overwrite` | Last source wins (per operation) | Last source wins | +| `rename` | Move the source's path to `/{source_name}{path}` | Rename to `{source_name}_{component}` | When `prefix_paths: true`, **all** paths are prefixed regardless of conflicts. -When using `rename`, any `$ref` pointing to a renamed component is rewritten automatically. +When using `rename`, any `$ref` pointing to a renamed component is rewritten automatically, and renamed names never overwrite an existing one (`b_2_Pet` is used if `b_Pet` is taken). Source names are sanitised (`my api` → `my_api`) wherever they become part of a path or component name. + +Other top-level fields: + +- `webhooks` (OpenAPI 3.1) are merged like paths. +- Top-level `security` requirements are combined and de-duplicated. +- The merged `openapi` version comes from the first source. The CLI prints a warning when sources use different `major.minor` versions (also available via `aggregate_with_report`). +- Component types are always emitted in the same order, so the output is stable across runs. ## Development diff --git a/config.example.yaml b/config.example.yaml index ddcce1f..c0b0f6f 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -24,7 +24,7 @@ sources: - name: billing url: https://billing.example.com/openapi.json headers: - Authorization: "Bearer your-token-here" + Authorization: "Bearer ${BILLING_TOKEN}" # ${VAR} is read from the environment Accept: "application/json" # additional_blocks: # x-another-extension: @@ -40,6 +40,7 @@ merge: # error – fail immediately (default) # overwrite – last source wins # rename – prefix conflicting names with the source name + # Different methods on the same path and identical definitions are never conflicts. conflict_strategy: error # Prefix every path with /{source_name} to guarantee uniqueness diff --git a/src/config.rs b/src/config.rs index 56f0616..f7cbf25 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; /// Top-level configuration for the aggregator. #[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct Config { pub sources: Vec, #[serde(default)] @@ -16,9 +17,12 @@ pub struct Config { /// A source of an OpenAPI specification. /// /// Detected automatically: if `url` is present it is treated as an HTTP source, -/// otherwise `path` is used as a local file source. +/// if `path` is present it is a local file source. Exactly one must be set. +/// +/// In HTTP sources, `${ENV_VAR}` placeholders in `url` and header values are +/// replaced with environment variables when the source is loaded. #[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(untagged)] +#[serde(untagged, try_from = "RawSource")] pub enum Source { Http { name: Option, @@ -44,11 +48,78 @@ pub enum Source { }, } +/// Flat representation of a source, used to give precise errors +/// before deciding which [`Source`] variant it is. +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct RawSource { + name: Option, + url: Option, + path: Option, + headers: Option>, + tag_prefix: Option, + additional_blocks: Option, +} + +impl TryFrom for Source { + type Error = String; + + fn try_from(raw: RawSource) -> Result { + let label = raw + .name + .as_deref() + .map(|n| format!("source '{n}'")) + .unwrap_or_else(|| "source".into()); + match (raw.url, raw.path) { + (Some(url), None) => Ok(Source::Http { + name: raw.name, + url, + headers: raw.headers.unwrap_or_default(), + tag_prefix: raw.tag_prefix, + additional_blocks: raw.additional_blocks, + }), + (None, Some(path)) => { + if raw.headers.is_some() { + return Err(format!( + "{label}: 'headers' is only supported for 'url' sources" + )); + } + Ok(Source::File { + name: raw.name, + path, + tag_prefix: raw.tag_prefix, + additional_blocks: raw.additional_blocks, + }) + } + (Some(_), Some(_)) => Err(format!( + "{label} has both 'path' and 'url'; set exactly one" + )), + (None, None) => Err(format!("{label} needs either 'path' or 'url'")), + } + } +} + +/// Extract the host from a URL, e.g. `https://user@api.test:8443/x` → `api.test`. +fn url_host(url: &str) -> Option<&str> { + let rest = url.split_once("://").map_or(url, |(_, rest)| rest); + let authority = rest.split(['/', '?', '#']).next()?; + let host_port = authority.rsplit_once('@').map_or(authority, |(_, h)| h); + let host = match host_port.strip_prefix('[') { + Some(ipv6) => ipv6.split(']').next()?, + None => host_port.split(':').next()?, + }; + (!host.is_empty()).then_some(host) +} + impl Source { - /// Return the user-provided name or derive one from the url / filename. + /// Return the user-provided name or derive one from the url host / filename. pub fn display_name(&self) -> String { match self { - Source::Http { name, url, .. } => name.clone().unwrap_or_else(|| url.clone()), + Source::Http { name, url, .. } => name.clone().unwrap_or_else(|| { + url_host(url) + .map(str::to_string) + .unwrap_or_else(|| url.clone()) + }), Source::File { name, path, .. } => name.clone().unwrap_or_else(|| { path.file_stem() .map(|s| s.to_string_lossy().into_owned()) @@ -69,6 +140,7 @@ impl Source { /// Output configuration. #[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct OutputConfig { #[serde(default)] pub format: OutputFormat, @@ -93,6 +165,7 @@ pub enum OutputFormat { /// Options that control how specs are merged. #[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct MergeConfig { /// Strategy for handling duplicate paths or component names. #[serde(default)] @@ -163,6 +236,7 @@ pub enum ConflictStrategy { /// Values used to override the top-level `info` object. #[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct InfoOverride { pub title: Option, pub version: Option, @@ -171,6 +245,7 @@ pub struct InfoOverride { /// A server entry for the merged spec. #[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct ServerEntry { pub url: String, pub description: Option, @@ -178,7 +253,68 @@ pub struct ServerEntry { /// A tag entry for the merged spec. #[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct TagEntry { pub name: String, pub description: Option, } + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(yaml: &str) -> Result { + serde_yaml::from_str(yaml).map_err(|e| e.to_string()) + } + + #[test] + fn unknown_merge_field_is_rejected() { + let err = parse("sources: []\nmerge:\n conflict_stratgy: rename\n").unwrap_err(); + assert!(err.contains("conflict_stratgy"), "{err}"); + } + + #[test] + fn unknown_source_field_is_rejected() { + let err = parse("sources:\n - path: a.yaml\n header: {}\n").unwrap_err(); + assert!(err.contains("header"), "{err}"); + } + + #[test] + fn source_needs_path_or_url() { + let err = parse("sources:\n - name: x\n").unwrap_err(); + assert!(err.contains("either 'path' or 'url'"), "{err}"); + } + + #[test] + fn source_cannot_have_both_path_and_url() { + let err = parse("sources:\n - path: a.yaml\n url: https://x.test/a\n").unwrap_err(); + assert!(err.contains("both 'path' and 'url'"), "{err}"); + } + + #[test] + fn file_source_cannot_have_headers() { + let err = parse("sources:\n - path: a.yaml\n headers: {A: b}\n").unwrap_err(); + assert!(err.contains("headers"), "{err}"); + } + + #[test] + fn sources_parse_into_variants() { + let config = + parse("sources:\n - path: a.yaml\n - url: https://x.test/a\n headers: {A: b}\n") + .unwrap(); + assert!(matches!(config.sources[0], Source::File { .. })); + assert!(matches!(&config.sources[1], Source::Http { headers, .. } if headers["A"] == "b")); + } + + #[test] + fn http_source_default_name_is_host() { + let source = Source::Http { + name: None, + url: "https://billing.example.com:8443/v1/openapi.json".into(), + headers: HashMap::new(), + tag_prefix: None, + additional_blocks: None, + }; + assert_eq!(source.display_name(), "billing.example.com"); + } +} diff --git a/src/lib.rs b/src/lib.rs index e627eeb..2c77d9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,36 +8,46 @@ pub use config::{ TagEntry, TagPrefixStrategy, }; pub use error::Error; -pub use merge::merge_specs; +pub use merge::{merge_specs, merge_specs_with_report, MergeReport}; pub use source::load_source; use std::path::Path; +use futures_util::future::try_join_all; +use source::{http_client, load_source_with_client}; + /// Load all sources defined in `config` and merge them into a single OpenAPI spec. pub async fn aggregate(config: &Config) -> Result { + aggregate_with_report(config) + .await + .map(|report| report.spec) +} + +/// Like [`aggregate`], but also returns non-fatal merge warnings. +/// +/// Sources are loaded concurrently; the merge order still follows `config.sources`. +pub async fn aggregate_with_report(config: &Config) -> Result { if config.sources.is_empty() { return Err(Error::NoSources); } - let mut specs = Vec::with_capacity(config.sources.len()); - for src in &config.sources { - let (name, spec) = load_source(src).await?; - let tag_prefix = src.tag_prefix(); - specs.push((name, tag_prefix, spec)); - } + let client = http_client()?; + let specs = try_join_all(config.sources.iter().map(|src| async { + let (name, spec) = load_source_with_client(src, &client).await?; + Ok::<_, Error>((name, src.tag_prefix(), spec)) + })) + .await?; - merge_specs(specs, &config.merge) + merge_specs_with_report(specs, &config.merge) } -/// Read a config file, resolve relative source paths against its directory, -/// then aggregate. -pub async fn aggregate_from_file(path: &Path) -> Result { +/// Read a config file and resolve relative source paths against its directory. +pub fn load_config(path: &Path) -> Result { let content = std::fs::read_to_string(path) .map_err(|e| Error::Config(format!("failed to read config file: {e}")))?; let mut config: Config = serde_yaml::from_str(&content) .map_err(|e| Error::Config(format!("failed to parse config file: {e}")))?; - // Resolve relative file paths against the config file's directory if let Some(config_dir) = path.parent() { for src in &mut config.sources { if let Source::File { @@ -52,5 +62,11 @@ pub async fn aggregate_from_file(path: &Path) -> Result Result { + aggregate(&load_config(path)?).await } diff --git a/src/main.rs b/src/main.rs index bff5b82..10313c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use clap::Parser; -use openapi_aggregator::{aggregate_from_file, OutputFormat}; +use openapi_aggregator::{aggregate_with_report, load_config, OutputFormat}; #[derive(Parser)] #[command(name = "openapi-aggregator")] @@ -42,12 +42,16 @@ async fn main() { } async fn run(cli: Cli) -> Result<(), Box> { - let merged = aggregate_from_file(&cli.config).await?; + let config = load_config(&cli.config)?; + let report = aggregate_with_report(&config).await?; + for warning in &report.warnings { + eprintln!("warning: {warning}"); + } - let format = cli.format.unwrap_or(OutputFormat::Yaml); + let format = cli.format.unwrap_or(config.output.format); let text = match format { - OutputFormat::Yaml => serde_yaml::to_string(&merged)?, - OutputFormat::Json => serde_json::to_string_pretty(&merged)?, + OutputFormat::Yaml => serde_yaml::to_string(&report.spec)?, + OutputFormat::Json => serde_json::to_string_pretty(&report.spec)? + "\n", }; match cli.output { diff --git a/src/merge.rs b/src/merge.rs index 74736e4..85f935e 100644 --- a/src/merge.rs +++ b/src/merge.rs @@ -1,10 +1,11 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use serde_json::{Map, Value}; use crate::config::{ConflictStrategy, InfoOverride, MergeConfig, TagPrefixStrategy}; use crate::error::Error; +/// Component types in the order they are emitted in the merged spec. const COMPONENT_TYPES: &[&str] = &[ "schemas", "responses", @@ -15,8 +16,20 @@ const COMPONENT_TYPES: &[&str] = &[ "securitySchemes", "links", "callbacks", + "pathItems", ]; +const HTTP_METHODS: &[&str] = &[ + "get", "post", "put", "patch", "delete", "options", "head", "trace", +]; + +/// The merged spec plus any non-fatal warnings raised while merging. +#[derive(Debug, Clone)] +pub struct MergeReport { + pub spec: Value, + pub warnings: Vec, +} + /// Merge multiple named OpenAPI specs into one according to `config`. /// /// Each entry is `(source_name, tag_prefix, spec_value)`. @@ -24,6 +37,15 @@ pub fn merge_specs( specs: Vec<(String, String, Value)>, config: &MergeConfig, ) -> Result { + merge_specs_with_report(specs, config).map(|report| report.spec) +} + +/// Like [`merge_specs`], but also returns non-fatal warnings +/// (e.g. sources using different OpenAPI minor versions). +pub fn merge_specs_with_report( + specs: Vec<(String, String, Value)>, + config: &MergeConfig, +) -> Result { if specs.is_empty() { return Err(Error::NoSources); } @@ -34,6 +56,7 @@ pub fn merge_specs( if let Some(version) = specs[0].2.get("openapi") { merged.insert("openapi".into(), version.clone()); } + let warnings = version_warnings(&specs); // --- info --- let info = build_info(&specs, config.info.as_ref())?; @@ -41,14 +64,19 @@ pub fn merge_specs( // --- paths & components (incremental merge) --- let mut merged_paths = Map::new(); + let mut merged_webhooks = Map::new(); let mut merged_components: HashMap> = HashMap::new(); + let mut merged_component_extras = Map::new(); let mut merged_tags: Vec = Vec::new(); let mut merged_servers: Vec = Vec::new(); + let mut merged_security: Vec = Vec::new(); let mut merged_custom_top_level = Map::new(); for (source_name, tag_prefix, mut spec) in specs { + let ident = sanitize_ident(&source_name); + // Phase 1: detect component conflicts and build a $ref rename map - let rename_map = build_rename_map(&source_name, &spec, &merged_components, config)?; + let rename_map = build_rename_map(&ident, &spec, &merged_components, config); // Phase 2: rewrite $refs in the source spec if needed if !rename_map.is_empty() { @@ -61,18 +89,50 @@ pub fn merge_specs( } // Phase 3a: merge paths - merge_paths(&source_name, &spec, &mut merged_paths, config)?; + merge_path_items( + &source_name, + "path", + spec.get("paths"), + &mut merged_paths, + &config.conflict_strategy, + |path| { + if config.prefix_paths { + format!("/{ident}{path}") + } else { + path.to_string() + } + }, + |path, n| match n { + 1 => format!("/{ident}{path}"), + n => format!("/{ident}_{n}{path}"), + }, + )?; - // Phase 3b: merge components + // Phase 3b: merge webhooks (OpenAPI 3.1) + merge_path_items( + &source_name, + "webhook", + spec.get("webhooks"), + &mut merged_webhooks, + &config.conflict_strategy, + str::to_string, + |name, n| match n { + 1 => format!("{ident}_{name}"), + n => format!("{ident}_{n}_{name}"), + }, + )?; + + // Phase 3c: merge components merge_components( &source_name, &spec, &mut merged_components, + &mut merged_component_extras, &rename_map, config, )?; - // Phase 3c: merge tags + // Phase 3d: merge tags if let Some(Value::Array(tags)) = spec.get("tags") { for tag in tags { let original_name = tag.get("name").and_then(|n| n.as_str()); @@ -104,7 +164,7 @@ pub fn merge_specs( } } - // Phase 3d: merge servers (deduplicate by url) + // Phase 3e: merge servers (deduplicate by url) if let Some(Value::Array(servers)) = spec.get("servers") { for server in servers { let url = server.get("url").and_then(|u| u.as_str()); @@ -119,14 +179,23 @@ pub fn merge_specs( } } - // Phase 3e: merge non-standard top-level blocks (e.g. vendor extensions) + // Phase 3f: merge top-level security requirements (deduplicate) + if let Some(Value::Array(requirements)) = spec.get("security") { + for requirement in requirements { + if !merged_security.contains(requirement) { + merged_security.push(requirement.clone()); + } + } + } + + // Phase 3g: merge non-standard top-level blocks (e.g. vendor extensions) if let Some(root) = spec.as_object() { for (key, value) in root { if is_reserved_top_level_key(key) { continue; } if let Some(existing) = merged_custom_top_level.get_mut(key) { - deep_merge_value(existing, value); + deep_merge(existing, value); } else { merged_custom_top_level.insert(key.clone(), value.clone()); } @@ -140,11 +209,18 @@ pub fn merge_specs( merged.insert("paths".into(), Value::Object(merged_paths)); - if !merged_components.is_empty() { - let mut comp_obj = Map::new(); - for (ctype, items) in merged_components { - comp_obj.insert(ctype, Value::Object(items)); + if !merged_webhooks.is_empty() { + merged.insert("webhooks".into(), Value::Object(merged_webhooks)); + } + + let mut comp_obj = Map::new(); + for &ctype in COMPONENT_TYPES { + if let Some(items) = merged_components.remove(ctype) { + comp_obj.insert(ctype.into(), Value::Object(items)); } + } + comp_obj.extend(merged_component_extras); + if !comp_obj.is_empty() { merged.insert("components".into(), Value::Object(comp_obj)); } @@ -184,7 +260,14 @@ pub fn merge_specs( merged.insert("servers".into(), Value::Array(merged_servers)); } - Ok(Value::Object(merged)) + if !merged_security.is_empty() { + merged.insert("security".into(), Value::Array(merged_security)); + } + + Ok(MergeReport { + spec: Value::Object(merged), + warnings, + }) } // --------------------------------------------------------------------------- @@ -220,19 +303,63 @@ fn build_info( Ok(info) } +/// Warn about sources whose `major.minor` OpenAPI version differs from the first source. +fn version_warnings(specs: &[(String, String, Value)]) -> Vec { + fn minor(version: &str) -> &str { + match version.match_indices('.').nth(1) { + Some((idx, _)) => &version[..idx], + None => version, + } + } + + let Some(first) = specs[0].2.get("openapi").and_then(Value::as_str) else { + return Vec::new(); + }; + + specs + .iter() + .skip(1) + .filter_map(|(name, _, spec)| { + let version = spec.get("openapi")?.as_str()?; + (minor(version) != minor(first)).then(|| { + format!( + "source '{name}' uses OpenAPI {version}, but the merged spec uses {first} \ + (taken from the first source)" + ) + }) + }) + .collect() +} + +/// Make a source name safe for use in URL paths and component names +/// (`^[a-zA-Z0-9._-]+$`). +fn sanitize_ident(name: &str) -> String { + name.chars() + .map(|c| { + if c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_') { + c + } else { + '_' + } + }) + .collect() +} + fn is_reserved_top_level_key(key: &str) -> bool { matches!( key, - "openapi" | "info" | "paths" | "components" | "tags" | "servers" + "openapi" | "info" | "paths" | "webhooks" | "components" | "tags" | "servers" | "security" ) } -fn deep_merge_value(target: &mut Value, patch: &Value) { +/// Recursively merge `patch` into `target`. Objects are merged key by key; +/// any other value in `patch` replaces the one in `target`. +pub(crate) fn deep_merge(target: &mut Value, patch: &Value) { match (target, patch) { (Value::Object(target_map), Value::Object(patch_map)) => { for (key, patch_value) in patch_map { if let Some(existing) = target_map.get_mut(key) { - deep_merge_value(existing, patch_value); + deep_merge(existing, patch_value); } else { target_map.insert(key.clone(), patch_value.clone()); } @@ -246,14 +373,10 @@ fn deep_merge_value(target: &mut Value, patch: &Value) { /// Rewrite tag arrays inside all operations of a source spec, prefixing each tag name. fn rewrite_spec_operation_tags(spec: &mut Value, tag_prefix: &str, config: &MergeConfig) { - let http_methods = [ - "get", "post", "put", "patch", "delete", "options", "head", "trace", - ]; - if let Some(Value::Object(paths)) = spec.get_mut("paths") { for (_path_key, path_item) in paths.iter_mut() { if let Some(obj) = path_item.as_object_mut() { - for method in &http_methods { + for method in HTTP_METHODS { if let Some(operation) = obj.get_mut(*method) { if let Some(Value::Array(tags)) = operation.get_mut("tags") { for tag_val in tags.iter_mut() { @@ -273,48 +396,95 @@ fn rewrite_spec_operation_tags(spec: &mut Value, tag_prefix: &str, config: &Merg } } +fn component_ref(ctype: &str, name: &str) -> String { + format!("#/components/{ctype}/{name}") +} + /// For the rename strategy: figure out which component names in `spec` clash -/// with already-merged names and return a map from old `$ref` → new `$ref`. +/// with already-merged, *different* components and return a map from +/// old `$ref` → new `$ref`. +/// +/// Identical components are shared rather than renamed. Because renaming one +/// component changes the `$ref`s inside the components that point at it, the +/// comparison is repeated until no new renames appear. fn build_rename_map( - source_name: &str, + ident: &str, spec: &Value, merged_components: &HashMap>, config: &MergeConfig, -) -> Result, Error> { +) -> HashMap { let mut rename_map = HashMap::new(); if config.conflict_strategy != ConflictStrategy::Rename { - return Ok(rename_map); + return rename_map; } + let Some(components) = spec.get("components") else { + return rename_map; + }; + // Names that a renamed component must not take. + let mut taken: HashMap<&str, HashSet> = HashMap::new(); for &ctype in COMPONENT_TYPES { - if let Some(Value::Object(items)) = spec.get("components").and_then(|c| c.get(ctype)) { - if let Some(existing) = merged_components.get(ctype) { - for item_name in items.keys() { - if existing.contains_key(item_name) { - let old_ref = format!("#/components/{ctype}/{item_name}"); - let new_name = format!("{source_name}_{item_name}"); - let new_ref = format!("#/components/{ctype}/{new_name}"); - rename_map.insert(old_ref, new_ref); - } + let names = taken.entry(ctype).or_default(); + if let Some(existing) = merged_components.get(ctype) { + names.extend(existing.keys().cloned()); + } + if let Some(Value::Object(items)) = components.get(ctype) { + names.extend(items.keys().cloned()); + } + } + + loop { + let mut candidate = components.clone(); + rewrite_refs(&mut candidate, &rename_map); + + let mut changed = false; + for &ctype in COMPONENT_TYPES { + let (Some(Value::Object(items)), Some(existing)) = + (candidate.get(ctype), merged_components.get(ctype)) + else { + continue; + }; + for (name, value) in items { + let old_ref = component_ref(ctype, name); + if rename_map.contains_key(&old_ref) + || existing.get(name).is_none_or(|e| e == value) + { + continue; } + let names = taken.entry(ctype).or_default(); + let mut n = 1; + let new_name = loop { + let candidate_name = match n { + 1 => format!("{ident}_{name}"), + n => format!("{ident}_{n}_{name}"), + }; + if !names.contains(&candidate_name) { + break candidate_name; + } + n += 1; + }; + names.insert(new_name.clone()); + rename_map.insert(old_ref, component_ref(ctype, &new_name)); + changed = true; } } - } - Ok(rename_map) + if !changed { + return rename_map; + } + } } -/// Walk the JSON tree and rewrite any `$ref` value found in `rename_map`. +/// Walk the JSON tree and rewrite any `$ref` that points at (or into) a +/// component found in `rename_map`. fn rewrite_refs(value: &mut Value, rename_map: &HashMap) { match value { Value::Object(map) => { - // Check for $ref to rewrite let new_ref = map .get("$ref") .and_then(|v| v.as_str()) - .and_then(|s| rename_map.get(s)) - .cloned(); + .and_then(|s| renamed_ref(s, rename_map)); if let Some(new_ref) = new_ref { map.insert("$ref".into(), Value::String(new_ref)); @@ -333,46 +503,86 @@ fn rewrite_refs(value: &mut Value, rename_map: &HashMap) { } } -fn merge_paths( +/// `#/components/schemas/Pet/properties/id` → `#/components/schemas/b_Pet/properties/id` +fn renamed_ref(reference: &str, rename_map: &HashMap) -> Option { + let component_end = reference + .match_indices('/') + .nth(3) + .map_or(reference.len(), |(idx, _)| idx); + let (component, rest) = reference.split_at(component_end); + rename_map + .get(component) + .map(|new_ref| format!("{new_ref}{rest}")) +} + +/// Fields of `incoming` that `existing` also defines with a different value. +fn conflicting_fields(existing: &Value, incoming: &Value) -> Vec { + match (existing.as_object(), incoming.as_object()) { + (Some(existing), Some(incoming)) => incoming + .iter() + .filter(|(key, value)| existing.get(*key).is_some_and(|e| e != *value)) + .map(|(key, _)| key.clone()) + .collect(), + _ if existing != incoming => vec!["entire item".into()], + _ => Vec::new(), + } +} + +/// Merge a map of Path Item objects (`paths` or `webhooks`) into `merged`. +/// +/// Items under the same key are combined field by field (e.g. `GET` from one +/// source and `POST` from another). Only fields that both define with +/// different values are conflicts, resolved according to `strategy`. +fn merge_path_items( source_name: &str, - spec: &Value, + kind: &str, + items: Option<&Value>, merged: &mut Map, - config: &MergeConfig, + strategy: &ConflictStrategy, + key_for: impl Fn(&str) -> String, + renamed_key: impl Fn(&str, usize) -> String, ) -> Result<(), Error> { - let spec_paths = match spec.get("paths").and_then(|p| p.as_object()) { - Some(p) => p, - None => return Ok(()), + let Some(items) = items.and_then(Value::as_object) else { + return Ok(()); }; - for (path, operations) in spec_paths { - let key = if config.prefix_paths { - format!("/{source_name}{path}") - } else { - path.clone() + for (name, item) in items { + let key = key_for(name); + let Some(existing) = merged.get_mut(&key) else { + merged.insert(key, item.clone()); + continue; }; - if merged.contains_key(&key) { - match config.conflict_strategy { - ConflictStrategy::Error => { - return Err(Error::MergeConflict(format!( - "duplicate path '{key}' from source '{source_name}'" - ))); - } - ConflictStrategy::Overwrite => { - merged.insert(key, operations.clone()); - } - ConflictStrategy::Rename => { - let mut renamed_path = format!("/{source_name}{path}"); - let mut suffix = 2; - while merged.contains_key(&renamed_path) { - renamed_path = format!("/{source_name}_{suffix}{path}"); - suffix += 1; + let conflicts = conflicting_fields(existing, item); + if conflicts.is_empty() || *strategy == ConflictStrategy::Overwrite { + match (existing.as_object_mut(), item.as_object()) { + (Some(existing), Some(item)) => { + for (field, value) in item { + existing.insert(field.clone(), value.clone()); } - merged.insert(renamed_path, operations.clone()); } + _ => *existing = item.clone(), } - } else { - merged.insert(key, operations.clone()); + continue; + } + + match strategy { + ConflictStrategy::Error => { + return Err(Error::MergeConflict(format!( + "duplicate {kind} '{key}' (conflicting: {}) from source '{source_name}'", + conflicts.join(", ") + ))); + } + ConflictStrategy::Rename => { + let mut n = 1; + let mut renamed = renamed_key(name, n); + while merged.contains_key(&renamed) { + n += 1; + renamed = renamed_key(name, n); + } + merged.insert(renamed, item.clone()); + } + ConflictStrategy::Overwrite => unreachable!("handled above"), } } @@ -383,48 +593,48 @@ fn merge_components( source_name: &str, spec: &Value, merged: &mut HashMap>, + extras: &mut Map, rename_map: &HashMap, config: &MergeConfig, ) -> Result<(), Error> { - for &ctype in COMPONENT_TYPES { - let items = match spec - .get("components") - .and_then(|c| c.get(ctype)) - .and_then(|v| v.as_object()) - { - Some(items) => items, - None => continue, + let Some(components) = spec.get("components").and_then(Value::as_object) else { + return Ok(()); + }; + + for (ctype, items) in components { + // Vendor extensions and unknown keys under `components` are deep-merged as-is. + if !COMPONENT_TYPES.contains(&ctype.as_str()) { + match extras.get_mut(ctype) { + Some(existing) => deep_merge(existing, items), + None => { + extras.insert(ctype.clone(), items.clone()); + } + } + continue; + } + let Some(items) = items.as_object() else { + continue; }; - let merged_type = merged.entry(ctype.into()).or_default(); + let merged_type = merged.entry(ctype.clone()).or_default(); for (item_name, item_value) in items { - // Determine the key to insert under (may have been renamed) - let old_ref = format!("#/components/{ctype}/{item_name}"); - let insert_name = if let Some(new_ref) = rename_map.get(&old_ref) { - // Extract the new component name from the new $ref - new_ref.rsplit('/').next().unwrap_or(item_name).to_string() - } else { - item_name.clone() - }; + if let Some(new_ref) = rename_map.get(&component_ref(ctype, item_name)) { + let new_name = new_ref.rsplit('/').next().unwrap_or(item_name); + merged_type.insert(new_name.to_string(), item_value.clone()); + continue; + } - if merged_type.contains_key(&insert_name) && rename_map.get(&old_ref).is_none() { - match config.conflict_strategy { - ConflictStrategy::Error => { - return Err(Error::MergeConflict(format!( - "duplicate component {ctype}/{item_name} from source '{source_name}'" - ))); - } - ConflictStrategy::Overwrite => { - merged_type.insert(insert_name, item_value.clone()); - } - ConflictStrategy::Rename => { - // Already handled via rename_map – should not reach here - merged_type.insert(insert_name, item_value.clone()); - } + match merged_type.get(item_name) { + Some(existing) if existing == item_value => {} + Some(_) if config.conflict_strategy == ConflictStrategy::Error => { + return Err(Error::MergeConflict(format!( + "duplicate component {ctype}/{item_name} from source '{source_name}'" + ))); + } + _ => { + merged_type.insert(item_name.clone(), item_value.clone()); } - } else { - merged_type.insert(insert_name, item_value.clone()); } } } @@ -500,9 +710,11 @@ mod tests { #[test] fn merge_conflict_error_strategy() { + let mut alt = petstore_spec(); + alt["paths"]["/pets"]["get"]["summary"] = json!("Different"); let specs = vec![ ("a".into(), "a".into(), petstore_spec()), - ("b".into(), "b".into(), petstore_spec()), + ("b".into(), "b".into(), alt), ]; let config = MergeConfig::default(); // Error strategy let result = merge_specs(specs, &config); @@ -551,9 +763,12 @@ mod tests { let mut first = petstore_spec(); first["paths"]["/b/pets"] = json!({"get": {"summary": "Existing"}}); + let mut second = petstore_spec(); + second["paths"]["/pets"]["get"]["summary"] = json!("List b pets"); + let specs = vec![ ("a".into(), "a".into(), first), - ("b".into(), "b".into(), petstore_spec()), + ("b".into(), "b".into(), second), ]; let config = MergeConfig { conflict_strategy: ConflictStrategy::Rename, @@ -562,7 +777,10 @@ mod tests { let merged = merge_specs(specs, &config).unwrap(); assert_eq!(merged["paths"]["/b/pets"]["get"]["summary"], "Existing"); - assert_eq!(merged["paths"]["/b_2/pets"]["get"]["summary"], "List pets"); + assert_eq!( + merged["paths"]["/b_2/pets"]["get"]["summary"], + "List b pets" + ); } #[test] @@ -827,6 +1045,298 @@ mod tests { assert_eq!(merged["paths"]["/pets"]["get"]["tags"][0], "pets"); } + fn spec_with(paths: Value, components: Value) -> Value { + json!({ + "openapi": "3.0.3", + "info": { "title": "T", "version": "1" }, + "paths": paths, + "components": components + }) + } + + #[test] + fn components_are_emitted_in_canonical_order() { + let mut components = Map::new(); + for &ctype in COMPONENT_TYPES.iter().rev() { + components.insert(ctype.into(), json!({ "X": {} })); + } + let spec = spec_with(json!({}), Value::Object(components)); + + let merged = merge_specs( + vec![("a".into(), "a".into(), spec)], + &MergeConfig::default(), + ) + .unwrap(); + + let keys: Vec<&str> = merged["components"] + .as_object() + .unwrap() + .keys() + .map(String::as_str) + .collect(); + assert_eq!(keys, COMPONENT_TYPES); + } + + #[test] + fn rename_does_not_clobber_existing_renamed_name() { + let a = spec_with( + json!({}), + json!({ "schemas": { + "Pet": { "description": "a pet" }, + "b_Pet": { "description": "a's own b_Pet" } + }}), + ); + let b = spec_with( + json!({ "/b-pets": { "get": { "responses": { "200": { + "description": "ok", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } } } + }}}}}), + json!({ "schemas": { "Pet": { "description": "b pet" } } }), + ); + let config = MergeConfig { + conflict_strategy: ConflictStrategy::Rename, + ..Default::default() + }; + let merged = merge_specs( + vec![("a".into(), "a".into(), a), ("b".into(), "b".into(), b)], + &config, + ) + .unwrap(); + + let schemas = &merged["components"]["schemas"]; + assert_eq!(schemas["b_Pet"]["description"], "a's own b_Pet"); + assert_eq!(schemas["b_2_Pet"]["description"], "b pet"); + assert_eq!( + merged["paths"]["/b-pets"]["get"]["responses"]["200"]["content"]["application/json"] + ["schema"]["$ref"], + "#/components/schemas/b_2_Pet" + ); + } + + #[test] + fn disjoint_methods_on_same_path_are_combined() { + let a = spec_with( + json!({ "/pets": { "get": { "summary": "list" } } }), + json!({}), + ); + let b = spec_with( + json!({ "/pets": { "post": { "summary": "create" } } }), + json!({}), + ); + + let merged = merge_specs( + vec![("a".into(), "a".into(), a), ("b".into(), "b".into(), b)], + &MergeConfig::default(), + ) + .unwrap(); + + assert_eq!(merged["paths"]["/pets"]["get"]["summary"], "list"); + assert_eq!(merged["paths"]["/pets"]["post"]["summary"], "create"); + } + + #[test] + fn same_method_on_same_path_conflicts() { + let a = spec_with( + json!({ "/pets": { "get": { "summary": "list" } } }), + json!({}), + ); + let b = spec_with( + json!({ "/pets": { "get": { "summary": "other" } } }), + json!({}), + ); + + let err = merge_specs( + vec![("a".into(), "a".into(), a), ("b".into(), "b".into(), b)], + &MergeConfig::default(), + ) + .unwrap_err() + .to_string(); + + assert!(err.contains("duplicate path '/pets'"), "{err}"); + assert!(err.contains("get"), "{err}"); + } + + #[test] + fn identical_operations_are_not_conflicts() { + let health = json!({ "/health": { "get": { "summary": "health" } } }); + let a = spec_with(health.clone(), json!({})); + let b = spec_with(health, json!({})); + + let merged = merge_specs( + vec![("a".into(), "a".into(), a), ("b".into(), "b".into(), b)], + &MergeConfig::default(), + ) + .unwrap(); + + assert_eq!(merged["paths"]["/health"]["get"]["summary"], "health"); + } + + #[test] + fn identical_components_are_not_conflicts() { + let auth = + json!({ "securitySchemes": { "bearer": { "type": "http", "scheme": "bearer" } } }); + let a = spec_with(json!({}), auth.clone()); + let b = spec_with(json!({}), auth); + + for strategy in [ConflictStrategy::Error, ConflictStrategy::Rename] { + let config = MergeConfig { + conflict_strategy: strategy, + ..Default::default() + }; + let merged = merge_specs( + vec![ + ("a".into(), "a".into(), a.clone()), + ("b".into(), "b".into(), b.clone()), + ], + &config, + ) + .unwrap(); + let schemes = merged["components"]["securitySchemes"].as_object().unwrap(); + assert_eq!(schemes.keys().collect::>(), ["bearer"]); + } + } + + #[test] + fn identical_component_with_renamed_dependency_is_renamed() { + let error = json!({ "properties": { "code": { "$ref": "#/components/schemas/Code" } } }); + let a = spec_with( + json!({}), + json!({ "schemas": { "Error": error.clone(), "Code": { "type": "integer" } } }), + ); + let b = spec_with( + json!({ "/b": { "get": { "responses": { "default": { + "description": "err", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } + }}}}}), + json!({ "schemas": { "Error": error, "Code": { "type": "string" } } }), + ); + let config = MergeConfig { + conflict_strategy: ConflictStrategy::Rename, + ..Default::default() + }; + let merged = merge_specs( + vec![("a".into(), "a".into(), a), ("b".into(), "b".into(), b)], + &config, + ) + .unwrap(); + + let schemas = &merged["components"]["schemas"]; + assert_eq!(schemas["b_Code"]["type"], "string"); + assert_eq!( + schemas["b_Error"]["properties"]["code"]["$ref"], + "#/components/schemas/b_Code" + ); + assert_eq!( + merged["paths"]["/b"]["get"]["responses"]["default"]["content"]["application/json"] + ["schema"]["$ref"], + "#/components/schemas/b_Error" + ); + } + + #[test] + fn source_names_are_sanitized_in_paths_and_component_names() { + let a = spec_with( + json!({}), + json!({ "schemas": { "Pet": { "type": "object" } } }), + ); + let b = spec_with( + json!({ "/pets": { "get": { "summary": "list" } } }), + json!({ "schemas": { "Pet": { "type": "string" } } }), + ); + let config = MergeConfig { + conflict_strategy: ConflictStrategy::Rename, + prefix_paths: true, + ..Default::default() + }; + let merged = merge_specs( + vec![ + ("a".into(), "a".into(), a), + ("my api".into(), "my api".into(), b), + ], + &config, + ) + .unwrap(); + + assert!(merged["paths"]["/my_api/pets"].is_object()); + assert!(merged["components"]["schemas"]["my_api_Pet"].is_object()); + } + + #[test] + fn webhooks_are_merged_with_conflict_detection() { + let mut a = spec_with(json!({}), json!({})); + a["webhooks"] = json!({ "petCreated": { "post": { "summary": "a" } } }); + let mut b = spec_with(json!({}), json!({})); + b["webhooks"] = json!({ "userCreated": { "post": { "summary": "b" } } }); + let mut c = spec_with(json!({}), json!({})); + c["webhooks"] = json!({ "petCreated": { "post": { "summary": "c" } } }); + + let merged = merge_specs( + vec![ + ("a".into(), "a".into(), a.clone()), + ("b".into(), "b".into(), b), + ], + &MergeConfig::default(), + ) + .unwrap(); + assert!(merged["webhooks"]["petCreated"].is_object()); + assert!(merged["webhooks"]["userCreated"].is_object()); + + let err = merge_specs( + vec![("a".into(), "a".into(), a), ("c".into(), "c".into(), c)], + &MergeConfig::default(), + ) + .unwrap_err() + .to_string(); + assert!(err.contains("petCreated"), "{err}"); + } + + #[test] + fn top_level_security_is_unioned() { + let mut a = spec_with(json!({}), json!({})); + a["security"] = json!([{ "bearer": [] }]); + let mut b = spec_with(json!({}), json!({})); + b["security"] = json!([{ "apiKey": [] }, { "bearer": [] }]); + + let merged = merge_specs( + vec![("a".into(), "a".into(), a), ("b".into(), "b".into(), b)], + &MergeConfig::default(), + ) + .unwrap(); + + assert_eq!( + merged["security"], + json!([{ "bearer": [] }, { "apiKey": [] }]) + ); + } + + #[test] + fn mixed_openapi_minor_versions_produce_warning() { + let a = spec_with(json!({}), json!({})); + let mut b = spec_with(json!({}), json!({})); + b["openapi"] = json!("3.1.0"); + let mut c = spec_with(json!({}), json!({})); + c["openapi"] = json!("3.0.1"); + + let report = merge_specs_with_report( + vec![ + ("a".into(), "a".into(), a.clone()), + ("b".into(), "b".into(), b), + ], + &MergeConfig::default(), + ) + .unwrap(); + assert_eq!(report.spec["openapi"], "3.0.3"); + assert_eq!(report.warnings.len(), 1); + assert!(report.warnings[0].contains("3.1.0")); + + let report = merge_specs_with_report( + vec![("a".into(), "a".into(), a), ("c".into(), "c".into(), c)], + &MergeConfig::default(), + ) + .unwrap(); + assert!(report.warnings.is_empty()); + } + #[test] fn merge_servers_from_sources_when_no_override() { let mut a = petstore_spec(); diff --git a/src/source.rs b/src/source.rs index ecb8898..cfcbee9 100644 --- a/src/source.rs +++ b/src/source.rs @@ -3,52 +3,58 @@ use std::time::Duration; use crate::config::Source; use crate::error::Error; +use crate::merge::deep_merge; /// Load an OpenAPI spec from a [`Source`], returning `(name, parsed_value)`. pub async fn load_source(source: &Source) -> Result<(String, Value), Error> { - match source { + load_source_with_client(source, &http_client()?).await +} + +pub(crate) fn http_client() -> Result { + reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .map_err(|e| Error::Config(format!("failed to create HTTP client: {e}"))) +} + +pub(crate) async fn load_source_with_client( + source: &Source, + client: &reqwest::Client, +) -> Result<(String, Value), Error> { + let content = match source { Source::File { path, .. } => { - let content = std::fs::read_to_string(path).map_err(|e| Error::FileRead { - path: path.display().to_string(), - source: e, - })?; - let value = parse_and_prepare(&content, source)?; - validate_openapi(&value, &source.display_name())?; - Ok((source.display_name(), value)) + tokio::fs::read_to_string(path) + .await + .map_err(|e| Error::FileRead { + path: path.display().to_string(), + source: e, + })? } Source::Http { url, headers, .. } => { - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(30)) - .build() - .map_err(|e| Error::HttpRequest { - url: url.clone(), - source: e, - })?; - let mut request = client.get(url); + let url = expand_env(url)?; + let http_err = |e| Error::HttpRequest { + url: url.clone(), + source: e, + }; + let mut request = client.get(&url); for (key, value) in headers { - request = request.header(key, value); + request = request.header(key, expand_env(value)?); } - let response = request + request .send() .await - .map_err(|e| Error::HttpRequest { - url: url.clone(), - source: e, - })? + .map_err(http_err)? .error_for_status() - .map_err(|e| Error::HttpRequest { - url: url.clone(), - source: e, - })?; - let content = response.text().await.map_err(|e| Error::HttpRequest { - url: url.clone(), - source: e, - })?; - let value = parse_and_prepare(&content, source)?; - validate_openapi(&value, &source.display_name())?; - Ok((source.display_name(), value)) + .map_err(http_err)? + .text() + .await + .map_err(http_err)? } - } + }; + + let value = parse_and_prepare(&content, source)?; + validate_openapi(&value, &source.display_name())?; + Ok((source.display_name(), value)) } fn parse_and_prepare(content: &str, source: &Source) -> Result { @@ -76,21 +82,27 @@ fn source_additional_blocks(source: &Source) -> Option<&Value> { } } -fn deep_merge(target: &mut Value, patch: &Value) { - match (target, patch) { - (Value::Object(target_map), Value::Object(patch_map)) => { - for (key, patch_value) in patch_map { - if let Some(existing) = target_map.get_mut(key) { - deep_merge(existing, patch_value); - } else { - target_map.insert(key.clone(), patch_value.clone()); - } - } - } - (target_slot, patch_value) => { - *target_slot = patch_value.clone(); - } +/// Replace `${NAME}` placeholders with environment variables. +fn expand_env(text: &str) -> Result { + expand_env_with(text, |name| std::env::var(name).ok()) +} + +fn expand_env_with(text: &str, lookup: impl Fn(&str) -> Option) -> Result { + let mut out = String::with_capacity(text.len()); + let mut rest = text; + while let Some(start) = rest.find("${") { + let Some(len) = rest[start + 2..].find('}') else { + break; + }; + let name = &rest[start + 2..start + 2 + len]; + let value = lookup(name) + .ok_or_else(|| Error::Config(format!("environment variable '{name}' is not set")))?; + out.push_str(&rest[..start]); + out.push_str(&value); + rest = &rest[start + 2 + len + 1..]; } + out.push_str(rest); + Ok(out) } /// Try JSON first, fall back to YAML. @@ -199,3 +211,40 @@ mod tests { assert!(err.contains("additional_blocks")); } } + +#[cfg(test)] +mod env_tests { + use super::*; + + fn lookup(name: &str) -> Option { + match name { + "TOKEN" => Some("secret".into()), + "HOST" => Some("api.test".into()), + _ => None, + } + } + + #[test] + fn expands_env_placeholders() { + assert_eq!( + expand_env_with("Bearer ${TOKEN} @ ${HOST}", lookup).unwrap(), + "Bearer secret @ api.test" + ); + } + + #[test] + fn leaves_text_without_placeholders_alone() { + assert_eq!( + expand_env_with("$5 {x} $TOKEN", lookup).unwrap(), + "$5 {x} $TOKEN" + ); + } + + #[test] + fn missing_env_var_is_an_error() { + let err = expand_env_with("Bearer ${NOPE}", lookup) + .unwrap_err() + .to_string(); + assert!(err.contains("NOPE"), "{err}"); + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 853715a..e80f8dd 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,8 +1,8 @@ use std::path::Path; use openapi_aggregator::{ - aggregate, aggregate_from_file, load_source, merge_specs, Config, ConflictStrategy, - MergeConfig, Source, + aggregate, aggregate_from_file, aggregate_with_report, load_source, merge_specs, Config, + ConflictStrategy, MergeConfig, Source, }; fn fixtures() -> &'static Path { @@ -301,3 +301,112 @@ async fn http_source_rejects_non_success_status() { let error = load_source(&source).await.unwrap_err(); assert!(error.to_string().contains("HTTP request failed")); } + +#[tokio::test] +async fn http_source_expands_env_vars_in_headers() { + std::env::set_var("OPENAPI_AGGREGATOR_TEST_TOKEN", "from-env"); + + let mut server = mockito::Server::new_async().await; + let _mock = server + .mock("GET", "/openapi.json") + .match_header("Authorization", "Bearer from-env") + .with_body(r#"{"openapi":"3.0.3","info":{"title":"Env","version":"1"},"paths":{}}"#) + .create_async() + .await; + + let source = Source::Http { + name: Some("env".into()), + url: format!("{}/openapi.json", server.url()), + headers: [( + "Authorization".into(), + "Bearer ${OPENAPI_AGGREGATOR_TEST_TOKEN}".into(), + )] + .into_iter() + .collect(), + tag_prefix: None, + additional_blocks: None, + }; + + let (_, spec) = load_source(&source).await.unwrap(); + assert_eq!(spec["info"]["title"], "Env"); +} + +#[tokio::test] +async fn aggregate_with_report_returns_version_warnings() { + let dir = tempfile::tempdir().unwrap(); + let v31 = dir.path().join("v31.yaml"); + std::fs::write( + &v31, + "openapi: 3.1.0\ninfo: {title: New, version: '1'}\npaths: {}\n", + ) + .unwrap(); + + let config = Config { + sources: vec![ + Source::File { + name: Some("petstore".into()), + path: fixtures().join("petstore.yaml"), + tag_prefix: None, + additional_blocks: None, + }, + Source::File { + name: Some("new".into()), + path: v31, + tag_prefix: None, + additional_blocks: None, + }, + ], + output: Default::default(), + merge: MergeConfig::default(), + }; + + let report = aggregate_with_report(&config).await.unwrap(); + assert_eq!(report.spec["openapi"], "3.0.3"); + assert_eq!(report.warnings.len(), 1); +} + +fn write_cli_config(dir: &Path, format: &str) -> std::path::PathBuf { + let config_path = dir.join("config.yaml"); + std::fs::write( + &config_path, + format!( + "sources:\n - path: {}\noutput:\n format: {format}\n", + fixtures().join("petstore.yaml").display() + ), + ) + .unwrap(); + config_path +} + +#[test] +fn cli_uses_output_format_from_config() { + let dir = tempfile::tempdir().unwrap(); + let config_path = write_cli_config(dir.path(), "json"); + + let output = std::process::Command::new(env!("CARGO_BIN_EXE_openapi-aggregator")) + .arg("-c") + .arg(&config_path) + .output() + .unwrap(); + + assert!(output.status.success()); + let parsed: serde_json::Value = serde_json::from_slice(&output.stdout) + .expect("stdout should be JSON when config sets output.format: json"); + assert_eq!(parsed["info"]["title"], "Petstore API"); +} + +#[test] +fn cli_format_flag_overrides_config() { + let dir = tempfile::tempdir().unwrap(); + let config_path = write_cli_config(dir.path(), "json"); + + let output = std::process::Command::new(env!("CARGO_BIN_EXE_openapi-aggregator")) + .arg("-c") + .arg(&config_path) + .args(["-f", "yaml"]) + .output() + .unwrap(); + + assert!(output.status.success()); + assert!(String::from_utf8_lossy(&output.stdout).starts_with("openapi:")); +} From ad26f5ef2222acabbbe286e6b1ba9a7bec7bb7e7 Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 10:48:13 +0200 Subject: [PATCH 02/15] ci(release): commit Cargo.lock and pass release notes via env --- .github/workflows/tag-release.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index 538f21b..cb47885 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -121,7 +121,7 @@ jobs: if: steps.skip_check.outputs.skip != 'true' run: | sed -i "0,/^version = .*/s//version = \"${{ steps.version.outputs.version }}\"/" Cargo.toml - cargo check --quiet 2>/dev/null || true # regenerate Cargo.lock if present + cargo update --workspace # sync the package version in Cargo.lock - name: Install git-cliff if: steps.skip_check.outputs.skip != 'true' @@ -147,7 +147,7 @@ jobs: - name: Commit version bump & changelog if: steps.skip_check.outputs.skip != 'true' run: | - git add Cargo.toml CHANGELOG.md + git add Cargo.toml Cargo.lock CHANGELOG.md git commit -m "chore(release): ${{ steps.version.outputs.tag }}" git push @@ -159,7 +159,9 @@ jobs: - name: Write release notes to file if: steps.skip_check.outputs.skip != 'true' - run: echo '${{ steps.release_notes.outputs.content }}' > /tmp/release_notes.md + env: + RELEASE_NOTES: ${{ steps.release_notes.outputs.content }} + run: printf '%s\n' "$RELEASE_NOTES" > /tmp/release_notes.md - name: Upload release notes if: steps.skip_check.outputs.skip != 'true' From a4258b843505ee4517a335244c446eb54c7482bd Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 11:51:06 +0200 Subject: [PATCH 03/15] refactor: move crate into a cargo workspace --- Cargo.lock | 2 +- Cargo.toml | 27 ++++------------- crates/openapi-aggregator/Cargo.toml | 29 +++++++++++++++++++ .../openapi-aggregator/src}/config.rs | 0 .../openapi-aggregator/src}/error.rs | 0 {src => crates/openapi-aggregator/src}/lib.rs | 0 .../openapi-aggregator/src}/main.rs | 0 .../openapi-aggregator/src}/merge.rs | 0 .../openapi-aggregator/src}/source.rs | 0 .../tests}/fixtures/conflict.json | 0 .../tests}/fixtures/petstore.yaml | 0 .../tests}/fixtures/users.yaml | 0 .../tests}/integration_test.rs | 0 13 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 crates/openapi-aggregator/Cargo.toml rename {src => crates/openapi-aggregator/src}/config.rs (100%) rename {src => crates/openapi-aggregator/src}/error.rs (100%) rename {src => crates/openapi-aggregator/src}/lib.rs (100%) rename {src => crates/openapi-aggregator/src}/main.rs (100%) rename {src => crates/openapi-aggregator/src}/merge.rs (100%) rename {src => crates/openapi-aggregator/src}/source.rs (100%) rename {tests => crates/openapi-aggregator/tests}/fixtures/conflict.json (100%) rename {tests => crates/openapi-aggregator/tests}/fixtures/petstore.yaml (100%) rename {tests => crates/openapi-aggregator/tests}/fixtures/users.yaml (100%) rename {tests => crates/openapi-aggregator/tests}/integration_test.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index e1aad27..3e72618 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -705,7 +705,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openapi-aggregator" -version = "0.6.0" +version = "0.6.1" dependencies = [ "clap", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 8cff557..a92a2a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,29 +1,14 @@ -[package] -name = "openapi-aggregator" +[workspace] +members = ["crates/*"] +resolver = "2" + +[workspace.package] version = "0.6.1" edition = "2021" -description = "Aggregate and merge OpenAPI specifications from multiple sources" license = "MIT" -readme = "README.md" - -[[bin]] -name = "openapi-aggregator" -path = "src/main.rs" -[lib] -name = "openapi_aggregator" -path = "src/lib.rs" - -[dependencies] -clap = { version = "4", features = ["derive"] } -futures-util = { version = "0.3", default-features = false, features = ["alloc"] } -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +[workspace.dependencies] serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } serde_yaml = "0.9" thiserror = "1" -tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs"] } - -[dev-dependencies] -mockito = "1" -tempfile = "3" diff --git a/crates/openapi-aggregator/Cargo.toml b/crates/openapi-aggregator/Cargo.toml new file mode 100644 index 0000000..b1c6259 --- /dev/null +++ b/crates/openapi-aggregator/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "openapi-aggregator" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Aggregate and merge OpenAPI specifications from multiple sources" +readme = "../../README.md" + +[[bin]] +name = "openapi-aggregator" +path = "src/main.rs" + +[lib] +name = "openapi_aggregator" +path = "src/lib.rs" + +[dependencies] +clap = { version = "4", features = ["derive"] } +futures-util = { version = "0.3", default-features = false, features = ["alloc"] } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +serde.workspace = true +serde_json.workspace = true +serde_yaml.workspace = true +thiserror.workspace = true +tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs"] } + +[dev-dependencies] +mockito = "1" +tempfile = "3" diff --git a/src/config.rs b/crates/openapi-aggregator/src/config.rs similarity index 100% rename from src/config.rs rename to crates/openapi-aggregator/src/config.rs diff --git a/src/error.rs b/crates/openapi-aggregator/src/error.rs similarity index 100% rename from src/error.rs rename to crates/openapi-aggregator/src/error.rs diff --git a/src/lib.rs b/crates/openapi-aggregator/src/lib.rs similarity index 100% rename from src/lib.rs rename to crates/openapi-aggregator/src/lib.rs diff --git a/src/main.rs b/crates/openapi-aggregator/src/main.rs similarity index 100% rename from src/main.rs rename to crates/openapi-aggregator/src/main.rs diff --git a/src/merge.rs b/crates/openapi-aggregator/src/merge.rs similarity index 100% rename from src/merge.rs rename to crates/openapi-aggregator/src/merge.rs diff --git a/src/source.rs b/crates/openapi-aggregator/src/source.rs similarity index 100% rename from src/source.rs rename to crates/openapi-aggregator/src/source.rs diff --git a/tests/fixtures/conflict.json b/crates/openapi-aggregator/tests/fixtures/conflict.json similarity index 100% rename from tests/fixtures/conflict.json rename to crates/openapi-aggregator/tests/fixtures/conflict.json diff --git a/tests/fixtures/petstore.yaml b/crates/openapi-aggregator/tests/fixtures/petstore.yaml similarity index 100% rename from tests/fixtures/petstore.yaml rename to crates/openapi-aggregator/tests/fixtures/petstore.yaml diff --git a/tests/fixtures/users.yaml b/crates/openapi-aggregator/tests/fixtures/users.yaml similarity index 100% rename from tests/fixtures/users.yaml rename to crates/openapi-aggregator/tests/fixtures/users.yaml diff --git a/tests/integration_test.rs b/crates/openapi-aggregator/tests/integration_test.rs similarity index 100% rename from tests/integration_test.rs rename to crates/openapi-aggregator/tests/integration_test.rs From fc308cc6b0e65754871730b50c8f494e306c9a6c Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 11:53:02 +0200 Subject: [PATCH 04/15] refactor: extract pure openapi-aggregator-core crate --- .github/workflows/ci.yml | 6 +- .github/workflows/tag-release.yml | 2 +- Cargo.lock | 12 +- Cargo.toml | 1 + README.md | 2 +- crates/openapi-aggregator-core/Cargo.toml | 12 ++ .../src/config.rs | 20 ++ crates/openapi-aggregator-core/src/error.rs | 19 ++ crates/openapi-aggregator-core/src/lib.rs | 15 ++ .../src/merge.rs | 0 crates/openapi-aggregator-core/src/prepare.rs | 182 ++++++++++++++++++ crates/openapi-aggregator/Cargo.toml | 2 +- crates/openapi-aggregator/src/error.rs | 16 +- crates/openapi-aggregator/src/lib.rs | 22 +-- crates/openapi-aggregator/src/source.rs | 154 ++------------- 15 files changed, 293 insertions(+), 172 deletions(-) create mode 100644 crates/openapi-aggregator-core/Cargo.toml rename crates/{openapi-aggregator => openapi-aggregator-core}/src/config.rs (94%) create mode 100644 crates/openapi-aggregator-core/src/error.rs create mode 100644 crates/openapi-aggregator-core/src/lib.rs rename crates/{openapi-aggregator => openapi-aggregator-core}/src/merge.rs (100%) create mode 100644 crates/openapi-aggregator-core/src/prepare.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04098b5..6e9b59a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,10 @@ jobs: run: cargo fmt --all -- --check - name: Clippy run: cargo clippy --all-targets --all-features -- -D warnings + - name: Core builds for wasm32 + run: | + rustup target add wasm32-unknown-unknown + cargo check -p openapi-aggregator-core --target wasm32-unknown-unknown test: name: Test @@ -42,6 +46,6 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Build release binary - run: cargo build --release + run: cargo build --release -p openapi-aggregator - name: Smoke test run: ./target/release/openapi-aggregator --help diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index cb47885..c5b47b7 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -214,7 +214,7 @@ jobs: key: ${{ matrix.target }} - name: Build - run: cargo build --release --target ${{ matrix.target }} + run: cargo build --release -p openapi-aggregator --target ${{ matrix.target }} - name: Package (Unix) if: runner.os != 'Windows' diff --git a/Cargo.lock b/Cargo.lock index 3e72618..2a3cb49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -710,8 +710,8 @@ dependencies = [ "clap", "futures-util", "mockito", + "openapi-aggregator-core", "reqwest", - "serde", "serde_json", "serde_yaml", "tempfile", @@ -719,6 +719,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "openapi-aggregator-core" +version = "0.6.0" +dependencies = [ + "serde", + "serde_json", + "serde_yaml", + "thiserror 1.0.69", +] + [[package]] name = "parking_lot" version = "0.12.5" diff --git a/Cargo.toml b/Cargo.toml index a92a2a9..5ca484f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" license = "MIT" [workspace.dependencies] +openapi-aggregator-core = { path = "crates/openapi-aggregator-core" } serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } serde_yaml = "0.9" diff --git a/README.md b/README.md index 8254bc6..5ecfae7 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ curl -sSfL https://raw.githubusercontent.com/includeamin/openapi-aggregator/main ### From source ```sh -cargo install --path . +cargo install --path crates/openapi-aggregator ``` ### Pre-built binaries diff --git a/crates/openapi-aggregator-core/Cargo.toml b/crates/openapi-aggregator-core/Cargo.toml new file mode 100644 index 0000000..168722e --- /dev/null +++ b/crates/openapi-aggregator-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "openapi-aggregator-core" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Pure merge logic for openapi-aggregator (no I/O, wasm-compatible)" + +[dependencies] +serde.workspace = true +serde_json.workspace = true +serde_yaml.workspace = true +thiserror.workspace = true diff --git a/crates/openapi-aggregator/src/config.rs b/crates/openapi-aggregator-core/src/config.rs similarity index 94% rename from crates/openapi-aggregator/src/config.rs rename to crates/openapi-aggregator-core/src/config.rs index f7cbf25..1d5e663 100644 --- a/crates/openapi-aggregator/src/config.rs +++ b/crates/openapi-aggregator-core/src/config.rs @@ -14,6 +14,14 @@ pub struct Config { pub merge: MergeConfig, } +impl Config { + /// Parse a YAML (or JSON) config document. + pub fn from_yaml(text: &str) -> Result { + serde_yaml::from_str(text) + .map_err(|e| crate::error::Error::Config(format!("failed to parse config file: {e}"))) + } +} + /// A source of an OpenAPI specification. /// /// Detected automatically: if `url` is present it is treated as an HTTP source, @@ -306,6 +314,18 @@ mod tests { assert!(matches!(&config.sources[1], Source::Http { headers, .. } if headers["A"] == "b")); } + #[test] + fn from_yaml_wraps_errors_as_config_errors() { + let err = Config::from_yaml("sources: [{name: x}]") + .unwrap_err() + .to_string(); + assert!( + err.starts_with("configuration error: failed to parse config file:"), + "{err}" + ); + assert!(err.contains("either 'path' or 'url'"), "{err}"); + } + #[test] fn http_source_default_name_is_host() { let source = Source::Http { diff --git a/crates/openapi-aggregator-core/src/error.rs b/crates/openapi-aggregator-core/src/error.rs new file mode 100644 index 0000000..a0ebf97 --- /dev/null +++ b/crates/openapi-aggregator-core/src/error.rs @@ -0,0 +1,19 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("failed to parse spec content: {0}")] + Parse(String), + + #[error("invalid OpenAPI spec from source '{name}': {reason}")] + InvalidSpec { name: String, reason: String }, + + #[error("merge conflict: {0}")] + MergeConflict(String), + + #[error("configuration error: {0}")] + Config(String), + + #[error("no sources provided")] + NoSources, +} diff --git a/crates/openapi-aggregator-core/src/lib.rs b/crates/openapi-aggregator-core/src/lib.rs new file mode 100644 index 0000000..25b8043 --- /dev/null +++ b/crates/openapi-aggregator-core/src/lib.rs @@ -0,0 +1,15 @@ +//! Pure OpenAPI aggregation logic: config types, spec parsing/validation and merging. +//! Contains no I/O, so it also builds for `wasm32-unknown-unknown`. + +pub mod config; +pub mod error; +pub mod merge; +pub mod prepare; + +pub use config::{ + Config, ConflictStrategy, InfoOverride, MergeConfig, OutputConfig, OutputFormat, ServerEntry, + Source, TagEntry, TagPrefixStrategy, +}; +pub use error::Error; +pub use merge::{merge_specs, merge_specs_with_report, MergeReport}; +pub use prepare::{parse_content, prepare_source}; diff --git a/crates/openapi-aggregator/src/merge.rs b/crates/openapi-aggregator-core/src/merge.rs similarity index 100% rename from crates/openapi-aggregator/src/merge.rs rename to crates/openapi-aggregator-core/src/merge.rs diff --git a/crates/openapi-aggregator-core/src/prepare.rs b/crates/openapi-aggregator-core/src/prepare.rs new file mode 100644 index 0000000..beee319 --- /dev/null +++ b/crates/openapi-aggregator-core/src/prepare.rs @@ -0,0 +1,182 @@ +use serde_json::Value; + +use crate::config::Source; +use crate::error::Error; +use crate::merge::deep_merge; + +/// Parse spec text, apply the source's `additional_blocks` and validate it. +/// Returns `(display_name, spec)`. +pub fn prepare_source(content: &str, source: &Source) -> Result<(String, Value), Error> { + let name = source.display_name(); + let value = parse_and_prepare(content, source)?; + validate_openapi(&value, &name)?; + Ok((name, value)) +} + +fn parse_and_prepare(content: &str, source: &Source) -> Result { + let mut value = parse_content(content)?; + if let Some(blocks) = source_additional_blocks(source) { + if !blocks.is_object() { + return Err(Error::InvalidSpec { + name: source.display_name(), + reason: "'additional_blocks' must be a mapping/object".into(), + }); + } + deep_merge(&mut value, blocks); + } + Ok(value) +} + +fn source_additional_blocks(source: &Source) -> Option<&Value> { + match source { + Source::File { + additional_blocks, .. + } + | Source::Http { + additional_blocks, .. + } => additional_blocks.as_ref(), + } +} + +/// Try JSON first, fall back to YAML. +pub fn parse_content(content: &str) -> Result { + serde_json::from_str(content).or_else(|_| { + serde_yaml::from_str::(content) + .map_err(|e| Error::Parse(format!("content is neither valid JSON nor YAML: {e}"))) + }) +} + +/// Minimal validation: the value must be an object with an `openapi` field. +fn validate_openapi(value: &Value, source_name: &str) -> Result<(), Error> { + match value.get("openapi").and_then(|v| v.as_str()) { + Some(v) if v.starts_with("3.") => Ok(()), + Some(v) => Err(Error::InvalidSpec { + name: source_name.into(), + reason: format!("unsupported OpenAPI version '{v}' (only 3.x is supported)"), + }), + None if value.get("swagger").is_some() => Err(Error::InvalidSpec { + name: source_name.into(), + reason: "Swagger 2.0 specs are not supported (only OpenAPI 3.x is supported)".into(), + }), + None => Err(Error::InvalidSpec { + name: source_name.into(), + reason: "missing 'openapi' field".into(), + }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::path::PathBuf; + + #[test] + fn parse_json_content() { + let json = r#"{"openapi":"3.0.3","info":{"title":"T","version":"1"},"paths":{}}"#; + let v = parse_content(json).unwrap(); + assert_eq!(v["openapi"], "3.0.3"); + } + + #[test] + fn parse_yaml_content() { + let yaml = "openapi: '3.0.3'\ninfo:\n title: T\n version: '1'\npaths: {}"; + let v = parse_content(yaml).unwrap(); + assert_eq!(v["openapi"], "3.0.3"); + } + + #[test] + fn validate_rejects_missing_openapi() { + let v = serde_json::json!({"info": {}}); + assert!(validate_openapi(&v, "test").is_err()); + } + + #[test] + fn validate_rejects_v2() { + let v = serde_json::json!({"openapi": "2.0"}); + assert!(validate_openapi(&v, "test").is_err()); + } + + #[test] + fn additional_blocks_are_deep_merged() { + let source = Source::File { + name: Some("test".into()), + path: PathBuf::from("ignored.yaml"), + tag_prefix: None, + additional_blocks: Some(json!({ + "x-vendor-root": { "enabled": true }, + "paths": { + "/pets": { + "get": { + "x-vendor-extension": { "timeout": 3000 } + } + } + } + })), + }; + + let base = r#"{ + "openapi": "3.0.3", + "info": {"title": "T", "version": "1"}, + "paths": { + "/pets": { + "get": {"summary": "list pets"} + } + } + }"#; + + let merged = parse_and_prepare(base, &source).unwrap(); + assert_eq!(merged["x-vendor-root"]["enabled"], true); + assert_eq!(merged["paths"]["/pets"]["get"]["summary"], "list pets"); + assert_eq!( + merged["paths"]["/pets"]["get"]["x-vendor-extension"]["timeout"], + 3000 + ); + } + + #[test] + fn prepare_source_applies_blocks_validates_and_names() { + let source = Source::File { + name: Some("pets".into()), + path: PathBuf::from("ignored.yaml"), + tag_prefix: None, + additional_blocks: Some(json!({ "x-team": "core" })), + }; + let (name, spec) = prepare_source( + r#"{"openapi":"3.0.3","info":{"title":"T","version":"1"},"paths":{}}"#, + &source, + ) + .unwrap(); + assert_eq!(name, "pets"); + assert_eq!(spec["x-team"], "core"); + } + + #[test] + fn prepare_source_rejects_non_openapi_with_source_name() { + let source = Source::File { + name: Some("legacy".into()), + path: PathBuf::from("ignored.yaml"), + tag_prefix: None, + additional_blocks: None, + }; + let err = prepare_source("swagger: '2.0'\ninfo: {title: T, version: '1'}\n", &source) + .unwrap_err() + .to_string(); + assert!(err.contains("legacy"), "{err}"); + assert!(err.contains("Swagger 2.0") && err.contains("3.x"), "{err}"); + } + + #[test] + fn additional_blocks_must_be_object() { + let source = Source::File { + name: Some("test".into()), + path: PathBuf::from("ignored.yaml"), + tag_prefix: None, + additional_blocks: Some(json!([1, 2, 3])), + }; + + let base = r#"{"openapi":"3.0.3","info":{"title":"T","version":"1"},"paths":{}}"#; + let err = parse_and_prepare(base, &source).unwrap_err().to_string(); + assert!(err.contains("additional_blocks")); + } +} diff --git a/crates/openapi-aggregator/Cargo.toml b/crates/openapi-aggregator/Cargo.toml index b1c6259..7fc6086 100644 --- a/crates/openapi-aggregator/Cargo.toml +++ b/crates/openapi-aggregator/Cargo.toml @@ -17,8 +17,8 @@ path = "src/lib.rs" [dependencies] clap = { version = "4", features = ["derive"] } futures-util = { version = "0.3", default-features = false, features = ["alloc"] } +openapi-aggregator-core.workspace = true reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } -serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true thiserror.workspace = true diff --git a/crates/openapi-aggregator/src/error.rs b/crates/openapi-aggregator/src/error.rs index ca74496..ff7ad66 100644 --- a/crates/openapi-aggregator/src/error.rs +++ b/crates/openapi-aggregator/src/error.rs @@ -9,9 +9,6 @@ pub enum Error { source: std::io::Error, }, - #[error("failed to parse spec content: {0}")] - Parse(String), - #[error("HTTP request failed for '{url}': {source}")] HttpRequest { url: String, @@ -19,15 +16,6 @@ pub enum Error { source: reqwest::Error, }, - #[error("invalid OpenAPI spec from source '{name}': {reason}")] - InvalidSpec { name: String, reason: String }, - - #[error("merge conflict: {0}")] - MergeConflict(String), - - #[error("configuration error: {0}")] - Config(String), - - #[error("no sources provided")] - NoSources, + #[error(transparent)] + Core(#[from] openapi_aggregator_core::Error), } diff --git a/crates/openapi-aggregator/src/lib.rs b/crates/openapi-aggregator/src/lib.rs index 2c77d9e..3fb7adb 100644 --- a/crates/openapi-aggregator/src/lib.rs +++ b/crates/openapi-aggregator/src/lib.rs @@ -1,19 +1,18 @@ -pub mod config; pub mod error; -pub mod merge; pub mod source; -pub use config::{ - Config, ConflictStrategy, InfoOverride, MergeConfig, OutputFormat, ServerEntry, Source, - TagEntry, TagPrefixStrategy, -}; pub use error::Error; -pub use merge::{merge_specs, merge_specs_with_report, MergeReport}; +pub use openapi_aggregator_core::{ + config, merge, merge_specs, merge_specs_with_report, parse_content, prepare_source, Config, + ConflictStrategy, InfoOverride, MergeConfig, MergeReport, OutputConfig, OutputFormat, + ServerEntry, Source, TagEntry, TagPrefixStrategy, +}; pub use source::load_source; use std::path::Path; use futures_util::future::try_join_all; +use openapi_aggregator_core::Error as CoreError; use source::{http_client, load_source_with_client}; /// Load all sources defined in `config` and merge them into a single OpenAPI spec. @@ -28,7 +27,7 @@ pub async fn aggregate(config: &Config) -> Result { /// Sources are loaded concurrently; the merge order still follows `config.sources`. pub async fn aggregate_with_report(config: &Config) -> Result { if config.sources.is_empty() { - return Err(Error::NoSources); + return Err(CoreError::NoSources.into()); } let client = http_client()?; @@ -38,15 +37,14 @@ pub async fn aggregate_with_report(config: &Config) -> Result Result { let content = std::fs::read_to_string(path) - .map_err(|e| Error::Config(format!("failed to read config file: {e}")))?; - let mut config: Config = serde_yaml::from_str(&content) - .map_err(|e| Error::Config(format!("failed to parse config file: {e}")))?; + .map_err(|e| CoreError::Config(format!("failed to read config file: {e}")))?; + let mut config = Config::from_yaml(&content)?; if let Some(config_dir) = path.parent() { for src in &mut config.sources { diff --git a/crates/openapi-aggregator/src/source.rs b/crates/openapi-aggregator/src/source.rs index cfcbee9..6fca942 100644 --- a/crates/openapi-aggregator/src/source.rs +++ b/crates/openapi-aggregator/src/source.rs @@ -1,11 +1,10 @@ use serde_json::Value; use std::time::Duration; -use crate::config::Source; +use openapi_aggregator_core::{prepare_source, Error as CoreError, Source}; + use crate::error::Error; -use crate::merge::deep_merge; -/// Load an OpenAPI spec from a [`Source`], returning `(name, parsed_value)`. pub async fn load_source(source: &Source) -> Result<(String, Value), Error> { load_source_with_client(source, &http_client()?).await } @@ -14,7 +13,11 @@ pub(crate) fn http_client() -> Result { reqwest::Client::builder() .timeout(Duration::from_secs(30)) .build() - .map_err(|e| Error::Config(format!("failed to create HTTP client: {e}"))) + .map_err(|e| { + Error::from(CoreError::Config(format!( + "failed to create HTTP client: {e}" + ))) + }) } pub(crate) async fn load_source_with_client( @@ -52,34 +55,7 @@ pub(crate) async fn load_source_with_client( } }; - let value = parse_and_prepare(&content, source)?; - validate_openapi(&value, &source.display_name())?; - Ok((source.display_name(), value)) -} - -fn parse_and_prepare(content: &str, source: &Source) -> Result { - let mut value = parse_content(content)?; - if let Some(blocks) = source_additional_blocks(source) { - if !blocks.is_object() { - return Err(Error::InvalidSpec { - name: source.display_name(), - reason: "'additional_blocks' must be a mapping/object".into(), - }); - } - deep_merge(&mut value, blocks); - } - Ok(value) -} - -fn source_additional_blocks(source: &Source) -> Option<&Value> { - match source { - Source::File { - additional_blocks, .. - } - | Source::Http { - additional_blocks, .. - } => additional_blocks.as_ref(), - } + Ok(prepare_source(&content, source)?) } /// Replace `${NAME}` placeholders with environment variables. @@ -95,8 +71,11 @@ fn expand_env_with(text: &str, lookup: impl Fn(&str) -> Option) -> Resul break; }; let name = &rest[start + 2..start + 2 + len]; - let value = lookup(name) - .ok_or_else(|| Error::Config(format!("environment variable '{name}' is not set")))?; + let value = lookup(name).ok_or_else(|| { + Error::from(CoreError::Config(format!( + "environment variable '{name}' is not set" + ))) + })?; out.push_str(&rest[..start]); out.push_str(&value); rest = &rest[start + 2 + len + 1..]; @@ -105,113 +84,6 @@ fn expand_env_with(text: &str, lookup: impl Fn(&str) -> Option) -> Resul Ok(out) } -/// Try JSON first, fall back to YAML. -fn parse_content(content: &str) -> Result { - serde_json::from_str(content).or_else(|_| { - serde_yaml::from_str::(content) - .map_err(|e| Error::Parse(format!("content is neither valid JSON nor YAML: {e}"))) - }) -} - -/// Minimal validation: the value must be an object with an `openapi` field. -fn validate_openapi(value: &Value, source_name: &str) -> Result<(), Error> { - match value.get("openapi").and_then(|v| v.as_str()) { - Some(v) if v.starts_with("3.") => Ok(()), - Some(v) => Err(Error::InvalidSpec { - name: source_name.into(), - reason: format!("unsupported OpenAPI version '{v}' (only 3.x is supported)"), - }), - None => Err(Error::InvalidSpec { - name: source_name.into(), - reason: "missing 'openapi' field".into(), - }), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - use std::path::PathBuf; - - #[test] - fn parse_json_content() { - let json = r#"{"openapi":"3.0.3","info":{"title":"T","version":"1"},"paths":{}}"#; - let v = parse_content(json).unwrap(); - assert_eq!(v["openapi"], "3.0.3"); - } - - #[test] - fn parse_yaml_content() { - let yaml = "openapi: '3.0.3'\ninfo:\n title: T\n version: '1'\npaths: {}"; - let v = parse_content(yaml).unwrap(); - assert_eq!(v["openapi"], "3.0.3"); - } - - #[test] - fn validate_rejects_missing_openapi() { - let v = serde_json::json!({"info": {}}); - assert!(validate_openapi(&v, "test").is_err()); - } - - #[test] - fn validate_rejects_v2() { - let v = serde_json::json!({"openapi": "2.0"}); - assert!(validate_openapi(&v, "test").is_err()); - } - - #[test] - fn additional_blocks_are_deep_merged() { - let source = Source::File { - name: Some("test".into()), - path: PathBuf::from("ignored.yaml"), - tag_prefix: None, - additional_blocks: Some(json!({ - "x-vendor-root": { "enabled": true }, - "paths": { - "/pets": { - "get": { - "x-vendor-extension": { "timeout": 3000 } - } - } - } - })), - }; - - let base = r#"{ - "openapi": "3.0.3", - "info": {"title": "T", "version": "1"}, - "paths": { - "/pets": { - "get": {"summary": "list pets"} - } - } - }"#; - - let merged = parse_and_prepare(base, &source).unwrap(); - assert_eq!(merged["x-vendor-root"]["enabled"], true); - assert_eq!(merged["paths"]["/pets"]["get"]["summary"], "list pets"); - assert_eq!( - merged["paths"]["/pets"]["get"]["x-vendor-extension"]["timeout"], - 3000 - ); - } - - #[test] - fn additional_blocks_must_be_object() { - let source = Source::File { - name: Some("test".into()), - path: PathBuf::from("ignored.yaml"), - tag_prefix: None, - additional_blocks: Some(json!([1, 2, 3])), - }; - - let base = r#"{"openapi":"3.0.3","info":{"title":"T","version":"1"},"paths":{}}"#; - let err = parse_and_prepare(base, &source).unwrap_err().to_string(); - assert!(err.contains("additional_blocks")); - } -} - #[cfg(test)] mod env_tests { use super::*; From 352f3a2213d4513291d7b1204663d80f8b1fadbf Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 11:54:42 +0200 Subject: [PATCH 05/15] feat(wasm): add wasm-bindgen wrapper crate --- .gitignore | 5 + Cargo.lock | 23 +++ crates/openapi-aggregator-wasm/Cargo.toml | 17 ++ crates/openapi-aggregator-wasm/src/lib.rs | 202 ++++++++++++++++++++++ 4 files changed, 247 insertions(+) create mode 100644 crates/openapi-aggregator-wasm/Cargo.toml create mode 100644 crates/openapi-aggregator-wasm/src/lib.rs diff --git a/.gitignore b/.gitignore index 83972fa..986edb5 100644 --- a/.gitignore +++ b/.gitignore @@ -216,3 +216,8 @@ __marimo__/ # Streamlit .streamlit/secrets.toml +web/node_modules/ +web/dist/ +web/src/wasm/ +web/test-results/ +web/playwright-report/ diff --git a/Cargo.lock b/Cargo.lock index 2a3cb49..e427981 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -729,6 +729,18 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "openapi-aggregator-wasm" +version = "0.6.0" +dependencies = [ + "openapi-aggregator-core", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_yaml", + "wasm-bindgen", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -1095,6 +1107,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_core" version = "1.0.229" diff --git a/crates/openapi-aggregator-wasm/Cargo.toml b/crates/openapi-aggregator-wasm/Cargo.toml new file mode 100644 index 0000000..7575c51 --- /dev/null +++ b/crates/openapi-aggregator-wasm/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "openapi-aggregator-wasm" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +openapi-aggregator-core.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_yaml.workspace = true +serde-wasm-bindgen = "0.6" +wasm-bindgen = "0.2" diff --git a/crates/openapi-aggregator-wasm/src/lib.rs b/crates/openapi-aggregator-wasm/src/lib.rs new file mode 100644 index 0000000..72608a3 --- /dev/null +++ b/crates/openapi-aggregator-wasm/src/lib.rs @@ -0,0 +1,202 @@ +//! Browser bindings for openapi-aggregator. Fetching happens in JS; +//! this crate only parses the config and merges already-loaded spec text. + +use std::collections::BTreeMap; + +use openapi_aggregator_core::{ + merge_specs_with_report, prepare_source, Config, Error, OutputFormat, Source, +}; +use serde::Serialize; +use wasm_bindgen::prelude::*; + +#[derive(Debug, Serialize, PartialEq)] +pub struct SourceSummary { + pub name: String, + pub kind: &'static str, + pub path: Option, + pub url: Option, + pub headers: BTreeMap, +} + +#[derive(Debug, Serialize, PartialEq)] +pub struct ConfigSummary { + pub sources: Vec, + pub format: &'static str, +} + +#[derive(Debug, Serialize, PartialEq)] +pub struct AggregateOutput { + pub text: String, + pub warnings: Vec, +} + +fn format_name(format: &OutputFormat) -> &'static str { + match format { + OutputFormat::Yaml => "yaml", + OutputFormat::Json => "json", + } +} + +pub fn summarize_config(yaml: &str) -> Result { + let config = Config::from_yaml(yaml).map_err(|e| e.to_string())?; + let sources = config + .sources + .iter() + .map(|source| match source { + Source::File { path, .. } => SourceSummary { + name: source.display_name(), + kind: "file", + path: Some(path.to_string_lossy().into_owned()), + url: None, + headers: BTreeMap::new(), + }, + Source::Http { url, headers, .. } => SourceSummary { + name: source.display_name(), + kind: "url", + path: None, + url: Some(url.clone()), + headers: headers + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + }, + }) + .collect(); + Ok(ConfigSummary { + sources, + format: format_name(&config.output.format), + }) +} + +pub fn aggregate_contents(yaml: &str, contents: Vec) -> Result { + let config = Config::from_yaml(yaml).map_err(|e| e.to_string())?; + if config.sources.is_empty() { + return Err(Error::NoSources.to_string()); + } + if contents.len() != config.sources.len() { + return Err(format!( + "expected {} source contents, got {}", + config.sources.len(), + contents.len() + )); + } + + let specs = config + .sources + .iter() + .zip(&contents) + .map(|(source, content)| { + let (name, spec) = prepare_source(content, source).map_err(|e| match e { + Error::Parse(_) => format!("source '{}': {e}", source.display_name()), + other => other.to_string(), + })?; + Ok((name, source.tag_prefix(), spec)) + }) + .collect::, String>>()?; + + let report = merge_specs_with_report(specs, &config.merge).map_err(|e| e.to_string())?; + let text = match config.output.format { + OutputFormat::Yaml => serde_yaml::to_string(&report.spec).map_err(|e| e.to_string())?, + OutputFormat::Json => { + serde_json::to_string_pretty(&report.spec).map_err(|e| e.to_string())? + "\n" + } + }; + Ok(AggregateOutput { + text, + warnings: report.warnings, + }) +} + +fn to_js(value: &T) -> Result { + value + .serialize(&serde_wasm_bindgen::Serializer::json_compatible()) + .map_err(|e| JsError::new(&e.to_string())) +} + +/// Parse and validate a config; returns a `ConfigSummary` or throws. +#[wasm_bindgen(js_name = parseConfig)] +pub fn parse_config(yaml: &str) -> Result { + to_js(&summarize_config(yaml).map_err(|e| JsError::new(&e))?) +} + +/// Merge already-loaded spec texts (`contents[i]` belongs to `sources[i]`); +/// returns an `AggregateOutput` or throws. +#[wasm_bindgen] +pub fn aggregate(yaml: &str, contents: Vec) -> Result { + to_js(&aggregate_contents(yaml, contents).map_err(|e| JsError::new(&e))?) +} + +#[cfg(test)] +mod tests { + use super::*; + + const PETS: &str = "openapi: 3.0.3\ninfo: {title: Pets, version: '1'}\npaths:\n /pets:\n get: {summary: list}\n"; + const USERS: &str = "openapi: 3.0.3\ninfo: {title: Users, version: '1'}\npaths:\n /users:\n get: {summary: list}\n"; + + #[test] + fn summarizes_sources_and_format() { + let summary = summarize_config( + "sources:\n - path: ./specs/pets.yaml\n - name: billing\n url: https://api.test/openapi.json\n headers: {Authorization: 'Bearer ${TOKEN}'}\noutput: {format: json}\n", + ) + .unwrap(); + + assert_eq!(summary.format, "json"); + assert_eq!( + summary.sources[0], + SourceSummary { + name: "pets".into(), + kind: "file", + path: Some("./specs/pets.yaml".into()), + url: None, + headers: BTreeMap::new(), + } + ); + assert_eq!(summary.sources[1].kind, "url"); + assert_eq!( + summary.sources[1].headers["Authorization"], + "Bearer ${TOKEN}" + ); + } + + #[test] + fn config_errors_are_reported() { + let err = summarize_config("sources: [{name: x}]").unwrap_err(); + assert!(err.contains("either 'path' or 'url'"), "{err}"); + } + + #[test] + fn aggregates_contents_in_source_order() { + let out = aggregate_contents( + "sources:\n - path: pets.yaml\n - path: users.yaml\n", + vec![PETS.into(), USERS.into()], + ) + .unwrap(); + assert!(out.text.starts_with("openapi: 3.0.3"), "{}", out.text); + assert!(out.text.contains("/pets:") && out.text.contains("/users:")); + assert!(out.warnings.is_empty()); + } + + #[test] + fn json_output_format_is_respected() { + let out = aggregate_contents( + "sources:\n - path: pets.yaml\noutput: {format: json}\n", + vec![PETS.into()], + ) + .unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&out.text).unwrap(); + assert_eq!(parsed["info"]["title"], "Pets"); + } + + #[test] + fn unparseable_content_names_the_source() { + let err = aggregate_contents("sources:\n - path: pets.yaml\n", vec!["{: nope".into()]) + .unwrap_err(); + assert!(err.contains("source 'pets'"), "{err}"); + } + + #[test] + fn content_count_must_match_sources() { + let err = aggregate_contents("sources:\n - path: pets.yaml\n", vec![]).unwrap_err(); + assert!(err.contains("expected 1"), "{err}"); + } +} From 22302cab4435e5a8b5b5e5a2538d07b3179fb0a5 Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 12:01:05 +0200 Subject: [PATCH 06/15] feat(web): scaffold playground with wasm engine wrapper --- web/index.html | 5 + web/package-lock.json | 5257 ++++++++++++++++++++++++++++++++++++++ web/package.json | 28 + web/src/engine.ts | 24 + web/src/main.ts | 4 + web/src/types.ts | 37 + web/tests/engine.test.ts | 27 + web/tests/helpers.ts | 11 + web/tsconfig.json | 13 + web/vite.config.ts | 7 + 10 files changed, 5413 insertions(+) create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/src/engine.ts create mode 100644 web/src/main.ts create mode 100644 web/src/types.ts create mode 100644 web/tests/engine.test.ts create mode 100644 web/tests/helpers.ts create mode 100644 web/tsconfig.json create mode 100644 web/vite.config.ts diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..143db6a --- /dev/null +++ b/web/index.html @@ -0,0 +1,5 @@ + + + OpenAPI Aggregator Playground + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..f19ca43 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,5257 @@ +{ + "name": "openapi-aggregator-playground", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openapi-aggregator-playground", + "dependencies": { + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/lint": "^6.9.7", + "@codemirror/state": "^6.7.6", + "@scalar/api-reference": "^1.72.3", + "codemirror": "^6.0.2" + }, + "devDependencies": { + "@playwright/test": "^1.63.0", + "@types/node": "^26.6.3", + "typescript": "^7.0.2", + "vite": "^8.3.1", + "vitest": "^5.0.3" + } + }, + "node_modules/@ai-sdk/gateway": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.13.tgz", + "integrity": "sha512-g7nE4PFtngOZNZSy1lOPpkC+FAiHxqBJXqyRMEG7NUrEVZlz5goBdtHg1YgWRJIX776JTXAmbOI5JreAKVAsVA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.2", + "@ai-sdk/provider-utils": "4.0.5", + "@vercel/oidc": "3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.2.tgz", + "integrity": "sha512-HrEmNt/BH/hkQ7zpi2o6N3k1ZR1QTb7z85WYhYygiTxOQuaml4CMtHCWRbric5WPU+RNsYI7r1EpyVQMKO1pYw==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.5.tgz", + "integrity": "sha512-Ow/X/SEkeExTTc1x+nYLB9ZHK2WUId8+9TlkamAx7Tl9vxU+cKzWx2dwjgMHeCN6twrgwkLrrtqckQeO4mxgVA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.2", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/vue": { + "version": "3.0.33", + "resolved": "https://registry.npmjs.org/@ai-sdk/vue/-/vue-3.0.33.tgz", + "integrity": "sha512-czM9Js3a7f+Eo35gjEYEeJYUoPvMg5Dfi4bOLyDBghLqn0gaVg8yTmTaSuHCg+3K/+1xPjyXd4+2XcQIohWWiQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "4.0.5", + "ai": "6.0.33", + "swrv": "^1.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "vue": "^3.3.4" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.1.tgz", + "integrity": "sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.12", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.12.tgz", + "integrity": "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz", + "integrity": "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", + "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", + "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.2.tgz", + "integrity": "sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.6.tgz", + "integrity": "sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.13", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.13.tgz", + "integrity": "sha512-sihaFrUzAsYBQsL9J2t69y8nfMQGwcYmggAZsk+kjPbjYZMyuf2hU8tUNTZ+P+isb6XRr8JE22TZlJxBoVdH1A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/core/node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom/node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@floating-ui/vue": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.9.tgz", + "integrity": "sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4", + "@floating-ui/utils": "^0.2.10", + "vue-demi": ">=0.13.0" + } + }, + "node_modules/@headlessui/tailwindcss": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.2.tgz", + "integrity": "sha512-xNe42KjdyA4kfUKLLPGzME9zkH7Q3rOZ5huFihWNWOQFxnItxPB3/67yBI8/qBfY8nwBRx5GHn4VprsoluVMGw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "tailwindcss": "^3.0 || ^4.0" + } + }, + "node_modules/@headlessui/vue": { + "version": "1.7.23", + "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz", + "integrity": "sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==", + "license": "MIT", + "dependencies": { + "@tanstack/vue-virtual": "^3.0.0-beta.60" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@internationalized/date": { + "version": "3.12.4", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.4.tgz", + "integrity": "sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.8", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.8.tgz", + "integrity": "sha512-8UmMFia46DUt+k97zKd9fKWXcWHR+k8ae3eYzILETuT2KbIvLyOfac7zesw+sJdRAAZ7Q9pM1Mk22aXp2LD0Ig==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.3.tgz", + "integrity": "sha512-H0iErY4e43LpXbYDyBci5W4v/RwTgGV3YzYOlJPiGZ8RY8w51kVE+xLn1tg3Z6UmSfyETvtsUK3r7YsgDQEI7Q==", + "license": "MIT" + }, + "node_modules/@lezer/css": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.8.tgz", + "integrity": "sha512-EJn1zcL9qoDptief6ipWKZKLiOpXkxSe0+t8CH9oiMVcZlq7NBWrjCqnc/41EIjeo/ITj1gFFiATdTkaJDL+Og==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.5.tgz", + "integrity": "sha512-O1GMVKgtf5YspFaRzpmqkVgMtIk2HG9uMAbH6wdAIdNh3TVzCsch/+cRi+6l2//UGDKe6NB9BW3cvuR5ezMQ0w==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.6.tgz", + "integrity": "sha512-sEvOqpl4vJQca0NugSQK02vQPbc5KrvoojnYGoqqFgFO0MtcYhwGiHKVEB+LuXYJnfyXo4QPA8ztbrEa4Fw5NA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.4.tgz", + "integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==", + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@phosphor-icons/core": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@phosphor-icons/core/-/core-2.1.1.tgz", + "integrity": "sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ==", + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@replit/codemirror-css-color-picker": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@replit/codemirror-css-color-picker/-/codemirror-css-color-picker-6.3.0.tgz", + "integrity": "sha512-19biDANghUm7Fz7L1SNMIhK48tagaWuCOHj4oPPxc7hxPGkTVY2lU/jVZ8tsbTKQPVG7BO2CBDzs7CBwb20t4A==", + "license": "MIT", + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@scalar/agent-chat": { + "version": "0.12.39", + "resolved": "https://registry.npmjs.org/@scalar/agent-chat/-/agent-chat-0.12.39.tgz", + "integrity": "sha512-BXvqGp7HM5bPOt0z4Vco+v6SPz26fOkE9soooScbgvUCXd3+zL7GjHVSPSB+M4Oac5mKAHRs0tjpg9Kt6De4cw==", + "license": "MIT", + "dependencies": { + "@ai-sdk/vue": "3.0.33", + "@scalar/api-client": "3.21.3", + "@scalar/components": "0.30.5", + "@scalar/helpers": "0.16.0", + "@scalar/icons": "0.7.6", + "@scalar/json-magic": "0.15.3", + "@scalar/openapi-types": "0.9.7", + "@scalar/schemas": "0.12.2", + "@scalar/themes": "0.18.1", + "@scalar/types": "0.22.3", + "@scalar/use-toasts": "0.10.5", + "@scalar/validation": "0.6.5", + "@scalar/workspace-store": "0.67.2", + "@vueuse/core": "13.9.0", + "ai": "6.0.33", + "js-base64": "^3.9.2", + "neverpanic": "0.0.8", + "truncate-json": "3.0.1", + "vue": "^3.5.40" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/api-client": { + "version": "3.21.3", + "resolved": "https://registry.npmjs.org/@scalar/api-client/-/api-client-3.21.3.tgz", + "integrity": "sha512-C9DYdcD7kWugtVR6BOJk7LR9N9WQqPHB5MIt+Ygu0IHyD9+5TsJ4L4i8XVJvLu2T9k59DMPiG1CjLGdRNxM/+g==", + "license": "MIT", + "dependencies": { + "@headlessui/tailwindcss": "^0.2.2", + "@headlessui/vue": "1.7.23", + "@scalar/blocks": "0.4.2", + "@scalar/components": "0.30.5", + "@scalar/helpers": "0.16.0", + "@scalar/icons": "0.7.6", + "@scalar/localization": "0.2.4", + "@scalar/oas-utils": "0.20.8", + "@scalar/openapi-types": "0.9.7", + "@scalar/sidebar": "0.11.10", + "@scalar/snippetz": "0.10.4", + "@scalar/themes": "0.18.1", + "@scalar/typebox": "^0.1.3", + "@scalar/types": "0.22.3", + "@scalar/use-codemirror": "0.14.15", + "@scalar/use-hooks": "0.4.16", + "@scalar/use-toasts": "0.10.5", + "@scalar/workspace-store": "0.67.2", + "@vueuse/core": "13.9.0", + "@vueuse/integrations": "13.9.0", + "focus-trap": "^7.8.0", + "fuse.js": "^7.5.0", + "js-base64": "^3.9.2", + "jsonc-parser": "3.3.1", + "nanoid": "^5.1.6", + "pretty-ms": "^9.3.0", + "radix-vue": "^1.9.17", + "set-cookie-parser": "3.1.0", + "vue": "^3.5.40", + "yaml": "^2.9.0", + "zod": "^4.4.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/api-client/node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@scalar/api-reference": { + "version": "1.72.3", + "resolved": "https://registry.npmjs.org/@scalar/api-reference/-/api-reference-1.72.3.tgz", + "integrity": "sha512-RQjAUSrtwZ7ASG6KlKwRohEdvK0GfIvPmLyhX3xHD9cY3tmsilJcrcqBSJRKKG6FeiwEz23C1Q+pOdx05LT6pw==", + "license": "MIT", + "dependencies": { + "@headlessui/vue": "1.7.23", + "@scalar/agent-chat": "0.12.39", + "@scalar/api-client": "3.21.3", + "@scalar/blocks": "0.4.2", + "@scalar/code-highlight": "0.4.7", + "@scalar/components": "0.30.5", + "@scalar/helpers": "0.16.0", + "@scalar/icons": "0.7.6", + "@scalar/localization": "0.2.4", + "@scalar/oas-utils": "0.20.8", + "@scalar/openapi-to-markdown": "1.5.0", + "@scalar/schemas": "0.12.2", + "@scalar/sidebar": "0.11.10", + "@scalar/snippetz": "0.10.4", + "@scalar/themes": "0.18.1", + "@scalar/types": "0.22.3", + "@scalar/use-hooks": "0.4.16", + "@scalar/use-toasts": "0.10.5", + "@scalar/validation": "0.6.5", + "@scalar/workspace-store": "0.67.2", + "@unhead/vue": "^2.1.4", + "@vueuse/core": "13.9.0", + "fuse.js": "^7.5.0", + "microdiff": "^1.5.0", + "nanoid": "^5.1.6", + "vue": "^3.5.40", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/api-reference/node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@scalar/asyncapi-upgrader": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/@scalar/asyncapi-upgrader/-/asyncapi-upgrader-0.1.14.tgz", + "integrity": "sha512-4mYgLim3EvRgJabyC5OzxAJrqyX5sjBhJIlV4962HJrGnQ95Xb3MJTxvuv38dSJCLG7B1TzXmH1QhAjkGKPCug==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/blocks": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@scalar/blocks/-/blocks-0.4.2.tgz", + "integrity": "sha512-5m+2uuUdrMimQMnfz3T5179AQkrUXIzVTysImd1rR6N2X70PNAYv504R7N3JYhV0p3pi9sIb13Dzl3kmUEsNAQ==", + "license": "MIT", + "dependencies": { + "@scalar/components": "0.30.5", + "@scalar/helpers": "0.16.0", + "@scalar/icons": "0.7.6", + "@scalar/snippetz": "0.10.4", + "@scalar/themes": "0.18.1", + "@scalar/types": "0.22.3", + "@scalar/workspace-store": "0.67.2", + "@types/har-format": "^1.2.16", + "js-base64": "^3.9.2", + "vue": "^3.5.40" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/code-highlight": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@scalar/code-highlight/-/code-highlight-0.4.7.tgz", + "integrity": "sha512-21K+/gnbUzhBlFpoxP2Qv1uo0OCNcyb0XkIHDx2JybnpH6HPQrnkKGBDQCcNDwChzba5CUv1DSu1nLTZ7QFx7Q==", + "license": "MIT", + "dependencies": { + "hast-util-to-text": "^4.0.2", + "highlight.js": "^11.11.1", + "lowlight": "^3.3.0", + "rehype-external-links": "^3.0.0", + "rehype-format": "^5.0.1", + "rehype-parse": "^9.0.1", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/components": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/@scalar/components/-/components-0.30.5.tgz", + "integrity": "sha512-HvR+a/+7P0NuvQg/V3Z6gWRmJ4q7HBANpCGUnyrtCPno6sEZU/Yuq6Akq22AMtSU2EfRcWZq5AHmdr2GzcLhCg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "0.2.10", + "@floating-ui/vue": "1.1.9", + "@headlessui/tailwindcss": "^0.2.2", + "@headlessui/vue": "1.7.23", + "@scalar/code-highlight": "0.4.7", + "@scalar/helpers": "0.16.0", + "@scalar/icons": "0.7.6", + "@scalar/themes": "0.18.1", + "@scalar/use-hooks": "0.4.16", + "@vueuse/core": "13.9.0", + "cva": "1.0.0-beta.4", + "radix-vue": "^1.9.17", + "vue": "^3.5.40", + "vue-component-type-helpers": "^3.2.6" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/helpers": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.16.0.tgz", + "integrity": "sha512-lCBNB3hN8sGfUghz5AK5r63G+1/x4PHSgjH9asJVWne5e1tnKkti5eaejPLgy8didpAZsyXEp744xIosoWu9AA==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/icons": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/@scalar/icons/-/icons-0.7.6.tgz", + "integrity": "sha512-UIQ7K/xNDo7Mgez/Z+ArG7JZnS8QRZNOl+s9frdS1T8uEJYuTqT1lnS7R0ECwIw3y6dn6EIfjaZKPihjYC7+qw==", + "license": "MIT", + "dependencies": { + "@phosphor-icons/core": "^2.1.1", + "@types/node": "^24.1.0", + "chalk": "^5.6.2", + "vue": "^3.5.40" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/icons/node_modules/@types/node": { + "version": "24.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz", + "integrity": "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@scalar/icons/node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/@scalar/json-magic": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@scalar/json-magic/-/json-magic-0.15.3.tgz", + "integrity": "sha512-ASw78byO+kVlRHTSHnUuFvzaLdNGvFEvF50hCJiqXRDHoGajZbakAbZRz6rNO7Twt7Nip4CneFHSXjdrOwfXHQ==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.0", + "pathe": "^2.0.3", + "undici": "7.29.0", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/localization": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@scalar/localization/-/localization-0.2.4.tgz", + "integrity": "sha512-EZIhatmvOY5YCpM1irclMs1KvQZa12zwZliVLJKTokhmug5i6IBD00lIdvThV94y198ITFmZA/C2GinqpW3XXw==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.0", + "type-fest": "^5.8.0", + "vue": "^3.5.40" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/oas-utils": { + "version": "0.20.8", + "resolved": "https://registry.npmjs.org/@scalar/oas-utils/-/oas-utils-0.20.8.tgz", + "integrity": "sha512-SBG6tJneoqgvsDmdPAr9fZj0yZJf1+kC26scUoyEE4hiCYT6vIg2zDK/1jN+95mPLUqaYS1IzeHZ+QkwkWS1Sw==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.0", + "@scalar/themes": "0.18.1", + "@scalar/types": "0.22.3", + "@scalar/workspace-store": "0.67.2", + "flatted": "^3.4.0", + "vue": "^3.5.40", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@scalar/openapi-to-markdown/-/openapi-to-markdown-1.5.0.tgz", + "integrity": "sha512-QWzykCLD1TPcq/94SW6QczRF0B5uTRlGXlN05uwTTv/j0jdG5hdiwEngU0//09ZjIE7YPQ2mpv7UTv+V11AVqQ==", + "license": "MIT", + "dependencies": { + "@scalar/code-highlight": "0.4.7", + "@scalar/helpers": "0.16.0", + "@scalar/json-magic": "0.15.3", + "@scalar/openapi-upgrader": "0.4.1", + "@scalar/workspace-store": "0.67.2", + "rehype-parse": "^9.0.1", + "rehype-remark": "^10.0.1", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-types": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.9.7.tgz", + "integrity": "sha512-UVSgxRxcTaIbTZGtNRdQJHZC60uyYi29zfDqQVp0lxx07BNczIHbf9jycgI/+W3Uxxod+yUUt7hzxnKllOHE0w==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-upgrader": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@scalar/openapi-upgrader/-/openapi-upgrader-0.4.1.tgz", + "integrity": "sha512-TbkgBG0speuaejyBcFtmeexowXVq6YCNZw7vUmKCBtuKDUCbO+YTMKDfPsvnEoZt96CVJtJlexX4HEW6IZDZuw==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.0", + "@scalar/openapi-types": "0.9.7" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/schemas": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@scalar/schemas/-/schemas-0.12.2.tgz", + "integrity": "sha512-LIqqT0G7Chj57h1XY1FC3mOJriqNvPOg41V8NOsWGP/VCHCL/lg6K+D3ejwUg4mYNR3TTYaQARlr630mzqKgDA==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.0", + "@scalar/validation": "0.6.5" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/sidebar": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@scalar/sidebar/-/sidebar-0.11.10.tgz", + "integrity": "sha512-aSz7ZWn0BKItipGb9BhYn/UiFIqO6Ux9OxH2o/swb8PVD3sZIWjqXrbyGfXhfo6WoZGZcNaGgmYxMH4YkAW8mw==", + "license": "MIT", + "dependencies": { + "@scalar/components": "0.30.5", + "@scalar/helpers": "0.16.0", + "@scalar/icons": "0.7.6", + "@scalar/themes": "0.18.1", + "@scalar/use-hooks": "0.4.16", + "@scalar/workspace-store": "0.67.2", + "vue": "^3.5.40" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/snippetz": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@scalar/snippetz/-/snippetz-0.10.4.tgz", + "integrity": "sha512-qtrkEysWrNRmdgr97vNi+vzJ/mfhT7OlpmnI34CNUf/dDiyKXJr2TMIr+taGUduEhOj+C2Ih4wWLcA/gGz2Vfg==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.0", + "@scalar/types": "0.22.3", + "js-base64": "^3.9.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/themes": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@scalar/themes/-/themes-0.18.1.tgz", + "integrity": "sha512-vPRNGjKFfKmHw3tSB5hQU63AbgVwG8SChGao65UAduuQ2Ap0jtIysZfNl4VJ2RfjX5/gglSLi/o2X1vyGyaprw==", + "license": "MIT", + "dependencies": { + "nanoid": "^5.1.6" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/themes/node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@scalar/typebox": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@scalar/typebox/-/typebox-0.1.3.tgz", + "integrity": "sha512-lU055AUccECZMIfGA0z/C1StYmboAYIPJLDFBzOO81yXBi35Pxdq+I4fWX6iUZ8qcoHneiLGk9jAUM1rA93iEg==", + "license": "MIT" + }, + "node_modules/@scalar/types": { + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.22.3.tgz", + "integrity": "sha512-Ai+NPBJPhuvqkR5EXsJPK7RAnP7SBhSOyxEhuGqShgUutHoMQ7Vl7tvzVvhuqbXvtAxdBUEhUe57B6kI8Xg+dg==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.0", + "nanoid": "^5.1.6", + "type-fest": "^5.8.0", + "zod": "^4.4.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/types/node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@scalar/use-codemirror": { + "version": "0.14.15", + "resolved": "https://registry.npmjs.org/@scalar/use-codemirror/-/use-codemirror-0.14.15.tgz", + "integrity": "sha512-Uvjx0qvOsWPs+I32FL+3v//+VKwtp8ROXgbIAMauNypuw52VeF7R4RM7YoN2Xg1EAEiMtM8ZhWVzCoYTFr6AKQ==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.18.3", + "@codemirror/commands": "^6.7.1", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.8", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.10.7", + "@codemirror/lint": "^6.8.4", + "@codemirror/state": "^6.5.0", + "@codemirror/view": "^6.35.3", + "@lezer/common": "^1.2.3", + "@lezer/highlight": "^1.2.1", + "@replit/codemirror-css-color-picker": "^6.3.0", + "vue": "^3.5.40" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/use-hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@scalar/use-hooks/-/use-hooks-0.4.16.tgz", + "integrity": "sha512-bm8b21qcg8D3qUpwD88vRrx7LLFPDFgE++QF9nOs97IMxGVXg+eq3a4obMTI+SLokYheJ4Ja84UmDy9UhHc5aw==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.0", + "@scalar/use-toasts": "0.10.5", + "@scalar/validation": "0.6.5", + "@vueuse/core": "13.9.0", + "cva": "1.0.0-beta.4", + "tailwind-merge": "3.5.0", + "vue": "^3.5.40" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/use-toasts": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@scalar/use-toasts/-/use-toasts-0.10.5.tgz", + "integrity": "sha512-5a8qTVd9eXvMDeD0ifqHvnzg0i2v15clhyFToXDHYmInQD51oBRwByCFqFO+m9ymxSLOHZ7/FiEcU6F90/QTlg==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.40", + "vue-sonner": "^1.3.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/validation": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@scalar/validation/-/validation-0.6.5.tgz", + "integrity": "sha512-t/dtbqesbOLsTusFI2kXfiFEQOiZC7+oAY+jZnE82Y64xLamo/1KrZpta8GkuAfIHyeOHOx5bDFid04eO13MIg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@scalar/workspace-store": { + "version": "0.67.2", + "resolved": "https://registry.npmjs.org/@scalar/workspace-store/-/workspace-store-0.67.2.tgz", + "integrity": "sha512-RfOGwKS/AYWDjr/lxBrCEVN+WrL14JA/cmU9DmfzMRnIQxuVggjWjKScQ79icyEbj4VizFXTP3o7i+kZnZJiww==", + "license": "MIT", + "dependencies": { + "@scalar/asyncapi-upgrader": "0.1.14", + "@scalar/helpers": "0.16.0", + "@scalar/json-magic": "0.15.3", + "@scalar/openapi-upgrader": "0.4.1", + "@scalar/schemas": "0.12.2", + "@scalar/snippetz": "0.10.4", + "@scalar/typebox": "0.1.3", + "@scalar/types": "0.22.3", + "@scalar/validation": "0.6.5", + "js-base64": "^3.9.2", + "type-fest": "^5.8.0", + "vue": "^3.5.40", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.17.11", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.11.tgz", + "integrity": "sha512-+ILjvtHup6Y2hzQ6YzwMgX1Q+oQpxEGOXCEsCNaPoIP0VxMbizIBTmYTDtkerkIQS8/CbP1BRuyt8V/8BCsy1g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/vue-virtual": { + "version": "3.13.39", + "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.39.tgz", + "integrity": "sha512-AG8sI+6nfrDvN6ixuHJL9LaJTf3//0V5VITZYW3UOaIuSVexnEzsbMu4LH/JhMfRTxwhUXPnLqkl1DlzkZbRtg==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.17.11" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.0.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", + "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, + "node_modules/@unhead/vue": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.17.tgz", + "integrity": "sha512-pnC8x9HLV3qQXdvWfylUEU25uhfCAy3ly9nmpQz84j9py818DRfU8jOsQ5wjdWtxyU1vX/fW2udfm4jtxUK8Bg==", + "license": "MIT", + "dependencies": { + "hookable": "^6.0.1", + "unhead": "2.1.17" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + }, + "peerDependencies": { + "vue": ">=3.5.18" + } + }, + "node_modules/@vercel/oidc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz", + "integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz", + "integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.3", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz", + "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.43.tgz", + "integrity": "sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.43", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.43.tgz", + "integrity": "sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.43.tgz", + "integrity": "sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.43", + "@vue/compiler-dom": "3.5.43", + "@vue/compiler-ssr": "3.5.43", + "@vue/shared": "3.5.43", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.28", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-sfc/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.43.tgz", + "integrity": "sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.43.tgz", + "integrity": "sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.43.tgz", + "integrity": "sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.43.tgz", + "integrity": "sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.43", + "@vue/runtime-core": "3.5.43", + "@vue/shared": "3.5.43", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.43.tgz", + "integrity": "sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.43", + "@vue/runtime-dom": "3.5.43", + "@vue/shared": "3.5.43" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.43.tgz", + "integrity": "sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz", + "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "13.9.0", + "@vueuse/shared": "13.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/integrations": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-13.9.0.tgz", + "integrity": "sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "13.9.0", + "@vueuse/shared": "13.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.9.0.tgz", + "integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.9.0.tgz", + "integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/ai": { + "version": "6.0.33", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.33.tgz", + "integrity": "sha512-bVokbmy2E2QF6Efl+5hOJx5MRWoacZ/CZY/y1E+VcewknvGlgaiCzMu8Xgddz6ArFJjiMFNUPHKxAhIePE4rmg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "3.0.13", + "@ai-sdk/provider": "3.0.2", + "@ai-sdk/provider-utils": "4.0.5", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cva": { + "version": "1.0.0-beta.4", + "resolved": "https://registry.npmjs.org/cva/-/cva-1.0.0-beta.4.tgz", + "integrity": "sha512-F/JS9hScapq4DBVQXcK85l9U91M6ePeXoBMSp7vypzShoefUBxjQTo3g3935PUHgQd+IW77DjbPRIxugy4/GCQ==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + }, + "peerDependencies": { + "typescript": ">= 4.5.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "license": "ISC" + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fuse.js": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.5.0.tgz", + "integrity": "sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/krisk" + } + }, + "node_modules/guess-json-indent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/guess-json-indent/-/guess-json-indent-3.0.1.tgz", + "integrity": "sha512-LWZ3Vr8BG7DHE3TzPYFqkhjNRw4vYgFSsv2nfMuHklAlOfiy54/EwiDQuQfFVLxENCVv20wpbjfTayooQHrEhQ==", + "license": "MIT", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-format": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz", + "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", + "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-minify-whitespace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", + "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-sanitize": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", + "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "unist-util-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-mdast": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-10.1.2.tgz", + "integrity": "sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "hast-util-to-text": "^4.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-minify-whitespace": "^6.0.0", + "trim-trailing-lines": "^2.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.12.0.tgz", + "integrity": "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hookable": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.2.tgz", + "integrity": "sha512-+abwxtiEA52GCVIsQqut3S/uKTbUwYIp4Pe/vv+6py5XiXBCqMZHg6pA6Y5qhgLSEys0/cYuPbO0z1QFj5ZCmg==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-whitespace-sensitive-tag-names": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz", + "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-base64": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.9.4.tgz", + "integrity": "sha512-PtOMXpEGuP0RRiRXsjzHzl44dMHxSu2CPvAhinupR1tBa88me+1DPqsobl7eupn5UukjLkln1ZnAOAOXOrYG0Q==", + "license": "BSD-3-Clause" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lowlight/node_modules/highlight.js": { + "version": "11.11.2", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.2.tgz", + "integrity": "sha512-oaXMACAU0kzOMXBjWpNcX+vlwSBCIAiZ9BHa7gA15NOTtT2L/l8OSZDuqS2XppOhZBPJ7hm4o8ep2kyuip2uEQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.1.tgz", + "integrity": "sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.3.tgz", + "integrity": "sha512-wgyJtgUkUcdU7zci7uuwc/tzoAhm0TswWhaXuvnWolqug+8jY7bgNkBBL0dYBnHgc/tHL7lCzRdPsdUxUKKBqw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/microdiff": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/microdiff/-/microdiff-1.6.0.tgz", + "integrity": "sha512-w7JWt8Bno6I8h0rEqlxr4lNG4UbT1FVtWo42wWosIiaJ+rP3pXh7shCYDnyqBrYx36LJ1CZ64p8A62aVp0sJpQ==", + "license": "MIT" + }, + "node_modules/micromark": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", + "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", + "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.1.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.3" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-edit-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", + "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neverpanic": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/neverpanic/-/neverpanic-0.0.8.tgz", + "integrity": "sha512-vVdkelrLxaow/fdWDumzNBO+jwm6X8bxeLJc34THtpj70u0C5QBkcV6CRCu2X726km7XD45N0A3QtYCla4RvKw==", + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz", + "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix-vue": { + "version": "1.9.17", + "resolved": "https://registry.npmjs.org/radix-vue/-/radix-vue-1.9.17.tgz", + "integrity": "sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.6.7", + "@floating-ui/vue": "^1.1.0", + "@internationalized/date": "^3.5.4", + "@internationalized/number": "^3.5.3", + "@tanstack/vue-virtual": "^3.8.1", + "@vueuse/core": "^10.11.0", + "@vueuse/shared": "^10.11.0", + "aria-hidden": "^1.2.4", + "defu": "^6.1.4", + "fast-deep-equal": "^3.1.3", + "nanoid": "^5.0.7" + }, + "peerDependencies": { + "vue": ">= 3.2.0" + } + }, + "node_modules/radix-vue/node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/radix-vue/node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/radix-vue/node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/radix-vue/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/radix-vue/node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/rehype-external-links": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-is-element": "^3.0.0", + "is-absolute-url": "^4.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-format": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz", + "integrity": "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-format": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-minify-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-6.0.2.tgz", + "integrity": "sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-remark": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-10.0.1.tgz", + "integrity": "sha512-EmDndlb5NVwXGfUa4c9GPK+lXeItTilLhE6ADSaQuHr4JUlKw9MidzGzx4HpqZrNCt6vnHmEifXQiiA+CEnjYQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "hast-util-to-mdast": "^10.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-sanitize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", + "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-sanitize": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-byte-length": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-byte-length/-/string-byte-length-3.0.1.tgz", + "integrity": "sha512-yJ8vP0HMwZ54CcA8S8mKoXbkezpZHANFtmafFo8lGxZThCQcAwRHjdFabuSLgOzxj9OFJcmssmiAvmcOK4O2Hw==", + "license": "MIT", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/string-byte-slice": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-byte-slice/-/string-byte-slice-3.0.1.tgz", + "integrity": "sha512-GWv2K4lYyd2+AhmKH3BV+OVx62xDX+99rSLfKpaqFiQU7uOMaUY1tDjdrRD4gsrCr9lTyjMgjna7tZcCOw+Smg==", + "license": "MIT", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-mod": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.4.tgz", + "integrity": "sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==", + "license": "MIT" + }, + "node_modules/swrv": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/swrv/-/swrv-1.3.0.tgz", + "integrity": "sha512-6uyQ79QemgpJzdwVNkxZdypTmKC80uHdjSZGAGo0HnizL2AzQJUZXQFR3KZq8yru32IC7HngiBA7o2FtL/Lozw==", + "license": "Apache-2.0", + "peerDependencies": { + "@vue/test-utils": ">=2.0.0", + "vue": "^3.2.0" + }, + "peerDependenciesMeta": { + "@vue/test-utils": { + "optional": true + } + } + }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "license": "MIT" + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT", + "peer": true + }, + "node_modules/tinybench": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", + "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trim-trailing-lines": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-2.1.0.tgz", + "integrity": "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/truncate-json": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/truncate-json/-/truncate-json-3.0.1.tgz", + "integrity": "sha512-QVsbr1WhGLq2F0oDyYbqtOXcf3gcnL8C9H5EX8bBwAr8ZWvWGJzukpPrDrWgJMrNtgDbo74BIjI4kJu3q2xQWw==", + "license": "MIT", + "dependencies": { + "guess-json-indent": "^3.0.1", + "string-byte-length": "^3.0.1", + "string-byte-slice": "^3.0.1" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.10.0.tgz", + "integrity": "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unhead": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.17.tgz", + "integrity": "sha512-HLMKXOszRhAPBrr6VlqCeVeJq2kbC4kXwzGLEZvvojPLWNYTJw22xG7Bfwhsvs31+IBet3Wl8ADg9dwYdyphfQ==", + "license": "MIT", + "dependencies": { + "hookable": "^6.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", + "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.9", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "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 + } + } + }, + "node_modules/vitest": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz", + "integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.3", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.3", + "@vitest/browser-preview": "5.0.3", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.3", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.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 + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vue": { + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.43.tgz", + "integrity": "sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.43", + "@vue/compiler-sfc": "3.5.43", + "@vue/runtime-dom": "3.5.43", + "@vue/server-renderer": "3.5.43", + "@vue/shared": "3.5.43" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz", + "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==", + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-sonner": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/vue-sonner/-/vue-sonner-1.3.2.tgz", + "integrity": "sha512-UbZ48E9VIya3ToiRHAZUbodKute/z/M1iT8/3fU8zEbwBRE11AKuHikssv18LMk2gTTr6eMQT4qf6JoLHWuj/A==", + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..a3005cf --- /dev/null +++ b/web/package.json @@ -0,0 +1,28 @@ +{ + "name": "openapi-aggregator-playground", + "private": true, + "type": "module", + "scripts": { + "wasm": "wasm-pack build ../crates/openapi-aggregator-wasm --target web --out-dir ../../web/src/wasm --out-name openapi_aggregator_wasm", + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "e2e": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.63.0", + "@types/node": "^26.6.3", + "typescript": "^7.0.2", + "vite": "^8.3.1", + "vitest": "^5.0.3" + }, + "dependencies": { + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/lint": "^6.9.7", + "@codemirror/state": "^6.7.6", + "@scalar/api-reference": "^1.72.3", + "codemirror": "^6.0.2" + } +} diff --git a/web/src/engine.ts b/web/src/engine.ts new file mode 100644 index 0000000..6f5cb6f --- /dev/null +++ b/web/src/engine.ts @@ -0,0 +1,24 @@ +import init, { + aggregate as wasmAggregate, + initSync, + parseConfig as wasmParseConfig, +} from './wasm/openapi_aggregator_wasm.js'; +import type { AggregateOutput, ConfigSummary } from './types'; + +export async function initEngine(): Promise { + await init(); +} + +export function initEngineSync(bytes: BufferSource): void { + initSync({ module: bytes }); +} + +/** Parse and validate `openapi-aggregator.yaml`. Throws `Error` with the config error. */ +export function parseConfig(yaml: string): ConfigSummary { + return wasmParseConfig(yaml) as ConfigSummary; +} + +/** Merge spec texts; `contents[i]` belongs to `parseConfig(yaml).sources[i]`. Throws on failure. */ +export function aggregate(yaml: string, contents: string[]): AggregateOutput { + return wasmAggregate(yaml, contents) as AggregateOutput; +} diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 0000000..aa2c48b --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,4 @@ +import { initEngine } from './engine'; + +await initEngine(); +document.body.textContent = 'engine ready'; diff --git a/web/src/types.ts b/web/src/types.ts new file mode 100644 index 0000000..fde3ab3 --- /dev/null +++ b/web/src/types.ts @@ -0,0 +1,37 @@ +export interface WorkspaceFile { + name: string; + content: string; +} + +export interface Settings { + proxy: string; + variables: Record; +} + +export interface SourceSummary { + name: string; + kind: 'file' | 'url'; + path: string | null; + url: string | null; + headers: Record; +} + +export interface ConfigSummary { + sources: SourceSummary[]; + format: 'yaml' | 'json'; +} + +export interface AggregateOutput { + text: string; + warnings: string[]; +} + +export type ProblemKind = 'config' | 'missing-file' | 'variable' | 'cors' | 'http' | 'merge'; + +export interface Problem { + level: 'error' | 'warning'; + kind: ProblemKind; + message: string; + source?: string; + line?: number; +} diff --git a/web/tests/engine.test.ts b/web/tests/engine.test.ts new file mode 100644 index 0000000..bf13a02 --- /dev/null +++ b/web/tests/engine.test.ts @@ -0,0 +1,27 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { aggregate, parseConfig } from '../src/engine'; +import { loadEngineForTests } from './helpers'; + +const PETS = "openapi: 3.0.3\ninfo: {title: Pets, version: '1'}\npaths:\n /pets:\n get: {summary: list}\n"; + +beforeAll(loadEngineForTests); + +describe('engine', () => { + it('summarizes the config as plain objects', () => { + const summary = parseConfig('sources:\n - path: ./pets.yaml\n'); + expect(summary).toEqual({ + sources: [{ name: 'pets', kind: 'file', path: './pets.yaml', url: null, headers: {} }], + format: 'yaml', + }); + }); + + it('throws an Error for an invalid config', () => { + expect(() => parseConfig('sources: [{name: x}]')).toThrowError(/either 'path' or 'url'/); + }); + + it('aggregates loaded contents', () => { + const out = aggregate('sources:\n - path: pets.yaml\n', [PETS]); + expect(out.text).toContain('/pets:'); + expect(out.warnings).toEqual([]); + }); +}); diff --git a/web/tests/helpers.ts b/web/tests/helpers.ts new file mode 100644 index 0000000..b63a58c --- /dev/null +++ b/web/tests/helpers.ts @@ -0,0 +1,11 @@ +import { readFileSync } from 'node:fs'; +import { initEngineSync } from '../src/engine'; + +let loaded = false; + +/** Load the real wasm build (run `npm run wasm` first). */ +export function loadEngineForTests(): void { + if (loaded) return; + initEngineSync(readFileSync(new URL('../src/wasm/openapi_aggregator_wasm_bg.wasm', import.meta.url))); + loaded = true; +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..a1c3e73 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["vite/client", "node"] + }, + "include": ["src", "tests", "e2e", "vite.config.ts", "playwright.config.ts"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..df9f0fb --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + base: '/openapi-aggregator/', + build: { target: 'es2022' }, + test: { include: ['tests/**/*.test.ts'], environment: 'node' }, +}); From b015ebb1a0237e3d2f0f5a60a88b2dff24c0dd60 Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 12:01:51 +0200 Subject: [PATCH 07/15] feat(web): resolve workspace and URL sources with CORS-aware errors --- web/src/resolve.ts | 97 +++++++++++++++++++++++++++++++++++++ web/src/settings.ts | 49 +++++++++++++++++++ web/src/workspace.ts | 27 +++++++++++ web/tests/resolve.test.ts | 92 +++++++++++++++++++++++++++++++++++ web/tests/settings.test.ts | 42 ++++++++++++++++ web/tests/workspace.test.ts | 35 +++++++++++++ 6 files changed, 342 insertions(+) create mode 100644 web/src/resolve.ts create mode 100644 web/src/settings.ts create mode 100644 web/src/workspace.ts create mode 100644 web/tests/resolve.test.ts create mode 100644 web/tests/settings.test.ts create mode 100644 web/tests/workspace.test.ts diff --git a/web/src/resolve.ts b/web/src/resolve.ts new file mode 100644 index 0000000..48f31df --- /dev/null +++ b/web/src/resolve.ts @@ -0,0 +1,97 @@ +import { workspaceKey } from './workspace'; +import type { Problem, ProblemKind, Settings, SourceSummary, WorkspaceFile } from './types'; + +export type Loader = (url: string, headers: Record) => Promise; +export type FetchFn = (input: string, init?: RequestInit) => Promise; + +export class SourceError extends Error { + constructor( + readonly kind: ProblemKind, + message: string, + ) { + super(message); + } +} + +export function errorMessage(e: unknown): string { + return e instanceof Error ? e.message : String(e); +} + +export function substituteVars(text: string, vars: Record): string { + return text.replace(/\$\{([^}]*)\}/g, (_, name: string) => { + if (!Object.hasOwn(vars, name)) { + throw new SourceError('variable', `Variable '${name}' is not set. Add it under Settings → Variables.`); + } + return vars[name]; + }); +} + +export async function fetchText( + url: string, + headers: Record, + settings: Settings, + fetchFn: FetchFn = (input, init) => fetch(input, init), +): Promise { + const target = substituteVars(url, settings.variables); + const finalHeaders = Object.fromEntries( + Object.entries(headers).map(([key, value]) => [key, substituteVars(value, settings.variables)]), + ); + let response: Response; + try { + response = await fetchFn(settings.proxy + target, { headers: finalHeaders }); + } catch { + throw new SourceError( + 'cors', + `Could not fetch ${target}. The server is unreachable or does not allow cross-origin requests (CORS). ` + + 'Paste or upload the spec instead, or set a CORS proxy under Settings.', + ); + } + if (!response.ok) { + throw new SourceError('http', `${target} returned HTTP ${response.status}.`); + } + return response.text(); +} + +/** Cache successful loads per (url, headers); failed loads are retried next time. */ +export function memoizeLoader(load: Loader): Loader { + const cache = new Map>(); + return (url, headers) => { + const key = JSON.stringify([url, headers]); + let hit = cache.get(key); + if (!hit) { + hit = load(url, headers); + cache.set(key, hit); + hit.catch(() => cache.delete(key)); + } + return hit; + }; +} + +export async function resolveSources( + sources: SourceSummary[], + files: WorkspaceFile[], + load: Loader, +): Promise<{ contents: string[] | null; problems: Problem[] }> { + const results = await Promise.all( + sources.map(async (source): Promise<{ content: string } | { problem: Problem }> => { + try { + if (source.kind === 'file') { + const key = workspaceKey(source.path ?? ''); + const file = files.find((f) => workspaceKey(f.name) === key); + if (!file) { + throw new SourceError('missing-file', `No workspace file named '${key}'. Add it to the workspace or fix the path.`); + } + return { content: file.content }; + } + return { content: await load(source.url ?? '', source.headers) }; + } catch (e) { + const kind = e instanceof SourceError ? e.kind : 'http'; + return { problem: { level: 'error', kind, source: source.name, message: errorMessage(e) } }; + } + }), + ); + + const problems = results.flatMap((r) => ('problem' in r ? [r.problem] : [])); + if (problems.length) return { contents: null, problems }; + return { contents: results.map((r) => (r as { content: string }).content), problems }; +} diff --git a/web/src/settings.ts b/web/src/settings.ts new file mode 100644 index 0000000..a517819 --- /dev/null +++ b/web/src/settings.ts @@ -0,0 +1,49 @@ +import type { Settings } from './types'; + +const KEY = 'openapi-aggregator.settings'; + +function defaults(): Settings { + return { proxy: '', variables: {} }; +} + +function browserStorage(): Storage | undefined { + try { + return globalThis.localStorage; + } catch { + return undefined; + } +} + +function isStringRecord(value: unknown): value is Record { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + Object.values(value).every((v) => typeof v === 'string') + ); +} + +/** Proxy and variables live only in this browser; never shared. */ +export function loadSettings(storage: Pick | undefined = browserStorage()): Settings { + try { + const raw = storage?.getItem(KEY); + if (!raw) return defaults(); + const parsed: unknown = JSON.parse(raw); + const { proxy, variables } = (parsed ?? {}) as Record; + if (typeof proxy !== 'string' || !isStringRecord(variables)) return defaults(); + return { proxy, variables }; + } catch { + return defaults(); + } +} + +export function saveSettings( + settings: Settings, + storage: Pick | undefined = browserStorage(), +): void { + try { + storage?.setItem(KEY, JSON.stringify(settings)); + } catch { + // Storage blocked (private mode, disabled site data): settings just won't persist. + } +} diff --git a/web/src/workspace.ts b/web/src/workspace.ts new file mode 100644 index 0000000..b636f3c --- /dev/null +++ b/web/src/workspace.ts @@ -0,0 +1,27 @@ +import type { WorkspaceFile } from './types'; + +/** Normalise a config `path:` or file name so `./specs/a.yaml` matches `specs/a.yaml`. */ +export function workspaceKey(path: string): string { + return path.replace(/^(\.\/)+/, ''); +} + +/** Insert `file`, or replace the content of the file with the same key. Returns its index. */ +export function upsertFile(files: WorkspaceFile[], file: WorkspaceFile): number { + const key = workspaceKey(file.name); + const index = files.findIndex((f) => workspaceKey(f.name) === key); + if (index >= 0) { + files[index].content = file.content; + return index; + } + files.push({ name: key, content: file.content }); + return files.length - 1; +} + +export function fileNameFromUrl(url: string): string { + try { + const last = new URL(url).pathname.split('/').filter(Boolean).pop(); + return last || 'imported.yaml'; + } catch { + return 'imported.yaml'; + } +} diff --git a/web/tests/resolve.test.ts b/web/tests/resolve.test.ts new file mode 100644 index 0000000..e6f4336 --- /dev/null +++ b/web/tests/resolve.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it, vi } from 'vitest'; +import { fetchText, memoizeLoader, resolveSources, substituteVars } from '../src/resolve'; +import type { Settings, SourceSummary } from '../src/types'; + +const settings = (over: Partial = {}): Settings => ({ proxy: '', variables: {}, ...over }); +const fileSource = (path: string, name = 'a'): SourceSummary => ({ name, kind: 'file', path, url: null, headers: {} }); +const urlSource = (url: string, name = 'remote'): SourceSummary => ({ name, kind: 'url', path: null, url, headers: { Authorization: 'Bearer ${T}' } }); + +describe('substituteVars', () => { + it('replaces placeholders', () => { + expect(substituteVars('Bearer ${T} ${T}', { T: 'x' })).toBe('Bearer x x'); + }); + it('names a missing variable', () => { + expect(() => substituteVars('${NOPE}', {})).toThrowError(/'NOPE'/); + }); + it('does not resolve prototype keys', () => { + expect(() => substituteVars('${toString}', {})).toThrowError(/'toString'/); + }); +}); + +describe('fetchText', () => { + it('applies variables and the proxy prefix', async () => { + const fetchFn = vi.fn(async () => new Response('spec')); + const text = await fetchText( + 'https://api.test/${V}.json', + { Authorization: 'Bearer ${T}' }, + settings({ proxy: 'https://proxy.test/?url=', variables: { V: 'openapi', T: 'tok' } }), + fetchFn, + ); + expect(text).toBe('spec'); + expect(fetchFn).toHaveBeenCalledWith('https://proxy.test/?url=https://api.test/openapi.json', { + headers: { Authorization: 'Bearer tok' }, + }); + }); + + it('classifies network failures as CORS problems', async () => { + const fetchFn = async () => { + throw new TypeError('Failed to fetch'); + }; + await expect(fetchText('https://api.test/a', {}, settings(), fetchFn)).rejects.toMatchObject({ + kind: 'cors', + message: expect.stringMatching(/CORS/), + }); + }); + + it('classifies non-2xx responses as HTTP problems', async () => { + const fetchFn = async () => new Response('no', { status: 404 }); + await expect(fetchText('https://api.test/a', {}, settings(), fetchFn)).rejects.toMatchObject({ + kind: 'http', + message: expect.stringMatching(/404/), + }); + }); +}); + +describe('memoizeLoader', () => { + it('caches successes and retries failures', async () => { + let calls = 0; + const load = memoizeLoader(async () => { + calls += 1; + if (calls === 1) throw new Error('flaky'); + return 'ok'; + }); + await expect(load('u', {})).rejects.toThrow('flaky'); + expect(await load('u', {})).toBe('ok'); + expect(await load('u', {})).toBe('ok'); + expect(calls).toBe(2); + }); +}); + +describe('resolveSources', () => { + const files = [{ name: 'specs/a.yaml', content: 'A' }]; + + it('maps file sources to workspace files and url sources to the loader, in order', async () => { + const load = vi.fn(async () => 'REMOTE'); + const result = await resolveSources([urlSource('https://x.test/o.json'), fileSource('./specs/a.yaml')], files, load); + expect(result).toEqual({ contents: ['REMOTE', 'A'], problems: [] }); + expect(load).toHaveBeenCalledWith('https://x.test/o.json', { Authorization: 'Bearer ${T}' }); + }); + + it('reports every failing source, in source order, and returns no contents', async () => { + const load = async () => { + throw new Error('boom'); + }; + const result = await resolveSources([fileSource('missing.yaml', 'm'), urlSource('https://x.test', 'r')], files, load); + expect(result.contents).toBeNull(); + expect(result.problems.map((p) => [p.source, p.kind])).toEqual([ + ['m', 'missing-file'], + ['r', 'http'], + ]); + expect(result.problems[0].message).toContain("No workspace file named 'missing.yaml'"); + }); +}); diff --git a/web/tests/settings.test.ts b/web/tests/settings.test.ts new file mode 100644 index 0000000..d740dc2 --- /dev/null +++ b/web/tests/settings.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { loadSettings, saveSettings } from '../src/settings'; + +function memoryStorage(initial: Record = {}) { + const data = { ...initial }; + return { + data, + getItem: (k: string) => data[k] ?? null, + setItem: (k: string, v: string) => { + data[k] = v; + }, + }; +} + +describe('settings', () => { + it('round-trips through storage', () => { + const storage = memoryStorage(); + saveSettings({ proxy: 'https://p.test/?u=', variables: { TOKEN: 't' } }, storage); + expect(loadSettings(storage)).toEqual({ proxy: 'https://p.test/?u=', variables: { TOKEN: 't' } }); + }); + + it('returns defaults for missing, corrupt or mistyped data', () => { + expect(loadSettings(memoryStorage())).toEqual({ proxy: '', variables: {} }); + expect(loadSettings(memoryStorage({ 'openapi-aggregator.settings': '{oops' }))).toEqual({ proxy: '', variables: {} }); + expect( + loadSettings(memoryStorage({ 'openapi-aggregator.settings': '{"proxy":1,"variables":{"A":2}}' })), + ).toEqual({ proxy: '', variables: {} }); + }); + + it('survives storage that throws', () => { + const throwing = { + getItem: () => { + throw new Error('blocked'); + }, + setItem: () => { + throw new Error('blocked'); + }, + }; + expect(loadSettings(throwing)).toEqual({ proxy: '', variables: {} }); + expect(() => saveSettings({ proxy: '', variables: {} }, throwing)).not.toThrow(); + }); +}); diff --git a/web/tests/workspace.test.ts b/web/tests/workspace.test.ts new file mode 100644 index 0000000..dbc33ab --- /dev/null +++ b/web/tests/workspace.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; +import { fileNameFromUrl, upsertFile, workspaceKey } from '../src/workspace'; + +describe('workspaceKey', () => { + it('strips leading ./ segments', () => { + expect(workspaceKey('./specs/a.yaml')).toBe('specs/a.yaml'); + expect(workspaceKey('././a.yaml')).toBe('a.yaml'); + expect(workspaceKey('specs/a.yaml')).toBe('specs/a.yaml'); + }); +}); + +describe('upsertFile', () => { + it('replaces the content of a file with the same key instead of duplicating it', () => { + const files = [{ name: 'specs/a.yaml', content: 'old' }]; + const index = upsertFile(files, { name: './specs/a.yaml', content: 'new' }); + expect(index).toBe(0); + expect(files).toEqual([{ name: 'specs/a.yaml', content: 'new' }]); + }); + + it('appends new files', () => { + const files = [{ name: 'a.yaml', content: '' }]; + expect(upsertFile(files, { name: 'b.yaml', content: '' })).toBe(1); + expect(files).toHaveLength(2); + }); +}); + +describe('fileNameFromUrl', () => { + it('uses the last path segment', () => { + expect(fileNameFromUrl('https://x.test/v1/openapi.json?x=1')).toBe('openapi.json'); + }); + it('falls back for bare hosts and invalid urls', () => { + expect(fileNameFromUrl('https://x.test/')).toBe('imported.yaml'); + expect(fileNameFromUrl('not a url')).toBe('imported.yaml'); + }); +}); From c854464b13baf2fd7095e87eb503b93d28d1a0ed Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 12:02:20 +0200 Subject: [PATCH 08/15] feat(web): compressed share links in the URL hash --- web/src/share.ts | 58 +++++++++++++++++++++++++++++++++++++++++ web/tests/share.test.ts | 33 +++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 web/src/share.ts create mode 100644 web/tests/share.test.ts diff --git a/web/src/share.ts b/web/src/share.ts new file mode 100644 index 0000000..277031a --- /dev/null +++ b/web/src/share.ts @@ -0,0 +1,58 @@ +import type { WorkspaceFile } from './types'; + +export interface SharePayload { + v: 1; + config: string; + files: WorkspaceFile[]; +} + +export const MAX_SHARE_LENGTH = 8192; + +async function pipe( + bytes: Uint8Array, + transform: CompressionStream | DecompressionStream, +): Promise> { + const stream = new Blob([bytes]).stream().pipeThrough(transform); + return new Uint8Array(await new Response(stream).arrayBuffer()); +} + +function toBase64Url(bytes: Uint8Array): string { + let binary = ''; + for (const b of bytes) binary += String.fromCharCode(b); + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +function fromBase64Url(text: string): Uint8Array { + const base64 = text.replace(/-/g, '+').replace(/_/g, '/'); + const binary = atob(base64 + '='.repeat((4 - (base64.length % 4)) % 4)); + return Uint8Array.from(binary, (c) => c.charCodeAt(0)); +} + +function isPayload(value: unknown): value is SharePayload { + const p = value as SharePayload; + return ( + typeof p === 'object' && + p !== null && + p.v === 1 && + typeof p.config === 'string' && + Array.isArray(p.files) && + p.files.every((f) => typeof f?.name === 'string' && typeof f?.content === 'string') + ); +} + +/** Variables and proxy settings are deliberately not part of the payload. */ +export async function encodeShare(payload: SharePayload): Promise { + const json = new TextEncoder().encode(JSON.stringify(payload)); + return toBase64Url(await pipe(json, new CompressionStream('deflate-raw'))); +} + +export async function decodeShare(encoded: string): Promise { + const bytes = await pipe(fromBase64Url(encoded), new DecompressionStream('deflate-raw')); + const value: unknown = JSON.parse(new TextDecoder().decode(bytes)); + if (!isPayload(value)) throw new Error('This share link is not a valid playground link.'); + return { v: 1, config: value.config, files: value.files.map(({ name, content }) => ({ name, content })) }; +} + +export function shareParam(hash: string): string | null { + return hash.startsWith('#s=') ? hash.slice(3) : null; +} diff --git a/web/tests/share.test.ts b/web/tests/share.test.ts new file mode 100644 index 0000000..f327cd1 --- /dev/null +++ b/web/tests/share.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { decodeShare, encodeShare, shareParam, type SharePayload } from '../src/share'; + +const payload: SharePayload = { + v: 1, + config: 'sources:\n - path: specs/a.yaml\n', + files: [{ name: 'specs/a.yaml', content: 'openapi: 3.0.3\ninfo: {title: 日本語 API 🚀, version: "1"}\npaths: {}\n' }], +}; + +describe('share links', () => { + it('round-trips, including non-ASCII text', async () => { + const encoded = await encodeShare(payload); + expect(encoded).toMatch(/^[A-Za-z0-9_-]+$/); + expect(await decodeShare(encoded)).toEqual(payload); + }); + + it('compresses repetitive specs', async () => { + const big = { ...payload, files: [{ name: 'a.yaml', content: 'paths:\n' + ' /x: {}\n'.repeat(2000) }] }; + expect((await encodeShare(big)).length).toBeLessThan(2000); + }); + + it('rejects garbage and wrong shapes', async () => { + await expect(decodeShare('!!!')).rejects.toThrow(); + const wrong = await encodeShare({ v: 2, config: 1 } as unknown as SharePayload); + await expect(decodeShare(wrong)).rejects.toThrow(/share link/); + }); + + it('extracts the s= hash parameter', () => { + expect(shareParam('#s=abc')).toBe('abc'); + expect(shareParam('#other')).toBeNull(); + expect(shareParam('')).toBeNull(); + }); +}); From 0a89227b154c5dc6626d78d2c53a9eb0a3149ae5 Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 12:02:42 +0200 Subject: [PATCH 09/15] feat(web): add playground examples --- web/src/examples.ts | 271 +++++++++++++++++++++++++++++++++++++ web/tests/examples.test.ts | 61 +++++++++ 2 files changed, 332 insertions(+) create mode 100644 web/src/examples.ts create mode 100644 web/tests/examples.test.ts diff --git a/web/src/examples.ts b/web/src/examples.ts new file mode 100644 index 0000000..0682965 --- /dev/null +++ b/web/src/examples.ts @@ -0,0 +1,271 @@ +import type { WorkspaceFile } from './types'; + +export interface Example { + id: string; + title: string; + config: string; + files: WorkspaceFile[]; +} + +const USERS = `openapi: 3.0.3 +info: + title: Users API + version: 1.0.0 +tags: + - name: users +paths: + /users: + get: + tags: [users] + summary: List users + responses: + '200': + description: Users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' +components: + schemas: + User: + type: object + properties: + id: { type: integer } + email: { type: string } + Error: + type: object + properties: + message: { type: string } +`; + +const ORDERS = `openapi: 3.0.3 +info: + title: Orders API + version: 1.0.0 +tags: + - name: orders +paths: + /orders: + get: + tags: [orders] + summary: List orders + responses: + '200': + description: Orders + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Order' +components: + schemas: + Order: + type: object + properties: + id: { type: integer } + total: { type: number } + Error: + type: object + properties: + message: { type: string } +`; + +const USERS_READ = `openapi: 3.0.3 +info: + title: Users (read) + version: 1.0.0 +paths: + /users: + get: + summary: List users + responses: + '200': { description: Users } +`; + +const USERS_WRITE = `openapi: 3.0.3 +info: + title: Users (write) + version: 1.0.0 +paths: + /users: + post: + summary: Create a user + responses: + '201': { description: Created } +`; + +const INVENTORY = `openapi: 3.0.3 +info: + title: Inventory API + version: 1.0.0 +paths: + /items: + get: + summary: List stock items + responses: + '200': + description: Items + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + properties: + sku: { type: string } + quantity: { type: integer } +`; + +const CATALOG = `openapi: 3.0.3 +info: + title: Catalog API + version: 1.0.0 +paths: + /items: + get: + summary: List catalog items + responses: + '200': + description: Items + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + properties: + id: { type: integer } + title: { type: string } +`; + +export const EXAMPLES: Example[] = [ + { + id: 'rename', + title: 'Conflicts → rename', + config: `# Both services define GET /items and a different "Item" schema. +# With rename, the second source's path and schema get its name as a prefix, +# and its $refs are rewritten to match. +sources: + - name: inventory + path: ./specs/inventory.yaml + - name: catalog + path: ./specs/catalog.yaml + +merge: + conflict_strategy: rename + info: + title: Shop API + version: 1.0.0 +`, + files: [ + { name: 'specs/inventory.yaml', content: INVENTORY }, + { name: 'specs/catalog.yaml', content: CATALOG }, + ], + }, + { + id: 'clean', + title: 'Clean merge of two services', + config: `# No conflicts: paths and schemas are combined. +# Both define an identical "Error" schema, which is shared, not duplicated. +sources: + - name: users + path: ./specs/users.yaml + - name: orders + path: ./specs/orders.yaml +`, + files: [ + { name: 'specs/users.yaml', content: USERS }, + { name: 'specs/orders.yaml', content: ORDERS }, + ], + }, + { + id: 'per-operation', + title: 'Same path, different methods', + config: `# GET /users and POST /users come from different services. +# They are merged into one path; only the same method twice would conflict. +sources: + - name: users-read + path: ./specs/users-read.yaml + - name: users-write + path: ./specs/users-write.yaml +`, + files: [ + { name: 'specs/users-read.yaml', content: USERS_READ }, + { name: 'specs/users-write.yaml', content: USERS_WRITE }, + ], + }, + { + id: 'tags', + title: 'Tag prefixing', + config: `# Every tag (and every operation's tag reference) is prefixed with its source. +sources: + - name: users + path: ./specs/users.yaml + - name: orders + path: ./specs/orders.yaml + tag_prefix: Orders # custom prefix instead of the source name + +merge: + tag_prefix: source_name + tag_separator: " / " +`, + files: [ + { name: 'specs/users.yaml', content: USERS }, + { name: 'specs/orders.yaml', content: ORDERS }, + ], + }, + { + id: 'additional-blocks', + title: 'Additional blocks (vendor extensions)', + config: `# additional_blocks is deep-merged into a source before merging, +# e.g. to add API gateway extensions without editing the original spec. +sources: + - name: users + path: ./specs/users.yaml + additional_blocks: + x-gateway: + upstream: http://users.internal:8080 + paths: + /users: + get: + x-rate-limit: 100 + +output: + format: json +`, + files: [{ name: 'specs/users.yaml', content: USERS }], + }, + { + id: 'remote', + title: 'Remote URL + local file', + config: `# The Petstore spec is fetched from the web (its server allows CORS). +# Both specs define "User", so rename keeps them apart. +sources: + - name: petstore + url: https://petstore3.swagger.io/api/v3/openapi.json + - name: users + path: ./specs/users.yaml + +merge: + conflict_strategy: rename +`, + files: [{ name: 'specs/users.yaml', content: USERS }], + }, +]; + +export const DEFAULT_EXAMPLE_ID = 'rename'; + +export function findExample(id: string): Example { + return EXAMPLES.find((e) => e.id === id) ?? EXAMPLES.find((e) => e.id === DEFAULT_EXAMPLE_ID)!; +} diff --git a/web/tests/examples.test.ts b/web/tests/examples.test.ts new file mode 100644 index 0000000..dd3510c --- /dev/null +++ b/web/tests/examples.test.ts @@ -0,0 +1,61 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { aggregate, parseConfig } from '../src/engine'; +import { DEFAULT_EXAMPLE_ID, EXAMPLES, findExample } from '../src/examples'; +import { resolveSources } from '../src/resolve'; +import { loadEngineForTests } from './helpers'; + +// Loaded at collection time too: parseConfigSafe() below runs while tests are being defined. +loadEngineForTests(); +beforeAll(loadEngineForTests); + +const noNetwork = async () => { + throw new Error('network not allowed in tests'); +}; + +describe('examples', () => { + it('has unique ids and the default exists', () => { + expect(new Set(EXAMPLES.map((e) => e.id)).size).toBe(EXAMPLES.length); + expect(findExample(DEFAULT_EXAMPLE_ID).id).toBe('rename'); + expect(findExample('nope').id).toBe('rename'); + }); + + for (const example of EXAMPLES) { + it(`${example.id}: config parses`, () => { + expect(() => parseConfig(example.config)).not.toThrow(); + }); + + const usesNetwork = parseConfigSafe(example.config).some((s) => s.kind === 'url'); + it.skipIf(usesNetwork)(`${example.id}: merges without errors or warnings`, async () => { + const summary = parseConfig(example.config); + const { contents, problems } = await resolveSources(summary.sources, example.files, noNetwork); + expect(problems).toEqual([]); + const out = aggregate(example.config, contents!); + expect(out.warnings).toEqual([]); + expect(out.text.length).toBeGreaterThan(0); + }); + } + + it('rename example shows renamed paths, components and refs', async () => { + const ex = findExample('rename'); + const { contents } = await resolveSources(parseConfig(ex.config).sources, ex.files, noNetwork); + const text = aggregate(ex.config, contents!).text; + expect(text).toContain('/catalog/items:'); + expect(text).toContain('catalog_Item:'); + expect(text).toContain("'#/components/schemas/catalog_Item'"); + }); + + it('per-operation example combines GET and POST on one path', async () => { + const ex = findExample('per-operation'); + const { contents } = await resolveSources(parseConfig(ex.config).sources, ex.files, noNetwork); + const text = aggregate(ex.config, contents!).text; + expect(text).toMatch(/\/users:\n\s+get:[\s\S]*\n\s+post:/); + }); +}); + +function parseConfigSafe(config: string) { + try { + return parseConfig(config).sources; + } catch { + return []; + } +} From 4a0f07b7dedb33cf5900901cad6e5e5a92720da9 Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 12:02:59 +0200 Subject: [PATCH 10/15] feat(web): merge pipeline that turns failures into problems --- web/src/pipeline.ts | 55 +++++++++++++++++++++++++ web/tests/pipeline.test.ts | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 web/src/pipeline.ts create mode 100644 web/tests/pipeline.test.ts diff --git a/web/src/pipeline.ts b/web/src/pipeline.ts new file mode 100644 index 0000000..ec23d0d --- /dev/null +++ b/web/src/pipeline.ts @@ -0,0 +1,55 @@ +import { errorMessage, resolveSources, type Loader } from './resolve'; +import type { AggregateOutput, ConfigSummary, Problem, WorkspaceFile } from './types'; + +export interface PipelineResult { + text: string | null; + format: 'yaml' | 'json'; + problems: Problem[]; +} + +export interface PipelineDeps { + parseConfig(yaml: string): ConfigSummary; + aggregate(yaml: string, contents: string[]): AggregateOutput; + load: Loader; +} + +export function lineOf(message: string): number | undefined { + const match = /line (\d+)/.exec(message); + return match ? Number(match[1]) : undefined; +} + +/** Config → resolve sources → merge. Never throws; every failure becomes a Problem. */ +export async function runPipeline(config: string, files: WorkspaceFile[], deps: PipelineDeps): Promise { + let summary: ConfigSummary; + try { + summary = deps.parseConfig(config); + } catch (e) { + const message = errorMessage(e); + return { text: null, format: 'yaml', problems: [{ level: 'error', kind: 'config', message, line: lineOf(message) }] }; + } + + const { contents, problems } = await resolveSources(summary.sources, files, deps.load); + if (!contents) return { text: null, format: summary.format, problems }; + + try { + const out = deps.aggregate(config, contents); + const warnings: Problem[] = out.warnings.map((message) => ({ level: 'warning', kind: 'merge', message })); + return { text: out.text, format: summary.format, problems: [...problems, ...warnings] }; + } catch (e) { + return { + text: null, + format: summary.format, + problems: [...problems, { level: 'error', kind: 'merge', message: errorMessage(e) }], + }; + } +} + +/** Wrap an async function so only the most recent call's result is delivered. */ +export function latestOnly(fn: (...args: A) => Promise): (...args: A) => Promise { + let latest = 0; + return async (...args: A) => { + const id = ++latest; + const result = await fn(...args); + return id === latest ? result : undefined; + }; +} diff --git a/web/tests/pipeline.test.ts b/web/tests/pipeline.test.ts new file mode 100644 index 0000000..6e40f28 --- /dev/null +++ b/web/tests/pipeline.test.ts @@ -0,0 +1,82 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { aggregate, parseConfig } from '../src/engine'; +import { latestOnly, lineOf, runPipeline, type PipelineDeps } from '../src/pipeline'; +import { loadEngineForTests } from './helpers'; + +beforeAll(loadEngineForTests); + +const deps = (load: PipelineDeps['load'] = async () => 'unused'): PipelineDeps => ({ parseConfig, aggregate, load }); +const spec = (title: string, paths = ' /a:\n get: {summary: a}\n') => + `openapi: 3.0.3\ninfo: {title: ${title}, version: '1'}\npaths:\n${paths}`; + +describe('runPipeline', () => { + it('merges workspace files', async () => { + const result = await runPipeline('sources:\n - path: a.yaml\n', [{ name: 'a.yaml', content: spec('A') }], deps()); + expect(result.text).toContain('title: A'); + expect(result.problems).toEqual([]); + expect(result.format).toBe('yaml'); + }); + + it('reports config errors with a line number and no output', async () => { + const result = await runPipeline('sources:\n - path: a.yaml\n bogus: 1\n', [], deps()); + expect(result.text).toBeNull(); + expect(result.problems).toHaveLength(1); + expect(result.problems[0]).toMatchObject({ level: 'error', kind: 'config' }); + expect(result.problems[0].message).toContain('bogus'); + expect(result.problems[0].line).toBeGreaterThan(0); + }); + + it('names a Swagger 2.0 source and says only 3.x is supported', async () => { + const result = await runPipeline( + 'sources:\n - name: legacy\n path: old.yaml\n', + [{ name: 'old.yaml', content: "swagger: '2.0'\ninfo: {title: Old, version: '1'}\npaths: {}\n" }], + deps(), + ); + expect(result.text).toBeNull(); + expect(result.problems[0]).toMatchObject({ kind: 'merge' }); + expect(result.problems[0].message).toMatch(/legacy/); + expect(result.problems[0].message).toMatch(/only OpenAPI 3\.x/); + }); + + it('turns merge warnings into warning problems while keeping output', async () => { + const v31 = spec('B', ' /b:\n get: {summary: b}\n').replace('3.0.3', '3.1.0'); + const result = await runPipeline( + 'sources:\n - path: a.yaml\n - path: b.yaml\n', + [ + { name: 'a.yaml', content: spec('A') }, + { name: 'b.yaml', content: v31 }, + ], + deps(), + ); + expect(result.text).not.toBeNull(); + expect(result.problems).toEqual([expect.objectContaining({ level: 'warning', kind: 'merge' })]); + }); + + it('stays fast for large specs (3,000 paths)', async () => { + const paths = Array.from({ length: 3000 }, (_, i) => ` /r${i}:\n get: {summary: r${i}}\n`).join(''); + const started = performance.now(); + const result = await runPipeline('sources:\n - path: big.yaml\n', [{ name: 'big.yaml', content: spec('Big', paths) }], deps()); + expect(result.problems).toEqual([]); + expect(performance.now() - started).toBeLessThan(2000); + }); +}); + +describe('lineOf', () => { + it('extracts the line from serde_yaml messages', () => { + expect(lineOf('unknown field `bogus` at line 3 column 5')).toBe(3); + expect(lineOf('no location')).toBeUndefined(); + }); +}); + +describe('latestOnly', () => { + it('drops results of superseded calls', async () => { + const resolvers: Array<(v: string) => void> = []; + const run = latestOnly((_: number) => new Promise((resolve) => resolvers.push(resolve))); + const first = run(1); + const second = run(2); + resolvers[1]('second'); + resolvers[0]('first'); + expect(await second).toBe('second'); + expect(await first).toBeUndefined(); + }); +}); From d0ce2f3b70792bee5d7a2bee9bc61c0b07a5ab11 Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 12:05:25 +0200 Subject: [PATCH 11/15] feat(web): playground UI with editors, examples, share links and API reference --- web/e2e/smoke.spec.ts | 29 +++++ web/index.html | 63 ++++++++- web/package-lock.json | 13 ++ web/package.json | 1 + web/playwright.config.ts | 12 ++ web/src/app.ts | 275 +++++++++++++++++++++++++++++++++++++++ web/src/main.ts | 4 +- web/src/style.css | 40 ++++++ web/src/ui/editor.ts | 50 +++++++ web/src/ui/reference.ts | 20 +++ 10 files changed, 504 insertions(+), 3 deletions(-) create mode 100644 web/e2e/smoke.spec.ts create mode 100644 web/playwright.config.ts create mode 100644 web/src/app.ts create mode 100644 web/src/style.css create mode 100644 web/src/ui/editor.ts create mode 100644 web/src/ui/reference.ts diff --git a/web/e2e/smoke.spec.ts b/web/e2e/smoke.spec.ts new file mode 100644 index 0000000..1e044bd --- /dev/null +++ b/web/e2e/smoke.spec.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; + +test('default example merges, reports no problems, and renders the reference', async ({ page }) => { + await page.goto('./'); + await expect(page.locator('#output')).toContainText('catalog_Item'); + await expect(page.locator('#problem-count')).toHaveText(''); + + await page.getByRole('tab', { name: 'API reference' }).click(); + await expect(page.locator('#reference')).toContainText('Shop API', { timeout: 20_000 }); +}); + +test('editing the config re-runs the merge and surfaces config errors', async ({ page }) => { + await page.goto('./'); + await expect(page.locator('#output')).toContainText('catalog_Item'); + + await page.locator('#config-editor .cm-content').click(); + await page.keyboard.press('ControlOrMeta+a'); + await page.keyboard.type('sources: [{name: x}]'); + + await expect(page.locator('#problem-count')).toHaveText('(1)'); + await page.getByRole('tab', { name: /Problems/ }).click(); + await expect(page.locator('#problems')).toContainText("either 'path' or 'url'"); +}); + +test('an invalid share link falls back to the default example with a notice', async ({ page }) => { + await page.goto('./#s=not-valid'); + await expect(page.locator('#notice')).toContainText('share link'); + await expect(page.locator('#output')).toContainText('catalog_Item'); +}); diff --git a/web/index.html b/web/index.html index 143db6a..57b6904 100644 --- a/web/index.html +++ b/web/index.html @@ -1,5 +1,64 @@ - OpenAPI Aggregator Playground - + + + + OpenAPI Aggregator Playground + + + +
+

openapi-aggregator playground

+ + +
GitHub +
+ +
+
+

Workspace

+
    +
    + + + +
    +
    + +

    Config openapi-aggregator.yaml

    +
    +
    + +
    + Settings + +

    Prepended to every fetched URL. Stored only in this browser.

    +

    Variables

    +

    Values for ${VAR} in source URLs and headers. Stored only in this browser and never included in share links.

    +
    + +
    +
    + +
    +
    + + + +
    +
    +
    + + +
    +
    
    +        
    + + +
    +
    + + diff --git a/web/package-lock.json b/web/package-lock.json index f19ca43..3f50239 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,6 +9,7 @@ "@codemirror/lang-yaml": "^6.1.3", "@codemirror/lint": "^6.9.7", "@codemirror/state": "^6.7.6", + "@codemirror/theme-one-dark": "^6.1.3", "@scalar/api-reference": "^1.72.3", "codemirror": "^6.0.2" }, @@ -282,6 +283,18 @@ "@marijn/find-cluster-break": "^1.0.0" } }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, "node_modules/@codemirror/view": { "version": "6.43.13", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.13.tgz", diff --git a/web/package.json b/web/package.json index a3005cf..667ed25 100644 --- a/web/package.json +++ b/web/package.json @@ -22,6 +22,7 @@ "@codemirror/lang-yaml": "^6.1.3", "@codemirror/lint": "^6.9.7", "@codemirror/state": "^6.7.6", + "@codemirror/theme-one-dark": "^6.1.3", "@scalar/api-reference": "^1.72.3", "codemirror": "^6.0.2" } diff --git a/web/playwright.config.ts b/web/playwright.config.ts new file mode 100644 index 0000000..ad7ce43 --- /dev/null +++ b/web/playwright.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: 'e2e', + use: { baseURL: 'http://localhost:4173/openapi-aggregator/' }, + webServer: { + command: 'npm run build && npm run preview -- --port 4173 --strictPort', + url: 'http://localhost:4173/openapi-aggregator/', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}); diff --git a/web/src/app.ts b/web/src/app.ts new file mode 100644 index 0000000..e35bf20 --- /dev/null +++ b/web/src/app.ts @@ -0,0 +1,275 @@ +import { aggregate, parseConfig } from './engine'; +import { DEFAULT_EXAMPLE_ID, EXAMPLES, findExample, type Example } from './examples'; +import { latestOnly, runPipeline, type PipelineResult } from './pipeline'; +import { errorMessage, fetchText, memoizeLoader } from './resolve'; +import { loadSettings, saveSettings } from './settings'; +import { decodeShare, encodeShare, MAX_SHARE_LENGTH, shareParam } from './share'; +import type { Problem, WorkspaceFile } from './types'; +import { createYamlEditor } from './ui/editor'; +import { showReference } from './ui/reference'; +import { fileNameFromUrl, upsertFile } from './workspace'; + +const NEW_FILE = `openapi: 3.0.3 +info: + title: New API + version: 1.0.0 +paths: {} +`; + +function $(selector: string): T { + const el = document.querySelector(selector); + if (!el) throw new Error(`missing element ${selector}`); + return el; +} + +function button(label: string, onClick: () => void): HTMLButtonElement { + const b = document.createElement('button'); + b.type = 'button'; + b.textContent = label; + b.onclick = onClick; + return b; +} + +export async function startApp(): Promise { + const settings = loadSettings(); + const state = { config: '', files: [] as WorkspaceFile[], selected: 0 }; + let result: PipelineResult = { text: null, format: 'yaml', problems: [] }; + let activeTab = 'merged'; + let timer: ReturnType | undefined; + let noticeTimer: ReturnType | undefined; + + const newLoader = () => memoizeLoader((url, headers) => fetchText(url, headers, settings)); + let load = newLoader(); + const pipeline = latestOnly(() => runPipeline(state.config, state.files, { parseConfig, aggregate, load })); + + const configEditor = createYamlEditor($('#config-editor'), '', (text) => { + state.config = text; + schedule(); + }); + const fileEditor = createYamlEditor($('#file-editor'), '', (text) => { + const file = state.files[state.selected]; + if (file) { + file.content = text; + schedule(); + } + }); + + function notice(message: string): void { + const el = $('#notice'); + el.textContent = message; + el.hidden = false; + clearTimeout(noticeTimer); + noticeTimer = setTimeout(() => (el.hidden = true), 8000); + } + + function schedule(): void { + clearTimeout(timer); + timer = setTimeout(run, 300); + } + + async function run(): Promise { + const next = await pipeline(); + if (!next) return; // superseded by a newer run + result = next; + $('#output').textContent = result.text ?? 'No output. See the Problems tab.'; + renderProblems(result.problems); + const configError = result.problems.find((p) => p.kind === 'config'); + configEditor.showError(configError?.line, configError?.message); + if (activeTab === 'reference' && result.text) void showReference($('#reference'), result.text); + } + + function renderProblems(problems: Problem[]): void { + $('#problem-count').textContent = problems.length ? `(${problems.length})` : ''; + $('#problems').replaceChildren( + ...problems.map((p) => { + const li = document.createElement('li'); + li.className = p.level; + li.textContent = (p.source ? `[${p.source}] ` : '') + p.message; + return li; + }), + ); + } + + function renderFiles(): void { + $('#files').replaceChildren( + ...state.files.map((file, index) => { + const li = document.createElement('li'); + if (index === state.selected) li.className = 'selected'; + const name = button(file.name, () => selectFile(index)); + name.className = 'file-name'; + const rename = button('Rename', () => { + const next = prompt('File name (as referenced by `path:` in the config)', file.name)?.trim(); + if (next) { + file.name = next; + renderFiles(); + schedule(); + } + }); + const remove = button('Delete', () => { + state.files.splice(index, 1); + selectFile(Math.min(state.selected, state.files.length - 1)); + schedule(); + }); + li.append(name, rename, remove); + return li; + }), + ); + $('#file-editor').hidden = state.files.length === 0; + } + + function selectFile(index: number): void { + state.selected = Math.max(0, index); + fileEditor.setDoc(state.files[state.selected]?.content ?? ''); + renderFiles(); + } + + function addFile(file: WorkspaceFile): void { + selectFile(upsertFile(state.files, file)); + schedule(); + } + + function loadWorkspace(config: string, files: WorkspaceFile[]): void { + state.config = config; + state.files = files.map((f) => ({ ...f })); + configEditor.setDoc(config); + selectFile(0); + void run(); + } + + function loadExample(example: Example): void { + $('#examples').value = example.id; + loadWorkspace(example.config, example.files); + } + + // --- examples --- + const select = $('#examples'); + select.replaceChildren(...EXAMPLES.map((e) => new Option(e.title, e.id))); + select.onchange = () => { + history.replaceState(null, '', location.pathname); + loadExample(findExample(select.value)); + }; + + // --- workspace actions --- + $('#add-file').onclick = () => { + const name = prompt('File name', 'specs/new.yaml')?.trim(); + if (name) addFile({ name, content: NEW_FILE }); + }; + $('#upload').onchange = async (event) => { + const input = event.target as HTMLInputElement; + for (const file of Array.from(input.files ?? [])) addFile({ name: `specs/${file.name}`, content: await file.text() }); + input.value = ''; + }; + $('#import-url').onclick = async () => { + const url = prompt('Spec URL')?.trim(); + if (!url) return; + try { + addFile({ name: `specs/${fileNameFromUrl(url)}`, content: await fetchText(url, {}, settings) }); + } catch (e) { + notice(errorMessage(e)); + } + }; + + // --- settings --- + const proxy = $('#proxy'); + proxy.value = settings.proxy; + proxy.onchange = () => { + settings.proxy = proxy.value.trim(); + settingsChanged(); + }; + + function settingsChanged(): void { + saveSettings(settings); + load = newLoader(); + schedule(); + } + + function renderVariables(): void { + $('#variables').replaceChildren( + ...Object.entries(settings.variables).map(([key, value]) => { + const row = document.createElement('div'); + row.className = 'row'; + const keyInput = Object.assign(document.createElement('input'), { value: key, placeholder: 'NAME' }); + const valueInput = Object.assign(document.createElement('input'), { value, placeholder: 'value', type: 'password' }); + const update = () => { + delete settings.variables[key]; + if (keyInput.value.trim()) settings.variables[keyInput.value.trim()] = valueInput.value; + settingsChanged(); + renderVariables(); + }; + keyInput.onchange = update; + valueInput.onchange = update; + row.append(keyInput, valueInput, button('Remove', () => { + delete settings.variables[key]; + settingsChanged(); + renderVariables(); + })); + return row; + }), + ); + } + $('#add-variable').onclick = () => { + const name = prompt('Variable name (used as ${NAME})')?.trim(); + if (name) { + settings.variables[name] = ''; + settingsChanged(); + renderVariables(); + } + }; + renderVariables(); + + // --- output --- + for (const tab of document.querySelectorAll('[role=tab]')) { + tab.onclick = () => { + activeTab = tab.dataset.tab!; + for (const t of document.querySelectorAll('[role=tab]')) { + t.setAttribute('aria-selected', String(t === tab)); + } + for (const panel of document.querySelectorAll('[data-panel]')) { + panel.hidden = panel.dataset.panel !== activeTab; + } + if (activeTab === 'reference' && result.text) void showReference($('#reference'), result.text); + }; + } + $('#copy-config').onclick = async () => { + await navigator.clipboard.writeText(state.config); + notice('Config copied. Run it with: openapi-aggregator -c openapi-aggregator.yaml'); + }; + $('#copy-output').onclick = async () => { + if (result.text) await navigator.clipboard.writeText(result.text); + }; + $('#download-output').onclick = () => { + if (!result.text) return; + const a = document.createElement('a'); + a.href = URL.createObjectURL(new Blob([result.text], { type: 'text/plain' })); + a.download = `openapi.${result.format}`; + a.click(); + URL.revokeObjectURL(a.href); + }; + + // --- sharing --- + $('#share').onclick = async () => { + const encoded = await encodeShare({ v: 1, config: state.config, files: state.files }); + if (encoded.length > MAX_SHARE_LENGTH) { + notice('This workspace is too large for a link. Download the merged spec and copy the config instead.'); + return; + } + const url = `${location.origin}${location.pathname}#s=${encoded}`; + history.replaceState(null, '', url); + await navigator.clipboard.writeText(url); + notice('Share link copied. It contains the config and workspace files, never your variables or proxy.'); + }; + + // --- startup --- + const shared = shareParam(location.hash); + if (shared) { + try { + const payload = await decodeShare(shared); + select.value = ''; + loadWorkspace(payload.config, payload.files); + return; + } catch { + notice('That share link could not be read, so the default example was loaded instead.'); + } + } + loadExample(findExample(DEFAULT_EXAMPLE_ID)); +} diff --git a/web/src/main.ts b/web/src/main.ts index aa2c48b..4dbd112 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -1,4 +1,6 @@ +import './style.css'; +import { startApp } from './app'; import { initEngine } from './engine'; await initEngine(); -document.body.textContent = 'engine ready'; +await startApp(); diff --git a/web/src/style.css b/web/src/style.css new file mode 100644 index 0000000..3b106a1 --- /dev/null +++ b/web/src/style.css @@ -0,0 +1,40 @@ +:root { + --bg: #fafaf9; --surface: #ffffff; --text: #1c1917; --muted: #78716c; + --border: #e7e5e4; --accent: #2563eb; --error: #b91c1c; --warning: #a16207; + font-family: system-ui, sans-serif; color-scheme: light; +} +@media (prefers-color-scheme: dark) { + :root:not([data-theme='light']) { + --bg: #0c0a09; --surface: #1c1917; --text: #f5f5f4; --muted: #a8a29e; + --border: #292524; --accent: #60a5fa; --error: #f87171; --warning: #facc15; + color-scheme: dark; + } +} +* { box-sizing: border-box; } +body { margin: 0; background: var(--bg); color: var(--text); } +button, .button { font: inherit; padding: 4px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text); cursor: pointer; } +.topbar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; padding: 10px 16px; border-bottom: 1px solid var(--border); } +.topbar h1 { font-size: 1.05rem; margin: 0 auto 0 0; } +.topbar h1 span { color: var(--muted); font-weight: 400; } +.topbar a { color: var(--accent); } +.notice { margin: 8px 16px 0; padding: 8px 12px; border: 1px solid var(--accent); border-radius: 6px; } +.layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; padding: 16px; } +@media (max-width: 900px) { .layout { grid-template-columns: minmax(0, 1fr); } } +h2 { font-size: 0.95rem; margin: 16px 0 8px; } +h2 small, .hint { color: var(--muted); font-weight: 400; font-size: 0.85rem; } +.row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; } +.editor { border: 1px solid var(--border); border-radius: 6px; background: var(--surface); max-height: 40vh; overflow: auto; } +.files { list-style: none; padding: 0; margin: 0 0 8px; } +.files li { display: flex; gap: 6px; align-items: center; padding: 2px 0; } +.files li.selected .file-name { border-color: var(--accent); } +.file-name { flex: 1; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.field { display: grid; gap: 4px; margin: 8px 0; } +.field input, #variables input { font: inherit; padding: 4px 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text); min-width: 0; } +#variables .row input { flex: 1; } +.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 8px; } +.tabs [aria-selected='true'] { border-color: var(--accent); } +#output { margin: 0; padding: 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); overflow: auto; max-height: 75vh; font-size: 0.85rem; } +.problems { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; } +.problems li { padding: 8px 12px; border-left: 3px solid var(--error); background: var(--surface); border-radius: 4px; white-space: pre-wrap; } +.problems li.warning { border-left-color: var(--warning); } +#reference { min-height: 60vh; } diff --git a/web/src/ui/editor.ts b/web/src/ui/editor.ts new file mode 100644 index 0000000..32311ce --- /dev/null +++ b/web/src/ui/editor.ts @@ -0,0 +1,50 @@ +import { EditorView, basicSetup } from 'codemirror'; +import { EditorState } from '@codemirror/state'; +import { yaml } from '@codemirror/lang-yaml'; +import { setDiagnostics } from '@codemirror/lint'; +import { oneDark } from '@codemirror/theme-one-dark'; + +export interface YamlEditor { + /** Replace the document without triggering `onChange`. */ + setDoc(text: string): void; + /** Show (or clear, when `message` is undefined) an error on a 1-based line. */ + showError(line: number | undefined, message: string | undefined): void; +} + +function prefersDark(): boolean { + return globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false; +} + +export function createYamlEditor(parent: HTMLElement, doc: string, onChange: (text: string) => void): YamlEditor { + let silent = false; + const view = new EditorView({ + parent, + state: EditorState.create({ + doc, + extensions: [ + basicSetup, + yaml(), + ...(prefersDark() ? [oneDark] : []), + EditorView.updateListener.of((update) => { + if (update.docChanged && !silent) onChange(update.state.doc.toString()); + }), + ], + }), + }); + + return { + setDoc(text) { + silent = true; + view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text } }); + silent = false; + }, + showError(line, message) { + if (!message) { + view.dispatch(setDiagnostics(view.state, [])); + return; + } + const target = view.state.doc.line(Math.min(Math.max(line ?? 1, 1), view.state.doc.lines)); + view.dispatch(setDiagnostics(view.state, [{ from: target.from, to: target.to, severity: 'error', message }])); + }, + }; +} diff --git a/web/src/ui/reference.ts b/web/src/ui/reference.ts new file mode 100644 index 0000000..f94a6a7 --- /dev/null +++ b/web/src/ui/reference.ts @@ -0,0 +1,20 @@ +interface ReferenceApp { + updateConfiguration(config: { content: string }): void; +} + +let app: ReferenceApp | undefined; +let loading: Promise | undefined; + +/** Render `content` with Scalar. The library is loaded on first use only. */ +export async function showReference(el: HTMLElement, content: string): Promise { + if (app) { + app.updateConfiguration({ content }); + return; + } + loading ??= (async () => { + const { createApiReference } = await import('@scalar/api-reference'); + await import('@scalar/api-reference/style.css'); + app = createApiReference(el, { content }) as unknown as ReferenceApp; + })(); + await loading; +} From dbddf1b02e64a8e0fa1c3481e9212179de5200bd Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 12:06:45 +0200 Subject: [PATCH 12/15] ci: build, test and deploy the web playground to GitHub Pages --- .github/workflows/ci.yml | 27 +++++++++++++++++++ .github/workflows/pages.yml | 52 +++++++++++++++++++++++++++++++++++++ README.md | 13 ++++++++++ 3 files changed, 92 insertions(+) create mode 100644 .github/workflows/pages.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e9b59a..4066fc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,3 +49,30 @@ jobs: run: cargo build --release -p openapi-aggregator - name: Smoke test run: ./target/release/openapi-aggregator --help + + web: + name: Web playground + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@v2 + with: + tool: wasm-pack + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci + - run: npm run wasm + - run: npm run typecheck + - run: npm test + - run: npx playwright install --with-deps chromium + - run: npm run e2e diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..1aaaebb --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,52 @@ +name: Deploy playground + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@v2 + with: + tool: wasm-pack + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci + - run: npm run wasm + - run: npm run build + - uses: actions/upload-pages-artifact@v3 + with: + path: web/dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 5ecfae7..3f67e04 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Aggregate and merge OpenAPI 3.x specifications from multiple sources into a sing Available as both a **Rust library** and a **CLI tool**. +**Try it in your browser:** the [playground](https://includeamin.github.io/openapi-aggregator/) runs the same merge engine compiled to WebAssembly. Add specs, edit the config, and share the result as a link. + ## Features - **Multiple source types** – local YAML files, local JSON files, and HTTP endpoints (with custom headers) @@ -211,6 +213,17 @@ cargo clippy --all-targets -- -D warnings cargo fmt ``` +### Web playground + +```sh +cd web +npm ci +npm run wasm # build the WASM engine (needs wasm-pack + wasm32-unknown-unknown target) +npm run dev # http://localhost:5173/openapi-aggregator/ +npm test # unit tests +npm run e2e # Playwright smoke test +``` + ## License [MIT](LICENSE) From bcfa8fdbda360a7097ba0d72fd672838beb517cc Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 12:13:31 +0200 Subject: [PATCH 13/15] fix(merge): keep per-source security semantics and rewrite scheme and discriminator renames - push a source's top-level security down to its operations when sources disagree - rename security requirement keys and discriminator.mapping values with their components - keep ${VAR} values out of HTTP error messages --- README.md | 2 +- crates/openapi-aggregator-core/src/merge.rs | 239 ++++++++++++++++-- crates/openapi-aggregator/src/source.rs | 8 +- .../tests/integration_test.rs | 31 +++ 4 files changed, 256 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 3f67e04..bf9972d 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ When using `rename`, any `$ref` pointing to a renamed component is rewritten aut Other top-level fields: - `webhooks` (OpenAPI 3.1) are merged like paths. -- Top-level `security` requirements are combined and de-duplicated. +- Top-level `security` stays top-level only when every source declares the same requirements. Otherwise each source's requirements are copied onto its own operations, so no operation gains or loses authentication. Renamed security schemes and `discriminator.mapping` entries are updated along with `$ref`s. - The merged `openapi` version comes from the first source. The CLI prints a warning when sources use different `major.minor` versions (also available via `aggregate_with_report`). - Component types are always emitted in the same order, so the output is stable across runs. diff --git a/crates/openapi-aggregator-core/src/merge.rs b/crates/openapi-aggregator-core/src/merge.rs index 85f935e..dbe1743 100644 --- a/crates/openapi-aggregator-core/src/merge.rs +++ b/crates/openapi-aggregator-core/src/merge.rs @@ -58,6 +58,14 @@ pub fn merge_specs_with_report( } let warnings = version_warnings(&specs); + // Top-level `security` applies to every operation of its own source. Keep it + // top-level only when all sources agree; otherwise push it down per source. + let first_security = specs[0].2.get("security"); + let shared_security = specs + .iter() + .all(|(_, _, spec)| spec.get("security") == first_security); + let top_level_security = shared_security.then(|| first_security.cloned()).flatten(); + // --- info --- let info = build_info(&specs, config.info.as_ref())?; merged.insert("info".into(), info); @@ -69,18 +77,24 @@ pub fn merge_specs_with_report( let mut merged_component_extras = Map::new(); let mut merged_tags: Vec = Vec::new(); let mut merged_servers: Vec = Vec::new(); - let mut merged_security: Vec = Vec::new(); let mut merged_custom_top_level = Map::new(); for (source_name, tag_prefix, mut spec) in specs { let ident = sanitize_ident(&source_name); + if !shared_security { + if let Some(requirements) = spec.get("security").cloned() { + push_down_security(&mut spec, &requirements); + } + } + // Phase 1: detect component conflicts and build a $ref rename map let rename_map = build_rename_map(&ident, &spec, &merged_components, config); // Phase 2: rewrite $refs in the source spec if needed if !rename_map.is_empty() { rewrite_refs(&mut spec, &rename_map); + rewrite_security_requirements(&mut spec, &rename_map); } // Phase 2b: rewrite tag references in operations before merging paths @@ -179,16 +193,7 @@ pub fn merge_specs_with_report( } } - // Phase 3f: merge top-level security requirements (deduplicate) - if let Some(Value::Array(requirements)) = spec.get("security") { - for requirement in requirements { - if !merged_security.contains(requirement) { - merged_security.push(requirement.clone()); - } - } - } - - // Phase 3g: merge non-standard top-level blocks (e.g. vendor extensions) + // Phase 3f: merge non-standard top-level blocks (e.g. vendor extensions) if let Some(root) = spec.as_object() { for (key, value) in root { if is_reserved_top_level_key(key) { @@ -260,8 +265,8 @@ pub fn merge_specs_with_report( merged.insert("servers".into(), Value::Array(merged_servers)); } - if !merged_security.is_empty() { - merged.insert("security".into(), Value::Array(merged_security)); + if let Some(security) = top_level_security { + merged.insert("security".into(), security); } Ok(MergeReport { @@ -396,6 +401,74 @@ fn rewrite_spec_operation_tags(spec: &mut Value, tag_prefix: &str, config: &Merg } } +/// Mutable access to every operation in `paths` and `webhooks`. +fn for_each_operation(spec: &mut Value, mut f: impl FnMut(&mut Map)) { + for section in ["paths", "webhooks"] { + let Some(Value::Object(items)) = spec.get_mut(section) else { + continue; + }; + for path_item in items.values_mut() { + let Some(path_item) = path_item.as_object_mut() else { + continue; + }; + for method in HTTP_METHODS { + if let Some(Value::Object(operation)) = path_item.get_mut(*method) { + f(operation); + } + } + } + } +} + +/// Copy a source's top-level security requirements into each of its operations +/// that doesn't declare its own, so they keep their meaning after merging. +fn push_down_security(spec: &mut Value, requirements: &Value) { + for_each_operation(spec, |operation| { + if !operation.contains_key("security") { + operation.insert("security".into(), requirements.clone()); + } + }); +} + +/// Security requirements name schemes by key, not `$ref`: rename those keys +/// for any renamed `securitySchemes` entry. +fn rewrite_security_requirements(spec: &mut Value, rename_map: &HashMap) { + const PREFIX: &str = "#/components/securitySchemes/"; + let renames: HashMap<&str, &str> = rename_map + .iter() + .filter_map(|(old, new)| Some((old.strip_prefix(PREFIX)?, new.strip_prefix(PREFIX)?))) + .collect(); + if renames.is_empty() { + return; + } + + let rename_requirements = |security: &mut Value| { + let Value::Array(requirements) = security else { + return; + }; + for requirement in requirements { + if let Value::Object(schemes) = requirement { + *schemes = std::mem::take(schemes) + .into_iter() + .map(|(name, scopes)| match renames.get(name.as_str()) { + Some(new) => (new.to_string(), scopes), + None => (name, scopes), + }) + .collect(); + } + } + }; + + if let Some(security) = spec.get_mut("security") { + rename_requirements(security); + } + for_each_operation(spec, |operation| { + if let Some(security) = operation.get_mut("security") { + rename_requirements(security); + } + }); +} + fn component_ref(ctype: &str, name: &str) -> String { format!("#/components/{ctype}/{name}") } @@ -490,6 +563,20 @@ fn rewrite_refs(value: &mut Value, rename_map: &HashMap) { map.insert("$ref".into(), Value::String(new_ref)); } + if let Some(Value::Object(mapping)) = map + .get_mut("discriminator") + .and_then(|d| d.get_mut("mapping")) + { + for target in mapping.values_mut() { + if let Some(new_target) = target + .as_str() + .and_then(|t| renamed_mapping_target(t, rename_map)) + { + *target = Value::String(new_target); + } + } + } + for v in map.values_mut() { rewrite_refs(v, rename_map); } @@ -503,6 +590,16 @@ fn rewrite_refs(value: &mut Value, rename_map: &HashMap) { } } +/// A discriminator mapping value is either a `$ref`-style string or a bare +/// schema name (`Dog`, meaning `#/components/schemas/Dog`). +fn renamed_mapping_target(target: &str, rename_map: &HashMap) -> Option { + if target.contains('/') { + return renamed_ref(target, rename_map); + } + let new_ref = rename_map.get(&component_ref("schemas", target))?; + new_ref.rsplit('/').next().map(str::to_string) +} + /// `#/components/schemas/Pet/properties/id` → `#/components/schemas/b_Pet/properties/id` fn renamed_ref(reference: &str, rename_map: &HashMap) -> Option { let component_end = reference @@ -1291,11 +1388,11 @@ mod tests { } #[test] - fn top_level_security_is_unioned() { - let mut a = spec_with(json!({}), json!({})); + fn identical_top_level_security_is_kept() { + let mut a = spec_with(json!({ "/a": { "get": { "summary": "a" } } }), json!({})); a["security"] = json!([{ "bearer": [] }]); - let mut b = spec_with(json!({}), json!({})); - b["security"] = json!([{ "apiKey": [] }, { "bearer": [] }]); + let mut b = spec_with(json!({ "/b": { "get": { "summary": "b" } } }), json!({})); + b["security"] = json!([{ "bearer": [] }]); let merged = merge_specs( vec![("a".into(), "a".into(), a), ("b".into(), "b".into(), b)], @@ -1303,10 +1400,114 @@ mod tests { ) .unwrap(); + assert_eq!(merged["security"], json!([{ "bearer": [] }])); + assert!(merged["paths"]["/a"]["get"].get("security").is_none()); + } + + #[test] + fn differing_top_level_security_is_pushed_down_to_operations() { + let mut a = spec_with(json!({ "/a": { "get": { "summary": "a" } } }), json!({})); + a["security"] = json!([{ "oauth": ["read"] }]); + let mut b = spec_with( + json!({ + "/b": { "get": { "summary": "b" } }, + "/public": { "get": { "summary": "p", "security": [] } } + }), + json!({}), + ); + b["security"] = json!([{ "apiKey": [] }]); + let c = spec_with(json!({ "/c": { "get": { "summary": "c" } } }), json!({})); + + let merged = merge_specs( + vec![ + ("a".into(), "a".into(), a), + ("b".into(), "b".into(), b), + ("c".into(), "c".into(), c), + ], + &MergeConfig::default(), + ) + .unwrap(); + + assert!(merged.get("security").is_none(), "{merged:#}"); + let paths = &merged["paths"]; + assert_eq!( + paths["/a"]["get"]["security"], + json!([{ "oauth": ["read"] }]) + ); + assert_eq!(paths["/b"]["get"]["security"], json!([{ "apiKey": [] }])); + assert_eq!(paths["/public"]["get"]["security"], json!([])); + assert!(paths["/c"]["get"].get("security").is_none()); + } + + #[test] + fn renamed_security_scheme_updates_requirements() { + let a = spec_with( + json!({}), + json!({ "securitySchemes": { "bearer": { "type": "http", "scheme": "bearer" } } }), + ); + let mut b = spec_with( + json!({ "/b": { "get": { "summary": "b", "security": [{ "bearer": [] }] } } }), + json!({ "securitySchemes": { + "bearer": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" } + }}), + ); + b["security"] = json!([{ "bearer": [] }]); + b["paths"]["/b2"] = json!({ "get": { "summary": "b2" } }); + let config = MergeConfig { + conflict_strategy: ConflictStrategy::Rename, + ..Default::default() + }; + + let merged = merge_specs( + vec![("a".into(), "a".into(), a), ("b".into(), "b".into(), b)], + &config, + ) + .unwrap(); + + assert!(merged["components"]["securitySchemes"]["b_bearer"].is_object()); assert_eq!( - merged["security"], - json!([{ "bearer": [] }, { "apiKey": [] }]) + merged["paths"]["/b"]["get"]["security"], + json!([{ "b_bearer": [] }]) ); + assert_eq!( + merged["paths"]["/b2"]["get"]["security"], + json!([{ "b_bearer": [] }]) + ); + } + + #[test] + fn renamed_schema_updates_discriminator_mapping() { + let a = spec_with( + json!({}), + json!({ "schemas": { "Dog": { "type": "object" } } }), + ); + let b = spec_with( + json!({}), + json!({ "schemas": { + "Dog": { "type": "object", "properties": { "bark": { "type": "string" } } }, + "Pet": { + "oneOf": [{ "$ref": "#/components/schemas/Dog" }], + "discriminator": { + "propertyName": "kind", + "mapping": { "dog": "#/components/schemas/Dog", "doggo": "Dog" } + } + } + }}), + ); + let config = MergeConfig { + conflict_strategy: ConflictStrategy::Rename, + ..Default::default() + }; + + let merged = merge_specs( + vec![("a".into(), "a".into(), a), ("b".into(), "b".into(), b)], + &config, + ) + .unwrap(); + + let mapping = &merged["components"]["schemas"]["Pet"]["discriminator"]["mapping"]; + assert_eq!(mapping["dog"], "#/components/schemas/b_Dog"); + assert_eq!(mapping["doggo"], "b_Dog"); } #[test] diff --git a/crates/openapi-aggregator/src/source.rs b/crates/openapi-aggregator/src/source.rs index 6fca942..2cfe7fa 100644 --- a/crates/openapi-aggregator/src/source.rs +++ b/crates/openapi-aggregator/src/source.rs @@ -34,12 +34,12 @@ pub(crate) async fn load_source_with_client( })? } Source::Http { url, headers, .. } => { - let url = expand_env(url)?; - let http_err = |e| Error::HttpRequest { + // Errors carry the unexpanded URL so `${SECRET}` values never reach logs. + let http_err = |e: reqwest::Error| Error::HttpRequest { url: url.clone(), - source: e, + source: e.without_url(), }; - let mut request = client.get(&url); + let mut request = client.get(expand_env(url)?); for (key, value) in headers { request = request.header(key, expand_env(value)?); } diff --git a/crates/openapi-aggregator/tests/integration_test.rs b/crates/openapi-aggregator/tests/integration_test.rs index e80f8dd..9ec2add 100644 --- a/crates/openapi-aggregator/tests/integration_test.rs +++ b/crates/openapi-aggregator/tests/integration_test.rs @@ -410,3 +410,34 @@ fn cli_format_flag_overrides_config() { assert!(output.status.success()); assert!(String::from_utf8_lossy(&output.stdout).starts_with("openapi:")); } + +#[tokio::test] +async fn http_errors_do_not_leak_expanded_env_secrets() { + std::env::set_var("OPENAPI_AGGREGATOR_TEST_SECRET", "s3cr3t-value"); + + let mut server = mockito::Server::new_async().await; + let _mock = server + .mock("GET", mockito::Matcher::Any) + .with_status(500) + .create_async() + .await; + + let source = Source::Http { + name: Some("secret".into()), + url: format!( + "{}/spec?token=${{OPENAPI_AGGREGATOR_TEST_SECRET}}", + server.url() + ), + headers: Default::default(), + tag_prefix: None, + additional_blocks: None, + }; + + let err = load_source(&source).await.unwrap_err(); + let message = format!("{err} {:?}", std::error::Error::source(&err)); + assert!(!message.contains("s3cr3t-value"), "{message}"); + assert!( + message.contains("${OPENAPI_AGGREGATOR_TEST_SECRET}"), + "{message}" + ); +} From 405a13b041e0defaf737bc09a4c237aeeb483636 Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 12:15:32 +0200 Subject: [PATCH 14/15] fix(web): gate remote fetches from shared configs and harden workspace edits - configs opened from a share link don't fetch URL sources until the user allows it - error messages show URL templates, never substituted variable values - New file and Rename no longer overwrite or shadow an existing file - the API reference shows the latest spec after its first load and retries a failed load --- web/e2e/smoke.spec.ts | 31 +++++++++++++++++++++++ web/index.html | 4 +++ web/src/app.ts | 47 ++++++++++++++++++++++++++++++---- web/src/resolve.ts | 20 +++++++++++++-- web/src/style.css | 1 + web/src/types.ts | 2 +- web/src/ui/reference.ts | 30 +++++++++++++++------- web/src/workspace.ts | 19 ++++++++++++++ web/tests/reference.test.ts | 50 +++++++++++++++++++++++++++++++++++++ web/tests/resolve.test.ts | 31 ++++++++++++++++++++++- web/tests/workspace.test.ts | 26 ++++++++++++++++++- 11 files changed, 242 insertions(+), 19 deletions(-) create mode 100644 web/tests/reference.test.ts diff --git a/web/e2e/smoke.spec.ts b/web/e2e/smoke.spec.ts index 1e044bd..78427b9 100644 --- a/web/e2e/smoke.spec.ts +++ b/web/e2e/smoke.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from '@playwright/test'; +import { encodeShare } from '../src/share'; test('default example merges, reports no problems, and renders the reference', async ({ page }) => { await page.goto('./'); @@ -27,3 +28,33 @@ test('an invalid share link falls back to the default example with a notice', as await expect(page.locator('#notice')).toContainText('share link'); await expect(page.locator('#output')).toContainText('catalog_Item'); }); + +test('a shared config does not fetch remote URLs until the user allows it', async ({ page }) => { + let hits = 0; + await page.route('https://remote.test/**', (route) => { + hits += 1; + return route.fulfill({ + status: 200, + headers: { 'access-control-allow-origin': '*' }, + body: "openapi: 3.0.3\ninfo: {title: Remote API, version: '1'}\npaths: {}\n", + }); + }); + const encoded = await encodeShare({ + v: 1, + config: 'sources:\n - name: remote\n url: https://remote.test/spec?t=${TOKEN}\n', + files: [], + }); + + await page.goto(`./#s=${encoded}`); + await expect(page.locator('#consent')).toBeVisible(); + await expect(page.locator('#consent')).toContainText('remote.test'); + expect(hits).toBe(0); + + await page.evaluate(() => + localStorage.setItem('openapi-aggregator.settings', JSON.stringify({ proxy: '', variables: { TOKEN: 't' } })), + ); + await page.reload(); + await page.getByRole('button', { name: 'Allow fetching' }).click(); + await expect(page.locator('#output')).toContainText('title: Remote API'); + expect(hits).toBe(1); +}); diff --git a/web/index.html b/web/index.html index 57b6904..4d8f0f9 100644 --- a/web/index.html +++ b/web/index.html @@ -14,6 +14,10 @@

    openapi-aggregator playground

    GitHub +

    Workspace

    diff --git a/web/src/app.ts b/web/src/app.ts index e35bf20..b523a74 100644 --- a/web/src/app.ts +++ b/web/src/app.ts @@ -1,13 +1,13 @@ import { aggregate, parseConfig } from './engine'; import { DEFAULT_EXAMPLE_ID, EXAMPLES, findExample, type Example } from './examples'; import { latestOnly, runPipeline, type PipelineResult } from './pipeline'; -import { errorMessage, fetchText, memoizeLoader } from './resolve'; +import { errorMessage, fetchText, guardLoader, memoizeLoader } from './resolve'; import { loadSettings, saveSettings } from './settings'; import { decodeShare, encodeShare, MAX_SHARE_LENGTH, shareParam } from './share'; import type { Problem, WorkspaceFile } from './types'; import { createYamlEditor } from './ui/editor'; import { showReference } from './ui/reference'; -import { fileNameFromUrl, upsertFile } from './workspace'; +import { fileNameFromUrl, isNameTaken, uniqueFileName, upsertFile } from './workspace'; const NEW_FILE = `openapi: 3.0.3 info: @@ -38,7 +38,13 @@ export async function startApp(): Promise { let timer: ReturnType | undefined; let noticeTimer: ReturnType | undefined; - const newLoader = () => memoizeLoader((url, headers) => fetchText(url, headers, settings)); + // Configs from share links may not fetch remote URLs until the user allows it. + let remoteAllowed = true; + const newLoader = () => + guardLoader( + memoizeLoader((url, headers) => fetchText(url, headers, settings)), + () => remoteAllowed, + ); let load = newLoader(); const pipeline = latestOnly(() => runPipeline(state.config, state.files, { parseConfig, aggregate, load })); @@ -73,6 +79,7 @@ export async function startApp(): Promise { result = next; $('#output').textContent = result.text ?? 'No output. See the Problems tab.'; renderProblems(result.problems); + renderConsent(); const configError = result.problems.find((p) => p.kind === 'config'); configEditor.showError(configError?.line, configError?.message); if (activeTab === 'reference' && result.text) void showReference($('#reference'), result.text); @@ -90,6 +97,27 @@ export async function startApp(): Promise { ); } + function renderConsent(): void { + const blocked = !remoteAllowed && result.problems.some((p) => p.kind === 'blocked'); + $('#consent').hidden = !blocked; + if (!blocked) return; + let hosts: string[] = []; + try { + hosts = parseConfig(state.config).sources.flatMap((s) => { + try { + return s.url ? [new URL(s.url).host] : []; + } catch { + return [s.url ?? '']; + } + }); + } catch { + // config error: nothing to list + } + $('#consent-text').textContent = + `This shared config fetches from ${[...new Set(hosts)].join(', ')}. ` + + 'Your saved variables would be sent there. Review the config first.'; + } + function renderFiles(): void { $('#files').replaceChildren( ...state.files.map((file, index) => { @@ -99,7 +127,9 @@ export async function startApp(): Promise { name.className = 'file-name'; const rename = button('Rename', () => { const next = prompt('File name (as referenced by `path:` in the config)', file.name)?.trim(); - if (next) { + if (next && isNameTaken(state.files, index, next)) { + notice(`A file named '${next}' already exists.`); + } else if (next) { file.name = next; renderFiles(); schedule(); @@ -137,6 +167,7 @@ export async function startApp(): Promise { } function loadExample(example: Example): void { + remoteAllowed = true; $('#examples').value = example.id; loadWorkspace(example.config, example.files); } @@ -152,7 +183,7 @@ export async function startApp(): Promise { // --- workspace actions --- $('#add-file').onclick = () => { const name = prompt('File name', 'specs/new.yaml')?.trim(); - if (name) addFile({ name, content: NEW_FILE }); + if (name) addFile({ name: uniqueFileName(state.files, name), content: NEW_FILE }); }; $('#upload').onchange = async (event) => { const input = event.target as HTMLInputElement; @@ -230,6 +261,11 @@ export async function startApp(): Promise { if (activeTab === 'reference' && result.text) void showReference($('#reference'), result.text); }; } + $('#allow-fetch').onclick = () => { + remoteAllowed = true; + $('#consent').hidden = true; + void run(); + }; $('#copy-config').onclick = async () => { await navigator.clipboard.writeText(state.config); notice('Config copied. Run it with: openapi-aggregator -c openapi-aggregator.yaml'); @@ -264,6 +300,7 @@ export async function startApp(): Promise { if (shared) { try { const payload = await decodeShare(shared); + remoteAllowed = false; select.value = ''; loadWorkspace(payload.config, payload.files); return; diff --git a/web/src/resolve.ts b/web/src/resolve.ts index 48f31df..2de1ad1 100644 --- a/web/src/resolve.ts +++ b/web/src/resolve.ts @@ -36,22 +36,38 @@ export async function fetchText( const finalHeaders = Object.fromEntries( Object.entries(headers).map(([key, value]) => [key, substituteVars(value, settings.variables)]), ); + // Messages use the URL template (`url`), so variable values never appear on screen. let response: Response; try { response = await fetchFn(settings.proxy + target, { headers: finalHeaders }); } catch { throw new SourceError( 'cors', - `Could not fetch ${target}. The server is unreachable or does not allow cross-origin requests (CORS). ` + + `Could not fetch ${url}. The server is unreachable or does not allow cross-origin requests (CORS). ` + 'Paste or upload the spec instead, or set a CORS proxy under Settings.', ); } if (!response.ok) { - throw new SourceError('http', `${target} returned HTTP ${response.status}.`); + throw new SourceError('http', `${url} returned HTTP ${response.status}.`); } return response.text(); } +/** + * Refuse remote loads until `isAllowed()` returns true. Used for configs opened from a + * share link, so a stranger's config can't send this browser's variables to their server. + */ +export function guardLoader(load: Loader, isAllowed: () => boolean): Loader { + return (url, headers) => { + if (!isAllowed()) { + return Promise.reject( + new SourceError('blocked', `Not fetched: ${url}. This config came from a share link; review it, then click "Allow fetching".`), + ); + } + return load(url, headers); + }; +} + /** Cache successful loads per (url, headers); failed loads are retried next time. */ export function memoizeLoader(load: Loader): Loader { const cache = new Map>(); diff --git a/web/src/style.css b/web/src/style.css index 3b106a1..a43496f 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -18,6 +18,7 @@ button, .button { font: inherit; padding: 4px 10px; border: 1px solid var(--bord .topbar h1 span { color: var(--muted); font-weight: 400; } .topbar a { color: var(--accent); } .notice { margin: 8px 16px 0; padding: 8px 12px; border: 1px solid var(--accent); border-radius: 6px; } +.consent { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; border-color: var(--warning); } .layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; padding: 16px; } @media (max-width: 900px) { .layout { grid-template-columns: minmax(0, 1fr); } } h2 { font-size: 0.95rem; margin: 16px 0 8px; } diff --git a/web/src/types.ts b/web/src/types.ts index fde3ab3..46c28bd 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -26,7 +26,7 @@ export interface AggregateOutput { warnings: string[]; } -export type ProblemKind = 'config' | 'missing-file' | 'variable' | 'cors' | 'http' | 'merge'; +export type ProblemKind = 'config' | 'missing-file' | 'variable' | 'cors' | 'http' | 'merge' | 'blocked'; export interface Problem { level: 'error' | 'warning'; diff --git a/web/src/ui/reference.ts b/web/src/ui/reference.ts index f94a6a7..c958774 100644 --- a/web/src/ui/reference.ts +++ b/web/src/ui/reference.ts @@ -4,17 +4,29 @@ interface ReferenceApp { let app: ReferenceApp | undefined; let loading: Promise | undefined; +let latest = ''; +let shown = ''; + +async function load(el: HTMLElement): Promise { + const { createApiReference } = await import('@scalar/api-reference'); + await import('@scalar/api-reference/style.css'); + app = createApiReference(el, { content: latest }) as unknown as ReferenceApp; + shown = latest; +} /** Render `content` with Scalar. The library is loaded on first use only. */ export async function showReference(el: HTMLElement, content: string): Promise { - if (app) { - app.updateConfiguration({ content }); - return; + latest = content; + if (!app) { + loading ??= load(el).catch((error: unknown) => { + loading = undefined; // let the next call retry + throw error; + }); + await loading; + } + // Content may have changed while the library was loading. + if (app && shown !== latest) { + app.updateConfiguration({ content: latest }); + shown = latest; } - loading ??= (async () => { - const { createApiReference } = await import('@scalar/api-reference'); - await import('@scalar/api-reference/style.css'); - app = createApiReference(el, { content }) as unknown as ReferenceApp; - })(); - await loading; } diff --git a/web/src/workspace.ts b/web/src/workspace.ts index b636f3c..6947180 100644 --- a/web/src/workspace.ts +++ b/web/src/workspace.ts @@ -17,6 +17,25 @@ export function upsertFile(files: WorkspaceFile[], file: WorkspaceFile): number return files.length - 1; } +/** `name` if no file uses that key yet, otherwise `name-2`, `name-3`, … (before the extension). */ +export function uniqueFileName(files: WorkspaceFile[], name: string): string { + const key = workspaceKey(name); + const taken = new Set(files.map((f) => workspaceKey(f.name))); + if (!taken.has(key)) return key; + const dot = key.lastIndexOf('.'); + const slash = key.lastIndexOf('/'); + const [stem, ext] = dot > slash ? [key.slice(0, dot), key.slice(dot)] : [key, '']; + let n = 2; + while (taken.has(`${stem}-${n}${ext}`)) n += 1; + return `${stem}-${n}${ext}`; +} + +/** Whether a file other than `files[index]` already uses `name`'s key. */ +export function isNameTaken(files: WorkspaceFile[], index: number, name: string): boolean { + const key = workspaceKey(name); + return files.some((f, i) => i !== index && workspaceKey(f.name) === key); +} + export function fileNameFromUrl(url: string): string { try { const last = new URL(url).pathname.split('/').filter(Boolean).pop(); diff --git a/web/tests/reference.test.ts b/web/tests/reference.test.ts new file mode 100644 index 0000000..18dd5e5 --- /dev/null +++ b/web/tests/reference.test.ts @@ -0,0 +1,50 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const scalar = vi.hoisted(() => ({ + created: [] as string[], + updates: [] as string[], + failNext: false, +})); + +vi.mock('@scalar/api-reference', () => ({ + createApiReference: (_el: unknown, config: { content: string }) => { + if (scalar.failNext) { + scalar.failNext = false; + throw new Error('offline'); + } + scalar.created.push(config.content); + return { updateConfiguration: (c: { content: string }) => scalar.updates.push(c.content) }; + }, +})); +vi.mock('@scalar/api-reference/style.css', () => ({})); + +const el = {} as HTMLElement; + +async function freshModule() { + vi.resetModules(); + return import('../src/ui/reference'); +} + +beforeEach(() => { + scalar.created = []; + scalar.updates = []; + scalar.failNext = false; +}); + +describe('showReference', () => { + it('shows the latest content when calls arrive during the first load', async () => { + const { showReference } = await freshModule(); + await Promise.all([showReference(el, 'old'), showReference(el, 'new')]); + const shown = scalar.updates.at(-1) ?? scalar.created.at(-1); + expect(shown).toBe('new'); + expect(scalar.created).toHaveLength(1); + }); + + it('retries after a failed load', async () => { + const { showReference } = await freshModule(); + scalar.failNext = true; + await expect(showReference(el, 'a')).rejects.toThrow('offline'); + await showReference(el, 'b'); + expect(scalar.created).toEqual(['b']); + }); +}); diff --git a/web/tests/resolve.test.ts b/web/tests/resolve.test.ts index e6f4336..7a66c28 100644 --- a/web/tests/resolve.test.ts +++ b/web/tests/resolve.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { fetchText, memoizeLoader, resolveSources, substituteVars } from '../src/resolve'; +import { fetchText, guardLoader, memoizeLoader, resolveSources, substituteVars } from '../src/resolve'; import type { Settings, SourceSummary } from '../src/types'; const settings = (over: Partial = {}): Settings => ({ proxy: '', variables: {}, ...over }); @@ -90,3 +90,32 @@ describe('resolveSources', () => { expect(result.problems[0].message).toContain("No workspace file named 'missing.yaml'"); }); }); + +describe('fetchText error messages', () => { + it('show the URL template, never substituted variable values', async () => { + const vars = settings({ variables: { T: 'top-secret' } }); + const network = async () => { + throw new TypeError('Failed to fetch'); + }; + const notFound = async () => new Response('', { status: 404 }); + for (const fetchFn of [network, notFound]) { + const error = (await fetchText('https://api.test/spec?t=${T}', {}, vars, fetchFn).catch((e: unknown) => e)) as Error; + expect(error.message).not.toContain('top-secret'); + expect(error.message).toContain('${T}'); + } + }); +}); + +describe('guardLoader', () => { + it('blocks remote loads until allowed, without calling the inner loader', async () => { + let allowed = false; + const inner = vi.fn(async () => 'spec'); + const load = guardLoader(inner, () => allowed); + + await expect(load('https://x.test/a', {})).rejects.toMatchObject({ kind: 'blocked' }); + expect(inner).not.toHaveBeenCalled(); + + allowed = true; + expect(await load('https://x.test/a', {})).toBe('spec'); + }); +}); diff --git a/web/tests/workspace.test.ts b/web/tests/workspace.test.ts index dbc33ab..0008a83 100644 --- a/web/tests/workspace.test.ts +++ b/web/tests/workspace.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { fileNameFromUrl, upsertFile, workspaceKey } from '../src/workspace'; +import { fileNameFromUrl, isNameTaken, uniqueFileName, upsertFile, workspaceKey } from '../src/workspace'; describe('workspaceKey', () => { it('strips leading ./ segments', () => { @@ -33,3 +33,27 @@ describe('fileNameFromUrl', () => { expect(fileNameFromUrl('not a url')).toBe('imported.yaml'); }); }); + +describe('uniqueFileName', () => { + it('keeps a free name and suffixes a taken one before the extension', () => { + const files = [ + { name: 'specs/new.yaml', content: '' }, + { name: 'specs/new-2.yaml', content: '' }, + ]; + expect(uniqueFileName(files, 'specs/other.yaml')).toBe('specs/other.yaml'); + expect(uniqueFileName(files, './specs/new.yaml')).toBe('specs/new-3.yaml'); + expect(uniqueFileName([{ name: 'README', content: '' }], 'README')).toBe('README-2'); + }); +}); + +describe('isNameTaken', () => { + it('detects another file with the same key, ignoring the file being renamed', () => { + const files = [ + { name: 'specs/a.yaml', content: '' }, + { name: 'specs/b.yaml', content: '' }, + ]; + expect(isNameTaken(files, 1, './specs/a.yaml')).toBe(true); + expect(isNameTaken(files, 0, 'specs/a.yaml')).toBe(false); + expect(isNameTaken(files, 1, 'specs/c.yaml')).toBe(false); + }); +}); From baf74912471c4ebdcefeb2f7d0bfb9eaa123dd22 Mon Sep 17 00:00:00 2001 From: includeamin Date: Thu, 1 Oct 2026 12:58:49 +0200 Subject: [PATCH 15/15] chore: sync Cargo.lock crate versions to 0.6.1 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e427981..37b7137 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -721,7 +721,7 @@ dependencies = [ [[package]] name = "openapi-aggregator-core" -version = "0.6.0" +version = "0.6.1" dependencies = [ "serde", "serde_json", @@ -731,7 +731,7 @@ dependencies = [ [[package]] name = "openapi-aggregator-wasm" -version = "0.6.0" +version = "0.6.1" dependencies = [ "openapi-aggregator-core", "serde",