From e197521774a40772c629fd3d37d40d532be6c965 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Mar 2026 14:54:59 +0000
Subject: [PATCH 01/76] Replace direct ryu usage with zmij
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
Cargo.lock | 4 ++--
lib/executor/Cargo.toml | 2 +-
lib/executor/src/json_writer.rs | 20 +++++++++++++++++++-
lib/graphql-tools/Cargo.toml | 2 +-
lib/graphql-tools/src/parser/query/minify.rs | 12 ++++++------
5 files changed, 29 insertions(+), 11 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 617ca5dac..7e9941ef2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2104,12 +2104,12 @@ dependencies = [
"itoa",
"lazy_static",
"pretty_assertions",
- "ryu",
"serde",
"serde_json",
"serde_with",
"thiserror 2.0.18",
"xxhash-rust",
+ "zmij",
]
[[package]]
@@ -2440,7 +2440,6 @@ dependencies = [
"ntex",
"regex-automata",
"rustls",
- "ryu",
"serde",
"serde_json",
"sonic-rs",
@@ -2451,6 +2450,7 @@ dependencies = [
"tokio",
"tracing",
"xxhash-rust",
+ "zmij",
]
[[package]]
diff --git a/lib/executor/Cargo.toml b/lib/executor/Cargo.toml
index 4f40d159f..8287a02bd 100644
--- a/lib/executor/Cargo.toml
+++ b/lib/executor/Cargo.toml
@@ -49,7 +49,7 @@ hyper-util = { version = "0.1.16", features = [
"tokio",
] }
itoa = "1.0.15"
-ryu = "1.0.20"
+zmij = "1.0.21"
indexmap = "2.10.0"
bumpalo = "3.19.0"
sonic-simd = "0.1.2"
diff --git a/lib/executor/src/json_writer.rs b/lib/executor/src/json_writer.rs
index 141fe70aa..9605c313a 100644
--- a/lib/executor/src/json_writer.rs
+++ b/lib/executor/src/json_writer.rs
@@ -16,7 +16,7 @@ pub fn write_f64(writer: &mut Vec, value: f64) {
return;
}
- let mut buffer = ryu::Buffer::new();
+ let mut buffer = zmij::Buffer::new();
let s = buffer.format_finite(value);
writer.put(s.as_bytes())
}
@@ -518,6 +518,24 @@ fn format_string(input_str: &str, writer: &mut Vec, need_quote: bool) {
mod test {
use super::*;
+ #[test]
+ fn test_write_f64_finite() {
+ let mut dst: Vec = Vec::new();
+
+ write_f64(&mut dst, 123.5);
+
+ assert_eq!(dst.as_slice(), b"123.5");
+ }
+
+ #[test]
+ fn test_write_f64_non_finite_is_null() {
+ let mut dst: Vec = Vec::new();
+
+ write_f64(&mut dst, f64::NAN);
+
+ assert_eq!(dst.as_slice(), b"null");
+ }
+
#[test]
fn test_quote() {
let mut dst: Vec = Vec::with_capacity(1000);
diff --git a/lib/graphql-tools/Cargo.toml b/lib/graphql-tools/Cargo.toml
index f429840b4..1e5362995 100644
--- a/lib/graphql-tools/Cargo.toml
+++ b/lib/graphql-tools/Cargo.toml
@@ -21,7 +21,7 @@ serde_with = "3.0.0"
combine = { workspace = true }
thiserror = { workspace = true }
itoa = "1.0.17"
-ryu = "1.0.22"
+zmij = "1.0.21"
xxhash-rust = { workspace = true }
[dev-dependencies]
diff --git a/lib/graphql-tools/src/parser/query/minify.rs b/lib/graphql-tools/src/parser/query/minify.rs
index 0e3e4f04c..f104f4e43 100644
--- a/lib/graphql-tools/src/parser/query/minify.rs
+++ b/lib/graphql-tools/src/parser/query/minify.rs
@@ -54,7 +54,7 @@ pub fn minify_query_document<'a, T: Text<'a>>(doc: &Document<'a, T>) -> String {
///
/// Key optimizations:
/// - Direct buffer writing avoids intermediate allocations
-/// - Reusable buffers for number formatting (itoa, ryu) reduce allocation overhead
+/// - Reusable buffers for number formatting (itoa, zmij) reduce allocation overhead
/// - Tracking `last_was_non_punctuator` allows spacing without post-processing
struct Minifier {
/// Accumulates the minified output as we traverse the AST
@@ -68,8 +68,8 @@ struct Minifier {
/// Reusable buffer for converting integers to strings using the `itoa` crate,
/// that's optimized for fast, allocation-free integer formatting.
int_buffer: itoa::Buffer,
- /// Reusable buffer for converting floats to strings using the `ryu` crate.
- floats_buffer: ryu::Buffer,
+ /// Reusable buffer for converting floats to strings using the `zmij` crate.
+ floats_buffer: zmij::Buffer,
}
impl Minifier {
@@ -81,7 +81,7 @@ impl Minifier {
last_was_non_punctuator: false,
block_indent: 2,
int_buffer: itoa::Buffer::new(),
- floats_buffer: ryu::Buffer::new(),
+ floats_buffer: zmij::Buffer::new(),
}
}
@@ -422,8 +422,8 @@ impl Minifier {
self.last_was_non_punctuator = true;
}
Value::Float(f) => {
- // Use ryu's format method for fast, accurate float-to-string conversion.
- // ryu produces the shortest decimal representation that correctly round-trips,
+ // Use zmij's format method for fast, accurate float-to-string conversion.
+ // zmij produces the shortest decimal representation that correctly round-trips,
// which is more efficient than using Display or other methods.
let s = self.floats_buffer.format(*f);
if self.last_was_non_punctuator {
From 01d5be7ad7f409611212bc62044f31db7533ce89 Mon Sep 17 00:00:00 2001
From: Kamil Kisiela
Date: Tue, 24 Mar 2026 12:14:24 +0100
Subject: [PATCH 02/76] Add router-level in-flight GraphQL request
deduplication (#849)
- Add inbound router request deduplication for concurrent identical
GraphQL requests, with atomic in-flight key claiming and leader/joiner
response sharing.
- Feature behind `traffic_shaping.router.dedupe.enabled` (default: false
- but I'm open to change it. I made it 'all' and disabled by default as
anything that makes a request "unique" affects the request fingerprint
and therefore deduplication), and include current schema checksum in the
dedupe fingerprint to prevent cross-schema sharing.
- Restrict response sharing to `query` operations only
```yaml
traffic_shaping:
router:
dedupe:
enabled: true # default
headers: all # default
```
---
The deduplication key is built out of:
- request method and path
- selected request headers (based on dedupe header policy - see examples
below)
- normalized operation hash
- GraphQL variables hash
- GraphQL extensions hash
- schema checksum
Header policy: include all headers in the req fingerprint
```yaml
headers: all
```
Header policy: do not include headers in the req fingerprint
```yaml
headers: none
```
Header policy: include only some headers in the req fingerprint
```yaml
headers:
include:
- authorization
- cookie
```
Docs: https://github.com/graphql-hive/docs/pull/62
---
.changeset/router-inflight-dedupe.md | 48 ++
Cargo.lock | 1 +
bench/configs/default.config.yaml | 4 +
bench/configs/no-deduplication.config.yaml | 3 +
bin/router/benches/router_benches.rs | 8 +
bin/router/src/pipeline/authorization/mod.rs | 10 +-
.../src/pipeline/authorization/tests.rs | 17 +-
bin/router/src/pipeline/mod.rs | 385 +++++++++---
bin/router/src/pipeline/normalize.rs | 49 +-
bin/router/src/pipeline/query_plan.rs | 17 +-
bin/router/src/pipeline/usage_reporting.rs | 17 +-
bin/router/src/shared_state.rs | 120 +++-
docs/README.md | 33 +-
e2e/src/http.rs | 573 ++++++++++++++++++
lib/executor/src/executors/dedupe.rs | 5 +-
lib/executor/src/executors/http.rs | 36 +-
lib/executor/src/executors/map.rs | 11 +-
.../src/plugins/hooks/on_supergraph_load.rs | 7 +
lib/internal/Cargo.toml | 1 +
lib/internal/src/inflight.rs | 147 +++++
lib/internal/src/lib.rs | 1 +
lib/router-config/src/traffic_shaping.rs | 62 ++
22 files changed, 1400 insertions(+), 155 deletions(-)
create mode 100644 .changeset/router-inflight-dedupe.md
create mode 100644 lib/internal/src/inflight.rs
diff --git a/.changeset/router-inflight-dedupe.md b/.changeset/router-inflight-dedupe.md
new file mode 100644
index 000000000..d2d3acf3b
--- /dev/null
+++ b/.changeset/router-inflight-dedupe.md
@@ -0,0 +1,48 @@
+---
+hive-router: minor
+hive-router-config: minor
+hive-router-internal: minor
+hive-router-plan-executor: minor
+---
+
+# Add router-level in-flight request deduplication for GraphQL queries
+
+The router now supports deduplicating identical incoming GraphQL query requests while they are in flight, so concurrent duplicates can share one execution result.
+
+## Configuration
+
+A new router traffic-shaping section is available:
+
+- `traffic_shaping.router.dedupe.enabled` (default: `false`)
+- `traffic_shaping.router.dedupe.headers` as `all`, `none`, or `{ include: [...] }` (default: `all`)
+
+Supported header config shapes:
+
+```yaml
+headers: all
+```
+
+```yaml
+headers: none
+```
+
+```yaml
+headers:
+ include:
+ - authorization
+ - cookie
+```
+
+Header names are validated and normalized as standard HTTP header names.
+
+## Deduplication key behavior
+
+The router dedupe fingerprint includes:
+
+- request method and path
+- selected request headers (based on dedupe header policy)
+- normalized operation hash
+- GraphQL variables hash
+- schema checksum
+- GraphQL extensions
+
diff --git a/Cargo.lock b/Cargo.lock
index 7e9941ef2..937f2c062 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2382,6 +2382,7 @@ dependencies = [
"async-trait",
"bytes",
"criterion",
+ "dashmap",
"futures",
"hive-router-config",
"http",
diff --git a/bench/configs/default.config.yaml b/bench/configs/default.config.yaml
index 26a68560a..1c4bba3e1 100644
--- a/bench/configs/default.config.yaml
+++ b/bench/configs/default.config.yaml
@@ -4,3 +4,7 @@ supergraph:
path: ../supergraph.graphql
log:
level: info
+traffic_shaping:
+ router:
+ dedupe:
+ enabled: true # it's opt-in
diff --git a/bench/configs/no-deduplication.config.yaml b/bench/configs/no-deduplication.config.yaml
index 28d80615c..9b33e32d4 100644
--- a/bench/configs/no-deduplication.config.yaml
+++ b/bench/configs/no-deduplication.config.yaml
@@ -7,3 +7,6 @@ log:
traffic_shaping:
all:
dedupe_enabled: false
+ router:
+ dedupe:
+ enabled: false
diff --git a/bin/router/benches/router_benches.rs b/bin/router/benches/router_benches.rs
index b29ff053d..55fd0b5a8 100644
--- a/bin/router/benches/router_benches.rs
+++ b/bin/router/benches/router_benches.rs
@@ -1,5 +1,6 @@
use criterion::{criterion_group, criterion_main, Criterion};
use hive_router::pipeline::authorization::metadata::AuthorizationMetadataExt;
+use hive_router::pipeline::normalize::hash_normalized_operation;
use hive_router::pipeline::{
authorization::apply_authorization_to_operation,
coerce_variables::CoerceVariablesPayload,
@@ -48,6 +49,10 @@ fn authorization_benchmark(c: &mut Criterion) {
let (root_type_name, projection_plan) =
FieldProjectionPlan::from_operation(&normalized.operation, &metadata);
let partitioned_operation = partition_operation(normalized.operation);
+ let hashes = hash_normalized_operation(
+ &partitioned_operation.downstream_operation,
+ partitioned_operation.introspection_operation.as_ref(),
+ );
GraphQLNormalizationPayload {
root_type_name,
@@ -61,6 +66,9 @@ fn authorization_benchmark(c: &mut Criterion) {
operation_type: "query",
client_document_hash: "".to_string(),
},
+ operation_for_plan_hash: hashes.operation_for_plan_hash,
+ operation_for_introspection_hash: hashes.operation_for_introspection_hash,
+ normalized_operation_hash: hashes.combined_operation_hash,
}
}
diff --git a/bin/router/src/pipeline/authorization/mod.rs b/bin/router/src/pipeline/authorization/mod.rs
index 62d7bdc2e..d41f073d5 100644
--- a/bin/router/src/pipeline/authorization/mod.rs
+++ b/bin/router/src/pipeline/authorization/mod.rs
@@ -25,7 +25,7 @@ use crate::pipeline::authorization::rebuilder::{
use crate::pipeline::authorization::tree::UnauthorizedPathTrie;
use crate::pipeline::coerce_variables::CoerceVariablesPayload;
use crate::pipeline::error::PipelineError;
-use crate::pipeline::normalize::GraphQLNormalizationPayload;
+use crate::pipeline::normalize::{hash_normalized_operation, GraphQLNormalizationPayload};
use hive_router_config::authorization::UnauthorizedMode;
use hive_router_config::HiveRouterConfig;
@@ -117,13 +117,21 @@ pub fn enforce_operation_authorization(
new_projection_plan,
errors,
} => {
+ let hashes = hash_normalized_operation(
+ &new_operation_definition,
+ normalized_payload.operation_for_introspection.as_deref(),
+ );
+
(
Arc::new(GraphQLNormalizationPayload {
operation_for_plan: Arc::new(new_operation_definition),
+ operation_for_plan_hash: hashes.operation_for_plan_hash,
// These are cheap Arc clones
operation_for_introspection: normalized_payload
.operation_for_introspection
.clone(),
+ operation_for_introspection_hash: hashes.operation_for_introspection_hash,
+ normalized_operation_hash: hashes.combined_operation_hash,
root_type_name: normalized_payload.root_type_name,
projection_plan: Arc::new(new_projection_plan),
operation_indentity: normalized_payload.operation_indentity.clone(),
diff --git a/bin/router/src/pipeline/authorization/tests.rs b/bin/router/src/pipeline/authorization/tests.rs
index 4e769ca00..0c6bb4423 100644
--- a/bin/router/src/pipeline/authorization/tests.rs
+++ b/bin/router/src/pipeline/authorization/tests.rs
@@ -19,7 +19,7 @@ use crate::pipeline::{
authorization::{
apply_authorization_to_operation, metadata::AuthorizationMetadataExt, AuthorizationDecision,
},
- normalize::{GraphQLNormalizationPayload, OperationIdentity},
+ normalize::{hash_normalized_operation, GraphQLNormalizationPayload, OperationIdentity},
};
struct SupergraphTestData {
@@ -67,14 +67,21 @@ impl SupergraphTestData {
let (root_type_name, projection_plan) =
FieldProjectionPlan::from_operation(&operation, &self.schema_metadata);
let partitioned_operation = partition_operation(operation);
+ let operation_for_plan = Arc::new(partitioned_operation.downstream_operation);
+ let operation_for_introspection =
+ partitioned_operation.introspection_operation.map(Arc::new);
+
+ let hashes =
+ hash_normalized_operation(&operation_for_plan, operation_for_introspection.as_deref());
let payload = GraphQLNormalizationPayload {
root_type_name,
projection_plan: Arc::new(projection_plan),
- operation_for_plan: Arc::new(partitioned_operation.downstream_operation),
- operation_for_introspection: partitioned_operation
- .introspection_operation
- .map(Arc::new),
+ operation_for_plan,
+ operation_for_plan_hash: hashes.operation_for_plan_hash,
+ operation_for_introspection,
+ operation_for_introspection_hash: hashes.operation_for_introspection_hash,
+ normalized_operation_hash: hashes.combined_operation_hash,
operation_indentity: OperationIdentity {
name: doc.operation_name.clone(),
operation_type: "query",
diff --git a/bin/router/src/pipeline/mod.rs b/bin/router/src/pipeline/mod.rs
index fcef3a1fd..ab00898ce 100644
--- a/bin/router/src/pipeline/mod.rs
+++ b/bin/router/src/pipeline/mod.rs
@@ -1,5 +1,11 @@
-use std::{sync::Arc, time::Instant};
+use std::{
+ collections::HashMap,
+ hash::{Hash, Hasher},
+ sync::Arc,
+ time::Instant,
+};
use tracing::{error, Instrument};
+use xxhash_rust::xxh3::Xxh3;
use hive_router_internal::telemetry::traces::spans::{
graphql::GraphQLOperationSpan, http_request::HttpServerRequestSpan,
@@ -9,6 +15,7 @@ use hive_router_plan_executor::{
client_request_details::{ClientRequestDetails, JwtRequestDetails, OperationDetails},
plan::PlanExecutionOutput,
},
+ hooks::on_graphql_params::GraphQLParams,
hooks::on_supergraph_load::SupergraphData,
plugin_context::{PluginContext, PluginRequestState},
};
@@ -17,6 +24,7 @@ use hive_router_query_planner::{
};
use http::{header::CONTENT_TYPE, Method};
use ntex::web::{self, HttpRequest};
+use sonic_rs::{JsonContainerTrait, JsonType, JsonValueTrait, Value};
use crate::{
pipeline::{
@@ -27,7 +35,7 @@ use crate::{
error::PipelineError,
execution::{execute_plan, PlannedRequest},
execution_request::{deserialize_graphql_params, DeserializationResult, GetQueryStr},
- header::{RequestAccepts, ResponseMode, TEXT_HTML_MIME},
+ header::{RequestAccepts, ResponseMode, SingleContentType, TEXT_HTML_MIME},
introspection_policy::handle_introspection_policy,
normalize::{normalize_request_with_cache, GraphQLNormalizationPayload},
parser::{parse_operation_with_cache, ParseResult},
@@ -40,7 +48,7 @@ use crate::{
validation::validate_operation_with_cache,
},
schema_state::SchemaState,
- shared_state::RouterSharedState,
+ shared_state::{RouterRequestDedupeHeaderPolicy, RouterSharedState, SharedRouterResponse},
GRAPHIQL_HTML,
};
@@ -132,7 +140,7 @@ pub async fn graphql_request_handler(
let deserialization_result =
deserialize_graphql_params(req, body_bytes, &plugin_req_state).await?;
- let mut graphql_params = match deserialization_result {
+ let graphql_params = match deserialization_result {
DeserializationResult::GraphQLParams(params) => params,
DeserializationResult::EarlyResponse(response) => {
return Ok(response);
@@ -205,10 +213,10 @@ pub async fn graphql_request_handler(
.await?;
write_graphql_operation_metric_identity(
- req,
- normalize_payload.operation_indentity.name.clone(),
- Some(normalize_payload.operation_indentity.operation_type),
- );
+ req,
+ normalize_payload.operation_indentity.name.clone(),
+ Some(normalize_payload.operation_indentity.operation_type),
+ );
if req.method() == Method::GET {
if let Some(OperationKind::Mutation) =
@@ -236,103 +244,190 @@ pub async fn graphql_request_handler(
return Err(PipelineError::UnsupportedContentType);
};
- let jwt_request_details = match &shared_state.jwt_auth_runtime {
- Some(jwt_auth_runtime) => match jwt_auth_runtime
- .validate_headers(req.headers(), &shared_state.jwt_claims_cache)
- .await?
- {
- Some(jwt_context) => JwtRequestDetails::Authenticated {
- scopes: jwt_context.extract_scopes(),
- claims: jwt_context.get_claims_value()?,
- token: jwt_context.token_raw,
- prefix: jwt_context.token_prefix,
- },
- None => JwtRequestDetails::Unauthenticated,
- },
- None => JwtRequestDetails::Unauthenticated,
- };
+ let request_dedupe_enabled =
+ shared_state.router_config.traffic_shaping.router.dedupe.enabled;
+
+ let shared_response = if request_dedupe_enabled
+ && matches!(
+ normalize_payload.operation_for_plan.operation_kind,
+ Some(OperationKind::Query) | None
+ ) {
+ let variables_hash = hash_graphql_variables(&graphql_params.variables);
+ let extensions_hash = graphql_params
+ .extensions
+ .as_ref()
+ .map_or(0, hash_graphql_extensions);
+
+ let schema_checksum = supergraph.schema_checksum();
+ let fingerprint = inbound_request_fingerprint(
+ req,
+ &shared_state.in_flight_requests_header_policy,
+ schema_checksum,
+ normalize_payload.normalized_operation_hash,
+ variables_hash,
+ extensions_hash,
+ );
+ let (shared_response, _role) = shared_state
+ .in_flight_requests
+ .claim(fingerprint)
+ .get_or_try_init(|| async {
+ execute_planned_request(
+ req,
+ graphql_params,
+ &normalize_payload,
+ supergraph,
+ shared_state,
+ schema_state,
+ &operation_span,
+ &plugin_req_state,
+ single_content_type,
+ )
+ .await
+ })
+ .await?;
- let variable_payload = coerce_request_variables(
- supergraph,
- &mut graphql_params.variables,
- &normalize_payload,
- )?;
-
- let client_request_details = ClientRequestDetails {
- method: req.method(),
- url: req.uri(),
- headers: req.headers(),
- operation: OperationDetails {
- name: normalize_payload.operation_for_plan.name.as_deref(),
- kind: match normalize_payload.operation_for_plan.operation_kind {
- Some(OperationKind::Query) => "query",
- Some(OperationKind::Mutation) => "mutation",
- Some(OperationKind::Subscription) => "subscription",
- None => "query",
- },
- query: graphql_params.get_query()?,
- },
- jwt: jwt_request_details,
+ Arc::unwrap_or_clone(shared_response)
+ } else {
+ execute_planned_request(
+ req,
+ graphql_params,
+ &normalize_payload,
+ supergraph,
+ shared_state,
+ schema_state,
+ &operation_span,
+ &plugin_req_state,
+ single_content_type,
+ )
+ .await?
};
- let pipeline_result = execute_pipeline(
- &client_request_details,
- &normalize_payload,
- &variable_payload,
- supergraph,
- shared_state,
- schema_state,
- &operation_span,
- &plugin_req_state,
- )
- .await?;
-
- write_graphql_response_metric_status(req, if pipeline_result.error_count > 0 {
- GraphQLResponseStatus::Error
- } else {
- GraphQLResponseStatus::Ok
- });
-
if let Some(hive_usage_agent) = &shared_state.hive_usage_agent {
- usage_reporting::collect_usage_report(
- supergraph.supergraph_schema.clone(),
- started_at.elapsed(),
- client_name,
- client_version,
- &client_request_details,
- hive_usage_agent,
- shared_state
- .router_config
- .telemetry
- .hive
- .as_ref()
- .map(|c| &c.usage_reporting)
- .expect(
- // SAFETY: According to `configure_app_from_config` in `bin/router/src/lib.rs`,
- // the UsageAgent is only created when usage reporting is enabled.
- // Thus, this expect should never panic.
- "Expected Usage Reporting options to be present when Hive Usage Agent is initialized",
- ),
- pipeline_result.error_count,
- )
- .await;
- }
+ usage_reporting::collect_usage_report(
+ supergraph.supergraph_schema.clone(),
+ started_at.elapsed(),
+ client_name,
+ client_version,
+ normalize_payload.operation_for_plan.name.as_deref(),
+ &parser_payload.minified_document,
+ hive_usage_agent,
+ shared_state
+ .router_config
+ .telemetry
+ .hive
+ .as_ref()
+ .map(|c| &c.usage_reporting)
+ .expect(
+ // SAFETY: According to `configure_app_from_config` in `bin/router/src/lib.rs`,
+ // the UsageAgent is only created when usage reporting is enabled.
+ // Thus, this expect should never panic.
+ "Expected Usage Reporting options to be present when Hive Usage Agent is initialized",
+ ),
+ shared_response.error_count,
+ )
+ .await;
+ }
- let mut response_builder = web::HttpResponse::Ok();
+ let pipeline_error_count = shared_response.error_count;
+ let response = shared_response.into();
- if let Some(response_headers_aggregator) = pipeline_result.response_headers_aggregator {
- response_headers_aggregator.modify_client_response_headers(&mut response_builder)?;
- }
+ write_graphql_response_metric_status(
+ req,
+ if pipeline_error_count > 0 {
+ GraphQLResponseStatus::Error
+ } else {
+ GraphQLResponseStatus::Ok
+ },
+ );
- Ok(response_builder
- .content_type(single_content_type.as_ref())
- .status(pipeline_result.status_code)
- .body(pipeline_result.body))
+ Ok(response)
}
.instrument(operation_span.clone())
.await
.inspect_err(|_| {
- write_graphql_response_metric_status(req, GraphQLResponseStatus::Error);
+ write_graphql_response_metric_status(req, GraphQLResponseStatus::Error);
+ })
+}
+
+#[allow(clippy::too_many_arguments)]
+async fn execute_planned_request<'exec>(
+ req: &'exec HttpRequest,
+ mut graphql_params: GraphQLParams,
+ normalize_payload: &Arc,
+ supergraph: &'exec SupergraphData,
+ shared_state: &'exec Arc,
+ schema_state: &'exec Arc,
+ operation_span: &'exec GraphQLOperationSpan,
+ plugin_req_state: &'exec Option>,
+ single_content_type: &'exec SingleContentType,
+) -> Result {
+ let jwt_request_details = match &shared_state.jwt_auth_runtime {
+ Some(jwt_auth_runtime) => match jwt_auth_runtime
+ .validate_headers(req.headers(), &shared_state.jwt_claims_cache)
+ .await?
+ {
+ Some(jwt_context) => JwtRequestDetails::Authenticated {
+ scopes: jwt_context.extract_scopes(),
+ claims: jwt_context.get_claims_value()?,
+ token: jwt_context.token_raw,
+ prefix: jwt_context.token_prefix,
+ },
+ None => JwtRequestDetails::Unauthenticated,
+ },
+ None => JwtRequestDetails::Unauthenticated,
+ };
+
+ let variable_payload =
+ coerce_request_variables(supergraph, &mut graphql_params.variables, normalize_payload)?;
+
+ let client_request_details = ClientRequestDetails {
+ method: req.method(),
+ url: req.uri(),
+ headers: req.headers(),
+ operation: OperationDetails {
+ name: normalize_payload.operation_for_plan.name.as_deref(),
+ kind: match normalize_payload.operation_for_plan.operation_kind {
+ Some(OperationKind::Query) => "query",
+ Some(OperationKind::Mutation) => "mutation",
+ Some(OperationKind::Subscription) => "subscription",
+ None => "query",
+ },
+ query: graphql_params.get_query()?,
+ },
+ jwt: jwt_request_details,
+ };
+
+ let pipeline_result = execute_pipeline(
+ &client_request_details,
+ normalize_payload,
+ &variable_payload,
+ supergraph,
+ shared_state,
+ schema_state,
+ operation_span,
+ plugin_req_state,
+ )
+ .await?;
+
+ let error_count = pipeline_result.error_count;
+ let mut response_builder = web::HttpResponse::Ok();
+
+ if let Some(response_headers_aggregator) = pipeline_result.response_headers_aggregator {
+ response_headers_aggregator.modify_client_response_headers(&mut response_builder)?;
+ }
+
+ let body = ntex::util::Bytes::from(pipeline_result.body);
+
+ let response = response_builder
+ .content_type(single_content_type.as_ref())
+ .status(pipeline_result.status_code)
+ .body(body.clone());
+
+ Ok(SharedRouterResponse {
+ body,
+ headers: Arc::new(response.headers().clone()),
+ status: response.status(),
+ error_count,
})
}
@@ -397,3 +492,105 @@ pub async fn execute_pipeline<'exec>(
execute_plan(supergraph, shared_state, planned_request, operation_span).await
}
+
+fn inbound_request_fingerprint(
+ req: &HttpRequest,
+ dedupe_header_policy: &RouterRequestDedupeHeaderPolicy,
+ schema_checksum: u64,
+ normalized_operation_hash: u64,
+ variables_hash: u64,
+ extensions_hash: u64,
+) -> u64 {
+ let mut hasher = Xxh3::new();
+
+ let mut headers: Vec<(&str, &str)> = req
+ .headers()
+ .iter()
+ .filter(|(name, _)| dedupe_header_policy.should_include(name.as_str()))
+ .filter_map(|(name, value)| value.to_str().ok().map(|v_str| (name.as_str(), v_str)))
+ .collect();
+ headers.sort_unstable_by(|(left_name, left_value), (right_name, right_value)| {
+ left_name
+ .cmp(right_name)
+ .then_with(|| left_value.cmp(right_value))
+ });
+
+ req.method().hash(&mut hasher);
+ req.path().hash(&mut hasher);
+ headers.hash(&mut hasher);
+ schema_checksum.hash(&mut hasher);
+ normalized_operation_hash.hash(&mut hasher);
+ variables_hash.hash(&mut hasher);
+ extensions_hash.hash(&mut hasher);
+
+ hasher.finish()
+}
+
+fn hash_graphql_variables(variables: &HashMap) -> u64 {
+ let mut hasher = Xxh3::new();
+
+ let mut keys: Vec<&str> = variables.keys().map(String::as_str).collect();
+ keys.sort_unstable();
+
+ keys.len().hash(&mut hasher);
+ for key in keys {
+ key.hash(&mut hasher);
+ if let Some(value) = variables.get(key) {
+ hash_graphql_value(value, &mut hasher);
+ }
+ }
+
+ hasher.finish()
+}
+
+fn hash_graphql_extensions(extensions: &HashMap) -> u64 {
+ // reused as hash_graphql_variables has the same function signature
+ hash_graphql_variables(extensions)
+}
+
+fn hash_graphql_value(value: &Value, hasher: &mut Xxh3) {
+ match value.get_type() {
+ JsonType::Null => 0u8.hash(hasher),
+ JsonType::Boolean => {
+ 1u8.hash(hasher);
+ value.as_bool().unwrap_or(false).hash(hasher);
+ }
+ JsonType::Number => {
+ 2u8.hash(hasher);
+ if let Some(number) = value.as_i64() {
+ 0u8.hash(hasher);
+ number.hash(hasher);
+ } else if let Some(number) = value.as_u64() {
+ 1u8.hash(hasher);
+ number.hash(hasher);
+ } else if let Some(number) = value.as_f64() {
+ 2u8.hash(hasher);
+ number.to_bits().hash(hasher);
+ }
+ }
+ JsonType::String => {
+ 3u8.hash(hasher);
+ value.as_str().unwrap_or_default().hash(hasher);
+ }
+ JsonType::Object => {
+ 4u8.hash(hasher);
+ if let Some(object) = value.as_object() {
+ object.len().hash(hasher);
+ for (key, nested_value) in object.iter() {
+ key.hash(hasher);
+ hash_graphql_value(nested_value, hasher);
+ }
+ }
+ }
+ JsonType::Array => {
+ 5u8.hash(hasher);
+ if let Some(array) = value.as_array() {
+ let slice = array.as_slice();
+ slice.len().hash(hasher);
+ for item in slice {
+ hash_graphql_value(item, hasher);
+ }
+ }
+ }
+ }
+}
diff --git a/bin/router/src/pipeline/normalize.rs b/bin/router/src/pipeline/normalize.rs
index 3a9a1233c..eb40060ae 100644
--- a/bin/router/src/pipeline/normalize.rs
+++ b/bin/router/src/pipeline/normalize.rs
@@ -23,7 +23,10 @@ use tracing::{trace, Instrument};
pub struct GraphQLNormalizationPayload {
/// The operation to execute, without introspection fields.
pub operation_for_plan: Arc,
+ pub operation_for_plan_hash: u64,
pub operation_for_introspection: Option>,
+ pub operation_for_introspection_hash: Option,
+ pub normalized_operation_hash: u64,
pub root_type_name: &'static str,
pub projection_plan: Arc>,
pub operation_indentity: OperationIdentity,
@@ -47,6 +50,34 @@ impl<'a> From<&'a OperationIdentity> for GraphQLSpanOperationIdentity<'a> {
}
}
+pub fn hash_normalized_operation(
+ operation_for_plan: &OperationDefinition,
+ operation_for_introspection: Option<&OperationDefinition>,
+) -> NormalizedOperationHashes {
+ let operation_for_plan_hash = operation_for_plan.hash();
+ let operation_for_introspection_hash =
+ operation_for_introspection.map(OperationDefinition::hash);
+
+ let mut hasher = Xxh3::new();
+ operation_for_plan_hash.hash(&mut hasher);
+ operation_for_introspection_hash.is_some().hash(&mut hasher);
+ if let Some(hash) = operation_for_introspection_hash {
+ hash.hash(&mut hasher);
+ }
+
+ NormalizedOperationHashes {
+ operation_for_plan_hash,
+ operation_for_introspection_hash,
+ combined_operation_hash: hasher.finish(),
+ }
+}
+
+pub struct NormalizedOperationHashes {
+ pub operation_for_plan_hash: u64,
+ pub operation_for_introspection_hash: Option,
+ pub combined_operation_hash: u64,
+}
+
#[inline]
pub async fn normalize_request_with_cache(
supergraph: &SupergraphData,
@@ -89,13 +120,23 @@ pub async fn normalize_request_with_cache(
FieldProjectionPlan::from_operation(&operation, &supergraph.metadata);
let partitioned_operation = partition_operation(operation);
+ let operation_for_plan = Arc::new(partitioned_operation.downstream_operation);
+ let operation_for_introspection =
+ partitioned_operation.introspection_operation.map(Arc::new);
+
+ let hashes = hash_normalized_operation(
+ &operation_for_plan,
+ operation_for_introspection.as_deref(),
+ );
+
let payload = GraphQLNormalizationPayload {
root_type_name,
projection_plan: Arc::new(projection_plan),
- operation_for_plan: Arc::new(partitioned_operation.downstream_operation),
- operation_for_introspection: partitioned_operation
- .introspection_operation
- .map(Arc::new),
+ operation_for_plan,
+ operation_for_plan_hash: hashes.operation_for_plan_hash,
+ operation_for_introspection,
+ operation_for_introspection_hash: hashes.operation_for_introspection_hash,
+ normalized_operation_hash: hashes.combined_operation_hash,
operation_indentity: OperationIdentity {
name: doc.operation_name.clone(),
operation_type: parser_payload.operation_type,
diff --git a/bin/router/src/pipeline/query_plan.rs b/bin/router/src/pipeline/query_plan.rs
index 92ad37a3e..2be40683c 100644
--- a/bin/router/src/pipeline/query_plan.rs
+++ b/bin/router/src/pipeline/query_plan.rs
@@ -44,12 +44,12 @@ pub async fn plan_operation_with_cache(
async {
let mut on_end_callbacks = vec![];
- let filtered_operation_for_plan = &normalized_operation.operation_for_plan;
+ let mut filtered_operation_for_plan = normalized_operation.operation_for_plan.as_ref();
if let Some(plugin_req_state) = plugin_req_state {
let mut start_payload = OnQueryPlanStartHookPayload {
router_http_request: &plugin_req_state.router_http_request,
context: &plugin_req_state.context,
- filtered_operation_for_plan: &normalized_operation.operation_for_plan,
+ filtered_operation_for_plan,
cancellation_token,
planner: &supergraph.planner,
};
@@ -69,6 +69,8 @@ pub async fn plan_operation_with_cache(
}
}
}
+
+ filtered_operation_for_plan = start_payload.filtered_operation_for_plan;
};
let metrics = &schema_state.telemetry_context.metrics;
@@ -76,8 +78,15 @@ pub async fn plan_operation_with_cache(
let stable_override_context =
StableOverrideContext::new(&supergraph.planner.supergraph, request_override_context);
- let plan_cache_key =
- calculate_cache_key(filtered_operation_for_plan.hash(), &stable_override_context);
+ let operation_for_plan_hash = if std::ptr::eq(
+ filtered_operation_for_plan,
+ normalized_operation.operation_for_plan.as_ref(),
+ ) {
+ normalized_operation.operation_for_plan_hash
+ } else {
+ filtered_operation_for_plan.hash()
+ };
+ let plan_cache_key = calculate_cache_key(operation_for_plan_hash, &stable_override_context);
let is_plan_operation_empty = filtered_operation_for_plan.selection_set.is_empty();
let is_projection_plan_empty = normalized_operation.projection_plan.is_empty();
let contains_introspection = normalized_operation.operation_for_introspection.is_some();
diff --git a/bin/router/src/pipeline/usage_reporting.rs b/bin/router/src/pipeline/usage_reporting.rs
index 941c72c64..10ce60876 100644
--- a/bin/router/src/pipeline/usage_reporting.rs
+++ b/bin/router/src/pipeline/usage_reporting.rs
@@ -13,7 +13,6 @@ use hive_router_config::{
};
use hive_router_internal::background_tasks::{BackgroundTask, BackgroundTasksManager};
use hive_router_internal::telemetry::utils::resolve_value_or_expression;
-use hive_router_plan_executor::execution::client_request_details::ClientRequestDetails;
use rand::prelude::*;
use tokio_util::sync::CancellationToken;
@@ -86,7 +85,8 @@ pub async fn collect_usage_report<'a>(
duration: Duration,
client_name: Option<&str>,
client_version: Option<&str>,
- client_request_details: &ClientRequestDetails<'a>,
+ operation_name: Option<&'a str>,
+ operation_body: &'a str,
hive_usage_agent: &UsageAgent,
usage_config: &UsageReportingConfig,
error_count: usize,
@@ -95,11 +95,7 @@ pub async fn collect_usage_report<'a>(
if sample_rate < 1.0 && !rand::rng().random_bool(sample_rate) {
return;
}
- if client_request_details
- .operation
- .name
- .is_some_and(|op_name| usage_config.exclude.iter().any(|s| s == op_name))
- {
+ if operation_name.is_some_and(|op_name| usage_config.exclude.iter().any(|s| s == op_name)) {
return;
}
let timestamp = SystemTime::now()
@@ -114,11 +110,8 @@ pub async fn collect_usage_report<'a>(
duration,
ok: error_count == 0,
errors: error_count,
- operation_body: client_request_details.operation.query.to_owned(),
- operation_name: client_request_details
- .operation
- .name
- .map(|op_name| op_name.to_owned()),
+ operation_body: operation_body.to_owned(),
+ operation_name: operation_name.map(|s| s.to_owned()),
persisted_document_hash: None,
};
diff --git a/bin/router/src/shared_state.rs b/bin/router/src/shared_state.rs
index 6b54e6394..22823db44 100644
--- a/bin/router/src/shared_state.rs
+++ b/bin/router/src/shared_state.rs
@@ -1,17 +1,24 @@
use graphql_tools::validation::validate::ValidationPlan;
use hive_console_sdk::agent::usage_agent::{AgentError, UsageAgent};
+use hive_router_config::traffic_shaping::{
+ TrafficShapingRouterDedupeHeadersConfig, TrafficShapingRouterDedupeHeadersKeyword,
+};
use hive_router_config::HiveRouterConfig;
use hive_router_internal::expressions::values::boolean::BooleanOrProgram;
use hive_router_internal::expressions::ExpressionCompileError;
+use hive_router_internal::inflight::InFlightMap;
use hive_router_internal::telemetry::TelemetryContext;
use hive_router_plan_executor::headers::{
compile::compile_headers_plan, errors::HeaderRuleCompileError, plan::HeaderRulesPlan,
};
use hive_router_plan_executor::plugin_trait::RouterPluginBoxed;
+use http::StatusCode;
use moka::future::Cache;
use moka::Expiry;
-use std::sync::Arc;
+use ntex::web;
+use ntex::{http::HeaderMap, util::Bytes};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
+use std::{collections::HashSet, sync::Arc};
use crate::cache_state::CacheState;
use crate::jwt::context::JwtTokenPayload;
@@ -22,6 +29,71 @@ use crate::pipeline::parser::ParseCacheEntry;
use crate::pipeline::progressive_override::{OverrideLabelsCompileError, OverrideLabelsEvaluator};
pub type JwtClaimsCache = Cache>;
+pub type RouterInflightRequestsMap = InFlightMap;
+
+#[derive(Clone)]
+pub enum RouterRequestDedupeHeaderPolicy {
+ All,
+ None,
+ Include(HashSet),
+}
+
+impl RouterRequestDedupeHeaderPolicy {
+ #[inline]
+ pub fn should_include(&self, header_name: &str) -> bool {
+ match self {
+ Self::All => true,
+ Self::None => false,
+ Self::Include(allowed_headers) => allowed_headers.contains(header_name),
+ }
+ }
+}
+
+impl From<&TrafficShapingRouterDedupeHeadersConfig> for RouterRequestDedupeHeaderPolicy {
+ fn from(headers: &TrafficShapingRouterDedupeHeadersConfig) -> Self {
+ match headers {
+ TrafficShapingRouterDedupeHeadersConfig::Keyword(
+ TrafficShapingRouterDedupeHeadersKeyword::All,
+ ) => Self::All,
+ TrafficShapingRouterDedupeHeadersConfig::Keyword(
+ TrafficShapingRouterDedupeHeadersKeyword::None,
+ ) => Self::None,
+ TrafficShapingRouterDedupeHeadersConfig::Include { include } => {
+ if include.is_empty() {
+ return Self::None;
+ }
+
+ let mut dedupe_headers = HashSet::with_capacity(include.len());
+ for header in include {
+ dedupe_headers.insert(header.get_header_ref().as_str().to_owned());
+ }
+
+ Self::Include(dedupe_headers)
+ }
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct SharedRouterResponse {
+ pub body: Bytes,
+ pub headers: Arc,
+ pub status: StatusCode,
+ pub error_count: usize,
+}
+
+impl From for web::HttpResponse {
+ fn from(shared_response: SharedRouterResponse) -> Self {
+ let mut response = web::HttpResponse::Ok();
+ response.status(shared_response.status);
+
+ for (header_name, header_value) in shared_response.headers.iter() {
+ response.set_header(header_name, header_value);
+ }
+
+ response.body(shared_response.body)
+ }
+}
/// Default TTL for JWT claims cache entries (5 seconds)
const DEFAULT_JWT_CACHE_TTL_SECS: u64 = 5;
@@ -79,6 +151,8 @@ pub struct RouterSharedState {
pub introspection_policy: BooleanOrProgram,
pub telemetry_context: Arc,
pub plugins: Option>>,
+ pub in_flight_requests: RouterInflightRequestsMap,
+ pub in_flight_requests_header_policy: RouterRequestDedupeHeaderPolicy,
}
impl RouterSharedState {
@@ -114,6 +188,13 @@ impl RouterSharedState {
.map_err(Box::new)?,
telemetry_context,
plugins,
+ in_flight_requests: InFlightMap::default(),
+ in_flight_requests_header_policy: (&router_config
+ .traffic_shaping
+ .router
+ .dedupe
+ .headers)
+ .into(),
})
}
}
@@ -131,3 +212,40 @@ pub enum SharedStateError {
#[error("invalid introspection config: {0}")]
IntrospectionPolicyCompile(#[from] Box),
}
+
+#[cfg(test)]
+mod tests {
+ use super::RouterRequestDedupeHeaderPolicy;
+ use hive_router_config::traffic_shaping::{
+ TrafficShapingRouterDedupeHeadersConfig, TrafficShapingRouterDedupeHeadersKeyword,
+ };
+
+ #[test]
+ fn should_map_header_variants_to_policy() {
+ let all = TrafficShapingRouterDedupeHeadersConfig::Keyword(
+ TrafficShapingRouterDedupeHeadersKeyword::All,
+ );
+ assert!(matches!(
+ RouterRequestDedupeHeaderPolicy::from(&all),
+ RouterRequestDedupeHeaderPolicy::All
+ ));
+
+ let none = TrafficShapingRouterDedupeHeadersConfig::Keyword(
+ TrafficShapingRouterDedupeHeadersKeyword::None,
+ );
+ assert!(matches!(
+ RouterRequestDedupeHeaderPolicy::from(&none),
+ RouterRequestDedupeHeaderPolicy::None
+ ));
+
+ let include = TrafficShapingRouterDedupeHeadersConfig::Include {
+ include: vec!["Authorization".into()],
+ };
+ let include_policy = RouterRequestDedupeHeaderPolicy::from(&include);
+ assert!(matches!(
+ include_policy,
+ RouterRequestDedupeHeaderPolicy::Include(_)
+ ));
+ assert!(include_policy.should_include("authorization"));
+ }
+}
diff --git a/docs/README.md b/docs/README.md
index a38bb7145..3b53337ae 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -20,7 +20,7 @@
|[**query\_planner**](#query_planner)|`object`|Query planning configuration. Default: `{"allow_expose":false,"timeout":"10s"}` ||
|[**supergraph**](#supergraph)|`object`|Configuration for the Federation supergraph source. By default, the router will use a local file-based supergraph source (`./supergraph.graphql`). ||
|[**telemetry**](#telemetry)|`object`|Default: `{"client_identification":{"name_header":"graphql-client-name","version_header":"graphql-client-version"},"hive":null,"metrics":{"exporters":[],"instrumentation":{"common":{"histogram":{"aggregation":"explicit","bytes":{"buckets":[128,512,1024,2048,4096,8192,16384,32768,65536,131072,262144,524288,1048576,2097152,3145728,4194304,5242880],"record_min_max":false},"seconds":{"buckets":[0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1,2.5,5,7.5,10],"record_min_max":false}}},"instruments":{}}},"resource":{"attributes":{}},"tracing":{"collect":{"max_attributes_per_event":16,"max_attributes_per_link":32,"max_attributes_per_span":128,"max_events_per_span":128,"parent_based_sampler":false,"sampling":1},"exporters":[],"instrumentation":{"spans":{"mode":"spec_compliant"}},"propagation":{"b3":false,"baggage":false,"jaeger":false,"trace_context":true}}}` ||
-|[**traffic\_shaping**](#traffic_shaping)|`object`|Configuration for the traffic-shaping of the executor. Use these configurations to control how requests are being executed to subgraphs. Default: `{"all":{"dedupe_enabled":true,"pool_idle_timeout":"50s","request_timeout":"30s"},"max_connections_per_host":100,"router":{"request_timeout":"1m"}}` ||
+|[**traffic\_shaping**](#traffic_shaping)|`object`|Configuration for the traffic-shaping of the executor. Use these configurations to control how requests are being executed to subgraphs. Default: `{"all":{"dedupe_enabled":true,"pool_idle_timeout":"50s","request_timeout":"30s"},"max_connections_per_host":100,"router":{"dedupe":{"enabled":false,"headers":"all"},"request_timeout":"1m"}}` ||
**Additional Properties:** not allowed
**Example**
@@ -195,6 +195,9 @@ traffic_shaping:
request_timeout: 30s
max_connections_per_host: 100
router:
+ dedupe:
+ enabled: false
+ headers: all
request_timeout: 1m
```
@@ -2914,7 +2917,7 @@ Configuration for the traffic-shaping of the executor. Use these configurations
|----|----|-----------|--------|
|[**all**](#traffic_shapingall)|`object`|The default configuration that will be applied to all subgraphs, unless overridden by a specific subgraph configuration. Default: `{"dedupe_enabled":true,"pool_idle_timeout":"50s","request_timeout":"30s"}` ||
|**max\_connections\_per\_host**|`integer`|Limits the concurrent amount of requests/connections per host/subgraph. Default: `100` Format: `"uint"` Minimum: `0` ||
-|[**router**](#traffic_shapingrouter)|`object`|Configuration for the router itself, e.g., for handling incoming requests, or other router-level traffic shaping configurations. Default: `{"request_timeout":"1m"}` ||
+|[**router**](#traffic_shapingrouter)|`object`|Configuration for the router itself, e.g., for handling incoming requests, or other router-level traffic shaping configurations. Default: `{"dedupe":{"enabled":false,"headers":"all"},"request_timeout":"1m"}` ||
|[**subgraphs**](#traffic_shapingsubgraphs)|`object`|Optional per-subgraph configurations that will override the default configuration for specific subgraphs. ||
**Additional Properties:** not allowed
@@ -2927,6 +2930,9 @@ all:
request_timeout: 30s
max_connections_per_host: 100
router:
+ dedupe:
+ enabled: false
+ headers: all
request_timeout: 1m
```
@@ -2965,16 +2971,39 @@ Configuration for the router itself, e.g., for handling incoming requests, or ot
|Name|Type|Description|Required|
|----|----|-----------|--------|
+|[**dedupe**](#traffic_shapingrouterdedupe)|`object`|Default: `{"enabled":false,"headers":"all"}` ||
|**request\_timeout**|`string`|Optional timeout configuration for incoming requests to the router. It starts from the moment the request is received by the router, and includes the entire processing of the request (validation, execution, etc.) until a response is sent back to the client. If a request takes longer than the specified duration, it will be aborted and a timeout error will be returned to the client. Default: `"1m"` ||
**Additional Properties:** not allowed
**Example**
```yaml
+dedupe:
+ enabled: false
+ headers: all
request_timeout: 1m
```
+
+#### traffic\_shaping\.router\.dedupe: object
+
+**Properties**
+
+|Name|Type|Description|Required|
+|----|----|-----------|--------|
+|**enabled**|`boolean`|Enables/disables in-flight request deduplication at the router endpoint level. When enabled, identical incoming GraphQL query requests that are processed at the same time share the same in-flight execution result. Default: `false` ||
+|**headers**||Header configuration participating in the dedupe key. Accepted forms: - `all` - `none` - `{ include: ["authorization", "cookie"] }` Header names are case-insensitive and validated as standard HTTP header names. Default: `"all"` ||
+
+**Additional Properties:** not allowed
+**Example**
+
+```yaml
+enabled: false
+headers: all
+
+```
+
### traffic\_shaping\.subgraphs: object
diff --git a/e2e/src/http.rs b/e2e/src/http.rs
index 7225ab9c6..069abc9a7 100644
--- a/e2e/src/http.rs
+++ b/e2e/src/http.rs
@@ -1,10 +1,33 @@
#[cfg(test)]
mod http_tests {
+ use std::{
+ thread::sleep,
+ time::{Duration, Instant},
+ };
+
+ use futures::{stream::FuturesUnordered, StreamExt};
use hive_router::pipeline::execution::EXPOSE_QUERY_PLAN_HEADER;
+ use mockito::Mock;
+ use ntex::time;
use sonic_rs::JsonValueTrait;
use crate::testkit::{some_header_map, ClientResponseExt, TestRouter, TestSubgraphs};
+ async fn wait_until_mock_matched(mock: &Mock, timeout: Duration) -> Result<(), String> {
+ let started = Instant::now();
+ loop {
+ if mock.matched_async().await {
+ return Ok(());
+ }
+
+ time::sleep(Duration::from_millis(10)).await;
+
+ if started.elapsed() > timeout {
+ return Err(format!("timeout after {:?}", started.elapsed()));
+ }
+ }
+ }
+
#[ntex::test]
async fn should_allow_to_customize_graphql_endpoint() {
let subgraphs = TestSubgraphs::builder().build().start().await;
@@ -205,4 +228,554 @@ mod http_tests {
"expected no requests to products subgraph"
);
}
+
+ #[ntex::test]
+ async fn should_not_dedupe_inflight_router_requests_by_default() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_on_request(|request| {
+ if request.path == "/products" {
+ sleep(Duration::from_millis(50));
+ }
+ None
+ })
+ .build()
+ .start()
+ .await;
+
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ traffic_shaping:
+ all:
+ dedupe_enabled: false
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let request_count = 12;
+ let mut requests = FuturesUnordered::new();
+
+ for _ in 0..request_count {
+ requests.push(router.send_graphql_request(
+ r#"
+ {
+ topProducts {
+ name
+ price
+ }
+ }
+ "#,
+ None,
+ None,
+ ));
+ }
+
+ while let Some(response) = requests.next().await {
+ assert!(response.status().is_success(), "Expected 200 OK");
+ let json_body = response.json_body().await;
+ assert!(json_body["data"]["topProducts"].is_array());
+ assert!(json_body["errors"].is_null());
+ }
+
+ let products_requests = subgraphs
+ .get_requests_log("products")
+ .unwrap_or_default()
+ .len();
+
+ assert!(
+ products_requests >= request_count,
+ "expected at least one products subgraph request per incoming request when router inflight dedupe is disabled by default; got {products_requests} for {request_count} incoming requests"
+ );
+ }
+
+ #[ntex::test]
+ async fn should_dedupe_inflight_router_requests_when_enabled() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_on_request(|request| {
+ if request.path == "/products" {
+ sleep(Duration::from_millis(50));
+ }
+ None
+ })
+ .build()
+ .start()
+ .await;
+
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ traffic_shaping:
+ all:
+ dedupe_enabled: false
+ router:
+ dedupe:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let request_count = 12;
+ let mut requests = FuturesUnordered::new();
+
+ for _ in 0..request_count {
+ requests.push(router.send_graphql_request(
+ r#"
+ {
+ topProducts {
+ name
+ price
+ }
+ }
+ "#,
+ None,
+ None,
+ ));
+ }
+
+ while let Some(response) = requests.next().await {
+ assert!(response.status().is_success(), "Expected 200 OK");
+ let json_body = response.json_body().await;
+ assert!(json_body["data"]["topProducts"].is_array());
+ assert!(json_body["errors"].is_null());
+ }
+
+ let products_requests = subgraphs
+ .get_requests_log("products")
+ .unwrap_or_default()
+ .len();
+
+ assert!(
+ products_requests < request_count,
+ "expected fewer products subgraph requests than incoming requests when router inflight dedupe is enabled; got {products_requests} for {request_count} incoming requests"
+ );
+ }
+
+ #[ntex::test]
+ async fn should_not_dedupe_inflight_router_mutation_requests() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_on_request(|request| {
+ if request.path == "/products" {
+ sleep(Duration::from_millis(50));
+ }
+ None
+ })
+ .build()
+ .start()
+ .await;
+
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ traffic_shaping:
+ all:
+ dedupe_enabled: false
+ router:
+ dedupe:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let request_count = 8;
+ let mut requests = FuturesUnordered::new();
+
+ for _ in 0..request_count {
+ requests.push(router.send_graphql_request(
+ r#"
+ mutation {
+ oneofTest(input: { string: "router-dedupe" }) {
+ string
+ }
+ }
+ "#,
+ None,
+ None,
+ ));
+ }
+
+ while let Some(response) = requests.next().await {
+ assert!(response.status().is_success(), "Expected 200 OK");
+ }
+
+ let products_requests = subgraphs
+ .get_requests_log("products")
+ .unwrap_or_default()
+ .len();
+
+ assert!(
+ products_requests >= request_count,
+ "expected no mutation dedupe at router level; got {products_requests} products requests for {request_count} incoming mutation requests"
+ );
+ }
+
+ #[ntex::test]
+ async fn should_not_share_inflight_dedupe_entry_across_schema_reload() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_on_request(|request| {
+ if request.path == "/products" {
+ sleep(Duration::from_millis(1500));
+ }
+ None
+ })
+ .build()
+ .start()
+ .await;
+
+ let initial_supergraph = subgraphs.supergraph(include_str!("../supergraph.graphql"));
+ let changed_supergraph = initial_supergraph.replacen("first: Int = 5", "first: Int = 6", 1);
+ assert_ne!(initial_supergraph, changed_supergraph);
+
+ let mut server = mockito::Server::new_async().await;
+ let host = server.host_with_port();
+
+ let mock_initial = server
+ .mock("GET", "/supergraph")
+ .expect_at_least(1)
+ .with_status(200)
+ .with_header("content-type", "text/plain")
+ .with_header("etag", "v1")
+ .with_body(initial_supergraph)
+ .create();
+
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(format!(
+ r#"
+ supergraph:
+ source: hive
+ endpoint: http://{host}/supergraph
+ key: dummy_key
+ poll_interval: 100ms
+ traffic_shaping:
+ all:
+ dedupe_enabled: false
+ router:
+ dedupe:
+ enabled: true
+ "#,
+ ))
+ .build()
+ .start()
+ .await;
+
+ let query = r#"
+ {
+ topProducts {
+ name
+ price
+ }
+ }
+ "#;
+
+ let first_request = async { router.send_graphql_request(query, None, None).await };
+ let second_request = async {
+ let started = Instant::now();
+ while subgraphs
+ .get_requests_log("products")
+ .unwrap_or_default()
+ .is_empty()
+ {
+ assert!(
+ started.elapsed() < Duration::from_secs(3),
+ "first request did not reach products subgraph in time"
+ );
+ time::sleep(Duration::from_millis(25)).await;
+ }
+
+ mock_initial.remove();
+
+ let mock_changed = server
+ .mock("GET", "/supergraph")
+ .expect_at_least(1)
+ .with_status(200)
+ .with_header("content-type", "text/plain")
+ .with_header("etag", "v2")
+ .with_body(changed_supergraph)
+ .create();
+
+ wait_until_mock_matched(&mock_changed, Duration::from_secs(2))
+ .await
+ .expect("expected schema reload poll to fetch changed supergraph");
+
+ router.send_graphql_request(query, None, None).await
+ };
+
+ let (first_response, second_response) = futures::join!(first_request, second_request);
+
+ assert!(
+ first_response.status().is_success(),
+ "expected first request 200 OK"
+ );
+ assert!(
+ second_response.status().is_success(),
+ "expected second request 200 OK"
+ );
+
+ let products_requests = subgraphs
+ .get_requests_log("products")
+ .unwrap_or_default()
+ .len();
+
+ assert!(
+ products_requests >= 2,
+ "expected at least two products subgraph requests across schema reload to avoid sharing old in-flight dedupe entry; got {products_requests}"
+ );
+ }
+
+ #[ntex::test]
+ async fn should_use_all_headers_in_router_dedupe_key_by_default() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_on_request(|request| {
+ if request.path == "/products" {
+ sleep(Duration::from_millis(50));
+ }
+ None
+ })
+ .build()
+ .start()
+ .await;
+
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ traffic_shaping:
+ all:
+ dedupe_enabled: false
+ router:
+ dedupe:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let query = r#"
+ {
+ topProducts {
+ name
+ price
+ }
+ }
+ "#;
+
+ let (response_a, response_b) = futures::join!(
+ router.send_graphql_request(
+ query,
+ None,
+ some_header_map! {
+ "x-user" => "a"
+ },
+ ),
+ router.send_graphql_request(
+ query,
+ None,
+ some_header_map! {
+ "x-user" => "b"
+ },
+ )
+ );
+
+ assert!(
+ response_a.status().is_success(),
+ "Expected first request 200 OK"
+ );
+ assert!(
+ response_b.status().is_success(),
+ "Expected second request 200 OK"
+ );
+
+ let products_requests = subgraphs
+ .get_requests_log("products")
+ .unwrap_or_default()
+ .len();
+
+ assert!(
+ products_requests >= 2,
+ "expected at least two products subgraph requests when all headers are part of dedupe key; got {products_requests}"
+ );
+ }
+
+ #[ntex::test]
+ async fn should_ignore_headers_in_router_dedupe_key_when_headers_is_empty() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_on_request(|request| {
+ if request.path == "/products" {
+ sleep(Duration::from_millis(50));
+ }
+ None
+ })
+ .build()
+ .start()
+ .await;
+
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ traffic_shaping:
+ all:
+ dedupe_enabled: false
+ router:
+ dedupe:
+ enabled: true
+ headers: none
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let query = r#"
+ {
+ topProducts {
+ name
+ price
+ }
+ }
+ "#;
+
+ let (response_a, response_b) = futures::join!(
+ router.send_graphql_request(
+ query,
+ None,
+ some_header_map! {
+ "x-user" => "a"
+ },
+ ),
+ router.send_graphql_request(
+ query,
+ None,
+ some_header_map! {
+ "x-user" => "b"
+ },
+ )
+ );
+
+ assert!(
+ response_a.status().is_success(),
+ "Expected first request 200 OK"
+ );
+ assert!(
+ response_b.status().is_success(),
+ "Expected second request 200 OK"
+ );
+
+ let products_requests = subgraphs
+ .get_requests_log("products")
+ .unwrap_or_default()
+ .len();
+
+ assert_eq!(
+ products_requests, 1,
+ "expected exactly one products subgraph request when router dedupe ignores headers"
+ );
+ }
+
+ #[ntex::test]
+ async fn should_use_case_insensitive_header_allowlist_in_router_dedupe_key() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_on_request(|request| {
+ if request.path == "/products" {
+ sleep(Duration::from_millis(50));
+ }
+ None
+ })
+ .build()
+ .start()
+ .await;
+
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ traffic_shaping:
+ all:
+ dedupe_enabled: false
+ router:
+ dedupe:
+ enabled: true
+ headers:
+ include: ["X-Tenant"]
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let query = r#"
+ {
+ topProducts {
+ name
+ price
+ }
+ }
+ "#;
+
+ let (response_a, response_b) = futures::join!(
+ router.send_graphql_request(
+ query,
+ None,
+ some_header_map! {
+ "x-tenant" => "acme",
+ "authorization" => "Bearer token-a"
+ },
+ ),
+ router.send_graphql_request(
+ query,
+ None,
+ some_header_map! {
+ "X-TENANT" => "acme",
+ "authorization" => "Bearer token-b"
+ },
+ )
+ );
+
+ assert!(
+ response_a.status().is_success(),
+ "Expected first request 200 OK"
+ );
+ assert!(
+ response_b.status().is_success(),
+ "Expected second request 200 OK"
+ );
+
+ let products_requests = subgraphs
+ .get_requests_log("products")
+ .unwrap_or_default()
+ .len();
+
+ assert_eq!(
+ products_requests, 1,
+ "expected exactly one products subgraph request when allowlisted header matches case-insensitively"
+ );
+ }
}
diff --git a/lib/executor/src/executors/dedupe.rs b/lib/executor/src/executors/dedupe.rs
index 1729ed7d4..bd18e5f65 100644
--- a/lib/executor/src/executors/dedupe.rs
+++ b/lib/executor/src/executors/dedupe.rs
@@ -1,7 +1,6 @@
-use ahash::AHasher;
use ahash::RandomState;
use std::collections::BTreeMap;
-use std::hash::{BuildHasherDefault, Hash, Hasher};
+use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::OnceLock;
use xxhash_rust::xxh3::Xxh3;
@@ -34,8 +33,6 @@ impl SendRequestOpts<'_> {
}
}
-pub type ABuildHasher = BuildHasherDefault;
-
static LEADER_COUNTER: AtomicU64 = AtomicU64::new(1);
static LEADER_SALT: OnceLock = OnceLock::new();
diff --git a/lib/executor/src/executors/http.rs b/lib/executor/src/executors/http.rs
index fa81c9997..4fb11fda0 100644
--- a/lib/executor/src/executors/http.rs
+++ b/lib/executor/src/executors/http.rs
@@ -10,6 +10,7 @@ use crate::plugin_context::PluginRequestState;
use crate::plugin_trait::{EndControlFlow, StartControlFlow};
use crate::response::subgraph_response::SubgraphResponse;
use hive_router_config::HiveRouterConfig;
+use hive_router_internal::inflight::InFlightRole;
use hive_router_internal::telemetry::metrics::catalog::values::GraphQLResponseStatus;
use hive_router_internal::telemetry::metrics::http_client_metrics::HttpClientRequestStateCapture;
use hive_router_internal::telemetry::TelemetryContext;
@@ -388,30 +389,17 @@ impl SubgraphExecutor for HTTPSubgraphExecutor {
);
let result: Result<_, SubgraphExecutorError> = async {
- // Clone the cell from the map, dropping the lock from the DashMap immediately.
- // Prevents any deadlocks.
- let cell = self
- .in_flight_requests
- .entry(fingerprint)
- .or_default()
- .clone();
- // Mark it as a joiner span by default.
- let mut is_leader = false;
+ let claim = self.in_flight_requests.claim(fingerprint);
let mut leader_http_request_capture = None;
- let (shared_response, leader_id) = cell
+ let (shared_response, role) = claim
.get_or_try_init(|| async {
- // Override the span to be a leader span for this request.
- is_leader = true;
let res = {
// This unwrap is safe because the semaphore is never closed during the application's lifecycle.
// `acquire()` only fails if the semaphore is closed, so this will always return `Ok`.
let _permit = self.semaphore.acquire().await.unwrap();
send_request(send_request_opts).await
};
- // It's important to remove the entry from the map before returning the result.
- // This ensures that once the OnceCell is set, no future requests can join it.
- // The cache is for the lifetime of the in-flight request only.
- self.in_flight_requests.remove(&fingerprint);
+
res.map(|fetched_response| {
leader_http_request_capture =
Some(HttpRequestTelemetryCapture {
@@ -425,10 +413,14 @@ impl SubgraphExecutor for HTTPSubgraphExecutor {
})
.await?;
- if is_leader {
- inflight_span.record_as_leader(leader_id);
+ let (shared_response, leader_id) = shared_response.as_ref();
+ let shared_response = shared_response.clone();
+ let leader_id = *leader_id;
+
+ if role == InFlightRole::Leader {
+ inflight_span.record_as_leader(&leader_id);
} else {
- inflight_span.record_as_joiner(leader_id);
+ inflight_span.record_as_joiner(&leader_id);
}
inflight_span
@@ -436,11 +428,11 @@ impl SubgraphExecutor for HTTPSubgraphExecutor {
deduplication_hint = DeduplicationHint::Deduped {
fingerprint,
- leader_id: *leader_id,
- is_leader,
+ leader_id,
+ is_leader: role == InFlightRole::Leader,
};
- Ok((shared_response.clone(), leader_http_request_capture))
+ Ok((shared_response, leader_http_request_capture))
}
.instrument(inflight_span.clone())
.await;
diff --git a/lib/executor/src/executors/map.rs b/lib/executor/src/executors/map.rs
index 400fc881c..80a2257bf 100644
--- a/lib/executor/src/executors/map.rs
+++ b/lib/executor/src/executors/map.rs
@@ -12,7 +12,8 @@ use hive_router_config::{
use hive_router_internal::expressions::vrl::core::Value as VrlValue;
use hive_router_internal::expressions::{CompileExpression, DurationOrProgram, ExecutableProgram};
use hive_router_internal::{
- expressions::vrl::compiler::Program as VrlProgram, telemetry::TelemetryContext,
+ expressions::vrl::compiler::Program as VrlProgram, inflight::InFlightMap,
+ telemetry::TelemetryContext,
};
use http::Uri;
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
@@ -20,13 +21,12 @@ use hyper_util::{
client::legacy::{connect::HttpConnector, Client},
rt::{TokioExecutor, TokioTimer},
};
-use tokio::sync::{OnceCell, Semaphore};
+use tokio::sync::Semaphore;
use crate::{
execution::client_request_details::ClientRequestDetails,
executors::{
common::{SubgraphExecutionRequest, SubgraphExecutor, SubgraphExecutorBoxedArc},
- dedupe::ABuildHasher,
error::SubgraphExecutorError,
http::{HTTPSubgraphExecutor, HttpClient, SubgraphHttpResponse},
},
@@ -52,8 +52,7 @@ struct ResolvedSubgraphConfig<'a> {
dedupe_enabled: bool,
}
-pub type InflightRequestsMap =
- Arc>, ABuildHasher>>;
+pub type InflightRequestsMap = InFlightMap;
pub struct SubgraphExecutorMap {
executors_by_subgraph: ExecutorsBySubgraphMap,
@@ -102,7 +101,7 @@ impl SubgraphExecutorMap {
client: Arc::new(client),
semaphores_by_origin: Default::default(),
max_connections_per_host,
- in_flight_requests: Arc::new(DashMap::with_hasher(ABuildHasher::default())),
+ in_flight_requests: InFlightMap::default(),
timeouts_by_subgraph: Default::default(),
global_timeout,
telemetry_context,
diff --git a/lib/executor/src/plugins/hooks/on_supergraph_load.rs b/lib/executor/src/plugins/hooks/on_supergraph_load.rs
index f28fde553..5449e1e23 100644
--- a/lib/executor/src/plugins/hooks/on_supergraph_load.rs
+++ b/lib/executor/src/plugins/hooks/on_supergraph_load.rs
@@ -25,6 +25,13 @@ pub struct SupergraphData {
pub supergraph_schema: Arc,
}
+impl SupergraphData {
+ #[inline]
+ pub fn schema_checksum(&self) -> u64 {
+ self.planner.consumer_schema.hash
+ }
+}
+
pub type OnSupergraphLoadResult = Result;
pub struct OnSupergraphLoadStartHookPayload {
diff --git a/lib/internal/Cargo.toml b/lib/internal/Cargo.toml
index 554e286f5..7ddc1df39 100644
--- a/lib/internal/Cargo.toml
+++ b/lib/internal/Cargo.toml
@@ -33,6 +33,7 @@ async-trait = { workspace = true }
futures = { workspace = true }
thiserror = { workspace = true }
ahash = { workspace = true }
+dashmap = { workspace = true }
tokio-stream = "0.1.18"
# telemetry
diff --git a/lib/internal/src/inflight.rs b/lib/internal/src/inflight.rs
new file mode 100644
index 000000000..7ac3c2237
--- /dev/null
+++ b/lib/internal/src/inflight.rs
@@ -0,0 +1,147 @@
+use std::{
+ future::Future,
+ hash::{BuildHasher, BuildHasherDefault, Hash},
+ sync::Arc,
+};
+
+use ahash::AHasher;
+use dashmap::{mapref::entry::Entry, DashMap};
+use tokio::sync::OnceCell;
+
+pub type ABuildHasher = BuildHasherDefault;
+type InFlightValue = Arc;
+type InFlightCell = Arc>>;
+type InFlightInnerMap = DashMap, S>;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum InFlightRole {
+ Leader,
+ Joiner,
+}
+
+pub struct InFlightMap {
+ inner: Arc>,
+}
+
+impl Clone for InFlightMap {
+ fn clone(&self) -> Self {
+ Self {
+ inner: self.inner.clone(),
+ }
+ }
+}
+
+impl Default for InFlightMap
+where
+ K: Eq + Hash,
+{
+ fn default() -> Self {
+ Self::with_hasher(ABuildHasher::default())
+ }
+}
+
+impl InFlightMap
+where
+ K: Eq + Hash,
+ S: BuildHasher + Clone,
+{
+ #[inline]
+ pub fn with_hasher(hasher: S) -> Self {
+ Self {
+ inner: Arc::new(DashMap::with_hasher(hasher)),
+ }
+ }
+
+ #[inline]
+ pub fn claim(&self, key: K) -> InFlightClaim
+ where
+ K: Clone,
+ {
+ match self.inner.entry(key.clone()) {
+ Entry::Occupied(entry) => InFlightClaim {
+ key,
+ cell: entry.get().clone(),
+ map: self.clone(),
+ },
+ Entry::Vacant(entry) => {
+ let cell = Arc::new(OnceCell::new());
+ entry.insert(cell.clone());
+ InFlightClaim {
+ key,
+ cell,
+ map: self.clone(),
+ }
+ }
+ }
+ }
+
+ #[inline]
+ pub fn remove(&self, key: &K) {
+ self.inner.remove(key);
+ }
+}
+
+pub struct InFlightClaim {
+ key: K,
+ cell: InFlightCell,
+ map: InFlightMap,
+}
+
+impl InFlightClaim
+where
+ K: Eq + Hash + Clone,
+ S: BuildHasher + Clone,
+{
+ #[inline]
+ pub async fn get_or_try_init(self, init: F) -> Result<(Arc, InFlightRole), E>
+ where
+ F: FnOnce() -> Fut,
+ Fut: Future>,
+ {
+ let mut did_initialize = false;
+ let key = self.key.clone();
+ let map = self.map.clone();
+
+ let value = self
+ .cell
+ .get_or_try_init(|| {
+ did_initialize = true;
+ async {
+ let _cleanup = InFlightCleanupGuard { key, map };
+ init().await.map(Arc::new)
+ }
+ })
+ .await?
+ .clone();
+
+ let role = if did_initialize {
+ InFlightRole::Leader
+ } else {
+ InFlightRole::Joiner
+ };
+
+ Ok((value, role))
+ }
+}
+
+struct InFlightCleanupGuard
+where
+ K: Eq + Hash,
+ S: BuildHasher + Clone,
+{
+ key: K,
+ map: InFlightMap,
+}
+
+impl Drop for InFlightCleanupGuard
+where
+ K: Eq + Hash,
+ S: BuildHasher + Clone,
+{
+ fn drop(&mut self) {
+ // It's important to remove the entry from the map before returning the result.
+ // This ensures that once the OnceCell is set, no future requests can join it.
+ // The cache is for the lifetime of the in-flight request only.
+ self.map.remove(&self.key);
+ }
+}
diff --git a/lib/internal/src/lib.rs b/lib/internal/src/lib.rs
index 5a3918101..7b6d626d0 100644
--- a/lib/internal/src/lib.rs
+++ b/lib/internal/src/lib.rs
@@ -3,6 +3,7 @@ pub mod background_tasks;
pub mod expressions;
pub mod graphql;
pub mod http;
+pub mod inflight;
pub mod telemetry;
pub type BoxError = Box;
diff --git a/lib/router-config/src/traffic_shaping.rs b/lib/router-config/src/traffic_shaping.rs
index a2c9a9bc1..8b362b6c6 100644
--- a/lib/router-config/src/traffic_shaping.rs
+++ b/lib/router-config/src/traffic_shaping.rs
@@ -3,6 +3,8 @@ use std::{collections::HashMap, time::Duration};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
+use crate::primitives::http_header::HttpHeaderName;
+
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub struct TrafficShapingConfig {
@@ -44,6 +46,10 @@ fn default_dedupe_enabled() -> bool {
true
}
+fn default_router_dedupe_enabled() -> bool {
+ false
+}
+
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub struct TrafficShapingExecutorSubgraphConfig {
@@ -160,6 +166,9 @@ impl Default for TrafficShapingExecutorGlobalConfig {
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub struct TrafficShapingRouterConfig {
+ #[serde(default)]
+ pub dedupe: TrafficShapingRouterDedupeConfig,
+
/// Optional timeout configuration for incoming requests to the router.
/// It starts from the moment the request is received by the router,
/// and includes the entire processing of the request (validation, execution, etc.) until a response is sent back to the client.
@@ -173,6 +182,58 @@ pub struct TrafficShapingRouterConfig {
pub request_timeout: Duration,
}
+#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
+#[serde(deny_unknown_fields)]
+pub struct TrafficShapingRouterDedupeConfig {
+ /// Enables/disables in-flight request deduplication at the router endpoint level.
+ ///
+ /// When enabled, identical incoming GraphQL query requests that are processed at the same time
+ /// share the same in-flight execution result.
+ #[serde(default = "default_router_dedupe_enabled")]
+ pub enabled: bool,
+
+ /// Header configuration participating in the dedupe key.
+ ///
+ /// Accepted forms:
+ /// - `all`
+ /// - `none`
+ /// - `{ include: ["authorization", "cookie"] }`
+ ///
+ /// Header names are case-insensitive and validated as standard HTTP header names.
+ #[serde(default)]
+ pub headers: TrafficShapingRouterDedupeHeadersConfig,
+}
+
+#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Copy, PartialEq, Eq, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum TrafficShapingRouterDedupeHeadersKeyword {
+ #[default]
+ All,
+ None,
+}
+
+#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
+#[serde(untagged)]
+pub enum TrafficShapingRouterDedupeHeadersConfig {
+ Keyword(TrafficShapingRouterDedupeHeadersKeyword),
+ Include { include: Vec },
+}
+
+impl Default for TrafficShapingRouterDedupeHeadersConfig {
+ fn default() -> Self {
+ Self::Keyword(TrafficShapingRouterDedupeHeadersKeyword::All)
+ }
+}
+
+impl Default for TrafficShapingRouterDedupeConfig {
+ fn default() -> Self {
+ Self {
+ enabled: default_router_dedupe_enabled(),
+ headers: Default::default(),
+ }
+ }
+}
+
fn default_router_request_timeout() -> Duration {
Duration::from_secs(60)
}
@@ -180,6 +241,7 @@ fn default_router_request_timeout() -> Duration {
impl Default for TrafficShapingRouterConfig {
fn default() -> Self {
Self {
+ dedupe: Default::default(),
request_timeout: default_router_request_timeout(),
}
}
From d2e46f43e3682978f2ba66277645fafa41996459 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Mar 2026 11:52:02 +0000
Subject: [PATCH 03/76] Add patch changeset for zmij migration
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
Agent-Logs-Url: https://github.com/graphql-hive/router/sessions/697ee76f-0b27-4111-9902-61d62a29e7b7
---
.changeset/replace-ryu-with-zmij.md | 7 +++++++
1 file changed, 7 insertions(+)
create mode 100644 .changeset/replace-ryu-with-zmij.md
diff --git a/.changeset/replace-ryu-with-zmij.md b/.changeset/replace-ryu-with-zmij.md
new file mode 100644
index 000000000..7a62f1d38
--- /dev/null
+++ b/.changeset/replace-ryu-with-zmij.md
@@ -0,0 +1,7 @@
+---
+graphql-tools: patch
+hive-router-plan-executor: patch
+---
+
+Replace direct `ryu` usage with `zmij` for float formatting in `graphql-tools` and `hive-router-plan-executor`.
+
From 46be1a199f6891dc63fc14094efea05f5f4d083d Mon Sep 17 00:00:00 2001
From: Dotan Simha
Date: Wed, 25 Mar 2026 11:27:09 +0200
Subject: [PATCH 04/76] fix(ci): use another supergraph for graphql-over-http
audit (#877) Co-authored-by: kamilkisiela
<8167190+kamilkisiela@users.noreply.github.com>
---
audits/graphql-over-http.supergraph.graphql | 125 ++++++++++++++++++++
audits/graphql-over-http.test.ts | 4 +-
audits/package.json | 4 +-
3 files changed, 129 insertions(+), 4 deletions(-)
create mode 100644 audits/graphql-over-http.supergraph.graphql
diff --git a/audits/graphql-over-http.supergraph.graphql b/audits/graphql-over-http.supergraph.graphql
new file mode 100644
index 000000000..98413b558
--- /dev/null
+++ b/audits/graphql-over-http.supergraph.graphql
@@ -0,0 +1,125 @@
+
+
+ schema
+ @link(url: "https://specs.apollo.dev/link/v1.0")
+ @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
+
+
+
+
+
+
+
+ {
+ query: Query
+
+
+ }
+
+
+ directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
+
+ directive @join__graph(name: String!, url: String!) on ENUM_VALUE
+
+
+ directive @join__field(
+ graph: join__Graph
+ requires: join__FieldSet
+ provides: join__FieldSet
+ type: String
+ external: Boolean
+ override: String
+ usedOverridden: Boolean
+
+
+ ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
+
+
+
+ directive @join__implements(
+ graph: join__Graph!
+ interface: String!
+ ) repeatable on OBJECT | INTERFACE
+
+ directive @join__type(
+ graph: join__Graph!
+ key: join__FieldSet
+ extension: Boolean! = false
+ resolvable: Boolean! = true
+ isInterfaceObject: Boolean! = false
+ ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
+
+ directive @join__unionMember(
+ graph: join__Graph!
+ member: String!
+ ) repeatable on UNION
+
+ scalar join__FieldSet
+
+
+
+ directive @link(
+ url: String
+ as: String
+ for: link__Purpose
+ import: [link__Import]
+ ) repeatable on SCHEMA
+
+ scalar link__Import
+
+ enum link__Purpose {
+ """
+ `SECURITY` features provide metadata necessary to securely resolve fields.
+ """
+ SECURITY
+
+ """
+ `EXECUTION` features provide metadata necessary for operation execution.
+ """
+ EXECUTION
+ }
+
+
+
+
+
+
+
+
+enum join__Graph {
+ PRODUCTS @join__graph(
+ name: "products"
+ url: "https://federation-demo.theguild.workers.dev/products"
+ )
+ REVIEWS @join__graph(name: "reviews", url: "https://federation-demo.theguild.workers.dev/reviews")
+ USERS @join__graph(name: "users", url: "https://federation-demo.theguild.workers.dev/users")
+}
+
+type Query @join__type(graph: PRODUCTS) @join__type(graph: REVIEWS) @join__type(graph: USERS) {
+ topProducts(first: Int = 5) : [Product] @join__field(graph: PRODUCTS)
+ me: User @join__field(graph: USERS)
+ user(id: ID!) : User @join__field(graph: USERS)
+ users: [User] @join__field(graph: USERS)
+}
+
+type Product @join__type(graph: PRODUCTS, key: "upc") @join__type(graph: REVIEWS, key: "upc") {
+ upc: String!
+ name: String @join__field(graph: PRODUCTS)
+ price: Int @join__field(graph: PRODUCTS)
+ weight: Int @join__field(graph: PRODUCTS)
+ reviews: [Review] @join__field(graph: REVIEWS)
+}
+
+type Review @join__type(graph: REVIEWS, key: "id") {
+ id: ID!
+ body: String
+ author: User @join__field(graph: REVIEWS, provides: "username")
+ product: Product
+}
+
+type User @join__type(graph: REVIEWS, key: "id") @join__type(graph: USERS, key: "id") {
+ username: String @join__field(graph: REVIEWS, external: true) @join__field(graph: USERS)
+ id: ID!
+ reviews: [Review] @join__field(graph: REVIEWS)
+ name: String @join__field(graph: USERS)
+}
diff --git a/audits/graphql-over-http.test.ts b/audits/graphql-over-http.test.ts
index 175ccd50b..1c7434b7d 100644
--- a/audits/graphql-over-http.test.ts
+++ b/audits/graphql-over-http.test.ts
@@ -8,7 +8,7 @@ describe("GraphQL over HTTP", () => {
})) {
it(audit.name, async () => {
const result = await audit.fn();
- assert.equal(result.status, "ok", (result as AuditFail).reason);
+ assert.equal(result.status, "ok", `${audit.name} failed: ${(result as AuditFail).reason}`);
});
}
it('preserve the order of the selection set', async () => {
@@ -38,4 +38,4 @@ describe("GraphQL over HTTP", () => {
"Order of selection set is not preserved"
);
})
-})
\ No newline at end of file
+})
diff --git a/audits/package.json b/audits/package.json
index a97c7ea43..ea6d9386a 100644
--- a/audits/package.json
+++ b/audits/package.json
@@ -5,7 +5,7 @@
"scripts": {
"test:federation-all": "graphql-federation-audit test --run-script=\"./run-router.sh\" --graphql=\"http://localhost:4000/graphql\" --healthcheck=\"http://localhost:4000/health\" --junit",
"test:federation-single": "graphql-federation-audit test-suite --run-script=\"./run-router.sh\" --graphql=\"http://localhost:4000/graphql\" --healthcheck=\"http://localhost:4000/health\" --write=\"federation-audit-results.txt\"",
- "start:test-router": "cd .. && SUPERGRAPH_FILE_PATH=lib/query-planner/fixture/spotify-supergraph.graphql cargo router",
+ "start:test-router": "cd .. && SUPERGRAPH_FILE_PATH=audits/graphql-over-http.supergraph.graphql cargo router",
"test:graphql-over-http": "node --test graphql-over-http.test.ts",
"test-junit:graphql-over-http": "node --test --test-reporter=junit --test-reporter-destination=\"./reports/graphql-over-http.xml\" graphql-over-http.test.ts",
"ci:test:graphql-over-http": "start-server-and-test start:test-router http://localhost:4000 test-junit:graphql-over-http"
@@ -18,4 +18,4 @@
"start-server-and-test": "2.1.5"
},
"packageManager": "npm@11.11.1"
-}
\ No newline at end of file
+}
From 51c001f89d38615a033e0af4c170a065ac5c215b Mon Sep 17 00:00:00 2001
From: Kamil Kisiela
Date: Thu, 26 Mar 2026 11:05:37 +0100
Subject: [PATCH 05/76] chore: add inbound dedupe benchmark scenario (#878)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
add a dedicated benchmark config and CI scenario for inbound
deduplication so it can be compared explicitly with `main`.
the `default` and `plugins` benchmark configs no longer enable inbound
deduplication, and the inbound-dedupe case now lives in its own
`dedupe-inbound` config.
---
✨ Let Copilot coding agent [set things up for
you](https://github.com/graphql-hive/router/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.github/workflows/ci.yaml | 6 ++++++
bench/configs/dedupe-inbound.config.yaml | 10 ++++++++++
bench/configs/default.config.yaml | 4 ----
bench/configs/plugins.config.yaml | 2 +-
4 files changed, 17 insertions(+), 5 deletions(-)
create mode 100644 bench/configs/dedupe-inbound.config.yaml
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index f347e5356..dab46ced5 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -153,6 +153,12 @@ jobs:
binary: "hive_router"
config: "bench/configs/default.config.yaml"
compare_with_default: true
+ - name: "dedupe-inbound"
+ args: ""
+ package: "hive-router"
+ binary: "hive_router"
+ config: "bench/configs/dedupe-inbound.config.yaml"
+ compare_with_default: false
- name: "no-dedupe"
args: ""
package: "hive-router"
diff --git a/bench/configs/dedupe-inbound.config.yaml b/bench/configs/dedupe-inbound.config.yaml
new file mode 100644
index 000000000..49961d576
--- /dev/null
+++ b/bench/configs/dedupe-inbound.config.yaml
@@ -0,0 +1,10 @@
+# yaml-language-server: $schema=../../router-config.schema.json
+supergraph:
+ source: file
+ path: ../supergraph.graphql
+log:
+ level: info
+traffic_shaping:
+ router:
+ dedupe:
+ enabled: true
diff --git a/bench/configs/default.config.yaml b/bench/configs/default.config.yaml
index 1c4bba3e1..26a68560a 100644
--- a/bench/configs/default.config.yaml
+++ b/bench/configs/default.config.yaml
@@ -4,7 +4,3 @@ supergraph:
path: ../supergraph.graphql
log:
level: info
-traffic_shaping:
- router:
- dedupe:
- enabled: true # it's opt-in
diff --git a/bench/configs/plugins.config.yaml b/bench/configs/plugins.config.yaml
index 91cea452b..e0e66775b 100644
--- a/bench/configs/plugins.config.yaml
+++ b/bench/configs/plugins.config.yaml
@@ -6,4 +6,4 @@ log:
level: info
plugins:
my_plugin:
- enabled: true
\ No newline at end of file
+ enabled: true
From a34896d9ec747b7dca223296693d3944c6dca977 Mon Sep 17 00:00:00 2001
From: Dotan Simha
Date: Thu, 26 Mar 2026 13:36:28 +0200
Subject: [PATCH 06/76] fix(disable_introspection): allow to query `__typename`
when introspection is disabled (#872)
Related: https://github.com/graphql-hive/router/issues/871
Closes ROUTER-278
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/typename_introspection_fix.md | 8 ++
.../src/pipeline/introspection_policy.rs | 2 +
e2e/src/disable_introspection.rs | 96 +++++++++++++++++++
lib/executor/src/introspection/partition.rs | 13 +--
4 files changed, 107 insertions(+), 12 deletions(-)
create mode 100644 .changeset/typename_introspection_fix.md
diff --git a/.changeset/typename_introspection_fix.md b/.changeset/typename_introspection_fix.md
new file mode 100644
index 000000000..9c9c87815
--- /dev/null
+++ b/.changeset/typename_introspection_fix.md
@@ -0,0 +1,8 @@
+---
+hive-router-plan-executor: patch
+hive-router: patch
+---
+
+# Introspection Bug Fix
+
+Fixed an issue where, when introspection is disabled, querying root `__typename` was incorrectly rejected (https://github.com/graphql-hive/router/issues/871).
diff --git a/bin/router/src/pipeline/introspection_policy.rs b/bin/router/src/pipeline/introspection_policy.rs
index 59311df24..fd5caadb7 100644
--- a/bin/router/src/pipeline/introspection_policy.rs
+++ b/bin/router/src/pipeline/introspection_policy.rs
@@ -5,6 +5,7 @@ use hive_router_internal::expressions::{
values::boolean::BooleanOrProgram, CompileExpression, ExpressionCompileError,
};
use hive_router_plan_executor::execution::client_request_details;
+use tracing::debug;
use vrl::core::Value as VrlValue;
use crate::pipeline::error::PipelineError;
@@ -35,6 +36,7 @@ pub fn handle_introspection_policy(
.map_err(|e| PipelineError::IntrospectionPermissionEvaluationError(e.to_string()))?;
if !is_enabled {
+ debug!("graphql request rejected because introspection is disabled");
Err(PipelineError::IntrospectionDisabled)
} else {
Ok(())
diff --git a/e2e/src/disable_introspection.rs b/e2e/src/disable_introspection.rs
index 81b3c6ad4..022bfbf78 100644
--- a/e2e/src/disable_introspection.rs
+++ b/e2e/src/disable_introspection.rs
@@ -4,6 +4,102 @@ mod disable_introspection_e2e_tests {
some_header_map, ClientResponseExt, EnvVarsGuard, TestRouter, TestSubgraphs,
};
+ #[ntex::test]
+ async fn should_not_allow_mixed_fields() {
+ let _env_var_guard = EnvVarsGuard::new()
+ .set("DISABLE_INTROSPECTION", "true")
+ .apply()
+ .await;
+
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .file_config("configs/disable_introspection_env.yaml")
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ "{ me { __typename id } __schema { queryType { name } } }",
+ None,
+ None,
+ )
+ .await;
+
+ insta::assert_snapshot!(res.json_body_string_pretty().await, @r###"
+ {
+ "errors": [
+ {
+ "message": "Introspection queries are disabled",
+ "extensions": {
+ "code": "INTROSPECTION_DISABLED"
+ }
+ }
+ ]
+ }
+ "###);
+ }
+
+ #[ntex::test]
+ async fn should_still_allow_nested_typename_when_disabled() {
+ let _env_var_guard = EnvVarsGuard::new()
+ .set("DISABLE_INTROSPECTION", "true")
+ .apply()
+ .await;
+
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .file_config("configs/disable_introspection_env.yaml")
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request("{ me { __typename id } }", None, None)
+ .await;
+
+ insta::assert_snapshot!(res.json_body_string_pretty().await, @r###"
+ {
+ "data": {
+ "me": {
+ "__typename": "User",
+ "id": "1"
+ }
+ }
+ }
+ "###);
+ }
+
+ #[ntex::test]
+ async fn should_still_allow_root_typename_when_disabled() {
+ let _env_var_guard = EnvVarsGuard::new()
+ .set("DISABLE_INTROSPECTION", "true")
+ .apply()
+ .await;
+
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .file_config("configs/disable_introspection_env.yaml")
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request("{ __typename }", None, None)
+ .await;
+
+ insta::assert_snapshot!(res.json_body_string_pretty().await, @r###"
+ {
+ "data": {
+ "__typename": "Query"
+ }
+ }
+ "###);
+ }
+
#[ntex::test]
async fn should_disable_based_on_env_var() {
let _env_var_guard = EnvVarsGuard::new()
diff --git a/lib/executor/src/introspection/partition.rs b/lib/executor/src/introspection/partition.rs
index 86231d6c7..adae381f4 100644
--- a/lib/executor/src/introspection/partition.rs
+++ b/lib/executor/src/introspection/partition.rs
@@ -57,12 +57,6 @@ enum SelectionSetLevel {
Nested,
}
-impl SelectionSetLevel {
- fn is_root(&self) -> bool {
- matches!(self, SelectionSetLevel::Root)
- }
-}
-
fn partition_selection_set(
selection_set: SelectionSet,
level: SelectionSetLevel,
@@ -73,12 +67,7 @@ fn partition_selection_set(
for item in selection_set.items {
match item {
SelectionItem::Field(field) => {
- // pass root level __typename to introspection
- if (level.is_root() && field.name.starts_with("__"))
- ||
- // do NOT pass non-root level __typename to introspection
- field.name.starts_with("__") && field.name != "__typename"
- {
+ if field.name.starts_with("__") && field.name != "__typename" {
introspection_items.push(SelectionItem::Field(field));
} else {
let is_leaf = field.is_leaf();
From 1ae81ec19fa4712b190c8cbcb94e2ac2480dfc28 Mon Sep 17 00:00:00 2001
From: "knope-bot[bot]" <152252888+knope-bot[bot]@users.noreply.github.com>
Date: Thu, 26 Mar 2026 17:31:08 +0200
Subject: [PATCH 07/76] chore(release): router crates and artifacts (#874)
Co-authored-by: knope-bot[bot] <152252888+knope-bot[bot]@users.noreply.github.com>
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/router-inflight-dedupe.md | 48 ----------------------
.changeset/typename_introspection_fix.md | 8 ----
Cargo.lock | 8 ++--
bin/router/CHANGELOG.md | 51 ++++++++++++++++++++++++
bin/router/Cargo.toml | 8 ++--
lib/executor/CHANGELOG.md | 51 ++++++++++++++++++++++++
lib/executor/Cargo.toml | 6 +--
lib/internal/CHANGELOG.md | 45 +++++++++++++++++++++
lib/internal/Cargo.toml | 4 +-
lib/router-config/CHANGELOG.md | 45 +++++++++++++++++++++
lib/router-config/Cargo.toml | 2 +-
11 files changed, 206 insertions(+), 70 deletions(-)
delete mode 100644 .changeset/router-inflight-dedupe.md
delete mode 100644 .changeset/typename_introspection_fix.md
diff --git a/.changeset/router-inflight-dedupe.md b/.changeset/router-inflight-dedupe.md
deleted file mode 100644
index d2d3acf3b..000000000
--- a/.changeset/router-inflight-dedupe.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-hive-router: minor
-hive-router-config: minor
-hive-router-internal: minor
-hive-router-plan-executor: minor
----
-
-# Add router-level in-flight request deduplication for GraphQL queries
-
-The router now supports deduplicating identical incoming GraphQL query requests while they are in flight, so concurrent duplicates can share one execution result.
-
-## Configuration
-
-A new router traffic-shaping section is available:
-
-- `traffic_shaping.router.dedupe.enabled` (default: `false`)
-- `traffic_shaping.router.dedupe.headers` as `all`, `none`, or `{ include: [...] }` (default: `all`)
-
-Supported header config shapes:
-
-```yaml
-headers: all
-```
-
-```yaml
-headers: none
-```
-
-```yaml
-headers:
- include:
- - authorization
- - cookie
-```
-
-Header names are validated and normalized as standard HTTP header names.
-
-## Deduplication key behavior
-
-The router dedupe fingerprint includes:
-
-- request method and path
-- selected request headers (based on dedupe header policy)
-- normalized operation hash
-- GraphQL variables hash
-- schema checksum
-- GraphQL extensions
-
diff --git a/.changeset/typename_introspection_fix.md b/.changeset/typename_introspection_fix.md
deleted file mode 100644
index 9c9c87815..000000000
--- a/.changeset/typename_introspection_fix.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-hive-router-plan-executor: patch
-hive-router: patch
----
-
-# Introspection Bug Fix
-
-Fixed an issue where, when introspection is disabled, querying root `__typename` was incorrectly rejected (https://github.com/graphql-hive/router/issues/871).
diff --git a/Cargo.lock b/Cargo.lock
index 937f2c062..a622b34b1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2298,7 +2298,7 @@ dependencies = [
[[package]]
name = "hive-router"
-version = "0.0.42"
+version = "0.0.43"
dependencies = [
"ahash",
"anyhow",
@@ -2354,7 +2354,7 @@ dependencies = [
[[package]]
name = "hive-router-config"
-version = "0.0.25"
+version = "0.0.26"
dependencies = [
"config",
"envconfig",
@@ -2376,7 +2376,7 @@ dependencies = [
[[package]]
name = "hive-router-internal"
-version = "0.0.14"
+version = "0.0.15"
dependencies = [
"ahash",
"async-trait",
@@ -2416,7 +2416,7 @@ dependencies = [
[[package]]
name = "hive-router-plan-executor"
-version = "6.8.0"
+version = "6.9.0"
dependencies = [
"ahash",
"async-trait",
diff --git a/bin/router/CHANGELOG.md b/bin/router/CHANGELOG.md
index 37d1afcab..9c70cc1ce 100644
--- a/bin/router/CHANGELOG.md
+++ b/bin/router/CHANGELOG.md
@@ -116,6 +116,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Other
- *(deps)* update release-plz/action action to v0.5.113 ([#389](https://github.com/graphql-hive/router/pull/389))
+## 0.0.43 (2026-03-26)
+
+### Features
+
+#### Add router-level in-flight request deduplication for GraphQL queries
+
+The router now supports deduplicating identical incoming GraphQL query requests while they are in flight, so concurrent duplicates can share one execution result.
+
+### Configuration
+
+A new router traffic-shaping section is available:
+
+- `traffic_shaping.router.dedupe.enabled` (default: `false`)
+- `traffic_shaping.router.dedupe.headers` as `all`, `none`, or `{ include: [...] }` (default: `all`)
+
+Supported header config shapes:
+
+```yaml
+headers: all
+```
+
+```yaml
+headers: none
+```
+
+```yaml
+headers:
+ include:
+ - authorization
+ - cookie
+```
+
+Header names are validated and normalized as standard HTTP header names.
+
+### Deduplication key behavior
+
+The router dedupe fingerprint includes:
+
+- request method and path
+- selected request headers (based on dedupe header policy)
+- normalized operation hash
+- GraphQL variables hash
+- schema checksum
+- GraphQL extensions
+
+### Fixes
+
+#### Introspection Bug Fix
+
+Fixed an issue where, when introspection is disabled, querying root `__typename` was incorrectly rejected (https://github.com/graphql-hive/router/issues/871).
+
## 0.0.42 (2026-03-16)
### Features
diff --git a/bin/router/Cargo.toml b/bin/router/Cargo.toml
index 493725cd2..16f480246 100644
--- a/bin/router/Cargo.toml
+++ b/bin/router/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router"
-version = "0.0.42"
+version = "0.0.43"
edition = "2021"
description = "GraphQL router/gateway for Federation"
license = "MIT"
@@ -22,9 +22,9 @@ testing = []
[dependencies]
hive-router-query-planner = { path = "../../lib/query-planner", version = "2.5.0" }
-hive-router-plan-executor = { path = "../../lib/executor", version = "6.8.0" }
-hive-router-config = { path = "../../lib/router-config", version = "0.0.25" }
-hive-router-internal = { path = "../../lib/internal", version = "0.0.14" }
+hive-router-plan-executor = { path = "../../lib/executor", version = "6.9.0" }
+hive-router-config = { path = "../../lib/router-config", version = "0.0.26" }
+hive-router-internal = { path = "../../lib/internal", version = "0.0.15" }
hive-console-sdk = { path = "../../lib/hive-console-sdk", version = "0.3.8" }
graphql-tools = { path = "../../lib/graphql-tools", version = "0.5.3" }
diff --git a/lib/executor/CHANGELOG.md b/lib/executor/CHANGELOG.md
index da3d033ff..dec2c0964 100644
--- a/lib/executor/CHANGELOG.md
+++ b/lib/executor/CHANGELOG.md
@@ -94,6 +94,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Other
- *(deps)* update release-plz/action action to v0.5.113 ([#389](https://github.com/graphql-hive/router/pull/389))
+## 6.9.0 (2026-03-26)
+
+### Features
+
+#### Add router-level in-flight request deduplication for GraphQL queries
+
+The router now supports deduplicating identical incoming GraphQL query requests while they are in flight, so concurrent duplicates can share one execution result.
+
+### Configuration
+
+A new router traffic-shaping section is available:
+
+- `traffic_shaping.router.dedupe.enabled` (default: `false`)
+- `traffic_shaping.router.dedupe.headers` as `all`, `none`, or `{ include: [...] }` (default: `all`)
+
+Supported header config shapes:
+
+```yaml
+headers: all
+```
+
+```yaml
+headers: none
+```
+
+```yaml
+headers:
+ include:
+ - authorization
+ - cookie
+```
+
+Header names are validated and normalized as standard HTTP header names.
+
+### Deduplication key behavior
+
+The router dedupe fingerprint includes:
+
+- request method and path
+- selected request headers (based on dedupe header policy)
+- normalized operation hash
+- GraphQL variables hash
+- schema checksum
+- GraphQL extensions
+
+### Fixes
+
+#### Introspection Bug Fix
+
+Fixed an issue where, when introspection is disabled, querying root `__typename` was incorrectly rejected (https://github.com/graphql-hive/router/issues/871).
+
## 6.8.0 (2026-03-16)
### Features
diff --git a/lib/executor/Cargo.toml b/lib/executor/Cargo.toml
index 8287a02bd..62a23eca7 100644
--- a/lib/executor/Cargo.toml
+++ b/lib/executor/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router-plan-executor"
-version = "6.8.0"
+version = "6.9.0"
edition = "2021"
description = "GraphQL query planner executor for Federation specification"
license = "MIT"
@@ -16,8 +16,8 @@ doctest = false
[dependencies]
hive-router-query-planner = { path = "../query-planner", version = "2.5.0" }
-hive-router-config = { path = "../router-config", version = "0.0.25" }
-hive-router-internal = { path = "../internal", version = "0.0.14" }
+hive-router-config = { path = "../router-config", version = "0.0.26" }
+hive-router-internal = { path = "../internal", version = "0.0.15" }
graphql-tools = { path = "../graphql-tools", version = "0.5.3" }
async-trait = { workspace = true }
diff --git a/lib/internal/CHANGELOG.md b/lib/internal/CHANGELOG.md
index 9a20ad2ee..c78023173 100644
--- a/lib/internal/CHANGELOG.md
+++ b/lib/internal/CHANGELOG.md
@@ -1,3 +1,48 @@
+## 0.0.15 (2026-03-26)
+
+### Features
+
+#### Add router-level in-flight request deduplication for GraphQL queries
+
+The router now supports deduplicating identical incoming GraphQL query requests while they are in flight, so concurrent duplicates can share one execution result.
+
+### Configuration
+
+A new router traffic-shaping section is available:
+
+- `traffic_shaping.router.dedupe.enabled` (default: `false`)
+- `traffic_shaping.router.dedupe.headers` as `all`, `none`, or `{ include: [...] }` (default: `all`)
+
+Supported header config shapes:
+
+```yaml
+headers: all
+```
+
+```yaml
+headers: none
+```
+
+```yaml
+headers:
+ include:
+ - authorization
+ - cookie
+```
+
+Header names are validated and normalized as standard HTTP header names.
+
+### Deduplication key behavior
+
+The router dedupe fingerprint includes:
+
+- request method and path
+- selected request headers (based on dedupe header policy)
+- normalized operation hash
+- GraphQL variables hash
+- schema checksum
+- GraphQL extensions
+
## 0.0.14 (2026-03-12)
### Features
diff --git a/lib/internal/Cargo.toml b/lib/internal/Cargo.toml
index 7ddc1df39..0cf55a4c4 100644
--- a/lib/internal/Cargo.toml
+++ b/lib/internal/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router-internal"
-version = "0.0.14"
+version = "0.0.15"
edition = "2021"
description = "GraphQL Hive Router internal crate"
license = "MIT"
@@ -15,7 +15,7 @@ authors = ["The Guild"]
noop_otlp_exporter = []
[dependencies]
-hive-router-config = { path = "../router-config", version = "0.0.25" }
+hive-router-config = { path = "../router-config", version = "0.0.26" }
sonic-rs = { workspace = true }
vrl = { workspace = true }
diff --git a/lib/router-config/CHANGELOG.md b/lib/router-config/CHANGELOG.md
index 2c147f04b..452171037 100644
--- a/lib/router-config/CHANGELOG.md
+++ b/lib/router-config/CHANGELOG.md
@@ -66,6 +66,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- *(hive-router)* fix docker image issues ([#394](https://github.com/graphql-hive/router/pull/394))
+## 0.0.26 (2026-03-26)
+
+### Features
+
+#### Add router-level in-flight request deduplication for GraphQL queries
+
+The router now supports deduplicating identical incoming GraphQL query requests while they are in flight, so concurrent duplicates can share one execution result.
+
+### Configuration
+
+A new router traffic-shaping section is available:
+
+- `traffic_shaping.router.dedupe.enabled` (default: `false`)
+- `traffic_shaping.router.dedupe.headers` as `all`, `none`, or `{ include: [...] }` (default: `all`)
+
+Supported header config shapes:
+
+```yaml
+headers: all
+```
+
+```yaml
+headers: none
+```
+
+```yaml
+headers:
+ include:
+ - authorization
+ - cookie
+```
+
+Header names are validated and normalized as standard HTTP header names.
+
+### Deduplication key behavior
+
+The router dedupe fingerprint includes:
+
+- request method and path
+- selected request headers (based on dedupe header policy)
+- normalized operation hash
+- GraphQL variables hash
+- schema checksum
+- GraphQL extensions
+
## 0.0.25 (2026-03-12)
### Features
diff --git a/lib/router-config/Cargo.toml b/lib/router-config/Cargo.toml
index daef378f0..44e4fa9ab 100644
--- a/lib/router-config/Cargo.toml
+++ b/lib/router-config/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router-config"
-version = "0.0.25"
+version = "0.0.26"
edition = "2021"
publish = true
license = "MIT"
From 1c2d3843e68401632ab3ad152263b5aaa180d01a Mon Sep 17 00:00:00 2001
From: Kamil Kisiela
Date: Sun, 29 Mar 2026 10:06:08 +0200
Subject: [PATCH 08/76] fix(router): fix null field handling in entity request
projection and prevent malformed JSON (#881) Co-authored-by: kamilkisiela
<8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/requires_projection_null.md | 8 +
e2e/src/issues/mod.rs | 205 +++++++++++++++++++++++++
e2e/src/issues/supergraph.880.graphql | 99 ++++++++++++
e2e/src/lib.rs | 2 +
lib/executor/src/projection/request.rs | 190 ++++++++++++++++++++++-
5 files changed, 497 insertions(+), 7 deletions(-)
create mode 100644 .changeset/requires_projection_null.md
create mode 100644 e2e/src/issues/mod.rs
create mode 100644 e2e/src/issues/supergraph.880.graphql
diff --git a/.changeset/requires_projection_null.md b/.changeset/requires_projection_null.md
new file mode 100644
index 000000000..7c3e3ac4d
--- /dev/null
+++ b/.changeset/requires_projection_null.md
@@ -0,0 +1,8 @@
+---
+hive-router-plan-executor: patch
+hive-router: patch
+---
+
+# Fix null field handling in entity request projection
+
+Fixed a bug in entity request projection where present `null` fields could be mishandled, which in some nested projection paths could also lead to malformed JSON output. [#880](https://github.com/graphql-hive/router/issues/880).
diff --git a/e2e/src/issues/mod.rs b/e2e/src/issues/mod.rs
new file mode 100644
index 000000000..18cb285d2
--- /dev/null
+++ b/e2e/src/issues/mod.rs
@@ -0,0 +1,205 @@
+#[cfg(test)]
+mod issues_e2e_tests {
+ use crate::testkit::{ClientResponseExt, TestRouter};
+
+ #[ntex::test]
+ /// https://github.com/graphql-hive/router/issues/880
+ async fn issue_880_null_in_required_field() {
+ let mut server = mockito::Server::new_async().await;
+ let host = server.host_with_port();
+
+ let router = TestRouter::builder()
+ .inline_config(format!(
+ r#"
+ supergraph:
+ source: file
+ path: src/issues/supergraph.880.graphql
+ query_planner:
+ allow_expose: true
+ override_subgraph_urls:
+ accounts:
+ url: "http://{host}/accounts"
+ products:
+ url: "http://{host}/products"
+ "#
+ ))
+ .build()
+ .start()
+ .await;
+
+ // QueryPlan {
+ // Sequence {
+ // Fetch(service: "products") {
+ // {
+ // ad(id: "1") {
+ // id
+ // branch {
+ // __typename
+ // id
+ // }
+ // }
+ // }
+ // },
+ let products_query_mock = server
+ .mock("POST", "/products")
+ .match_request(|r| {
+ let body = r.body().unwrap();
+ let body_str = String::from_utf8(body.clone()).unwrap();
+
+ body_str.contains("ad(")
+ })
+ .expect(1)
+ .with_status(200)
+ .with_header("content-type", "application/json")
+ .with_body(
+ r#"
+ {
+ "data": {
+ "ad": { "id": "1", "branch": { "__typename": "Branch", "id": "branch-1" } }
+ }
+ }
+ "#,
+ )
+ .create();
+
+ // Flatten(path: "ad.branch") {
+ // Fetch(service: "accounts") {
+ // {
+ // ... on Branch {
+ // __typename
+ // id
+ // }
+ // } =>
+ // {
+ // ... on Branch {
+ // contactOptions {
+ // email
+ // user {
+ // name
+ // id
+ // }
+ // }
+ // }
+ // }
+ // },
+ // },
+ let accounts_mock = server
+ .mock("POST", "/accounts")
+ .expect(1)
+ .with_status(200)
+ .with_header("content-type", "application/json")
+ .with_body(
+ r#"{
+ "data": {
+ "_entities": [
+ { "__typename": "Branch", "id": "branch-1", "contactOptions": null }
+ ]
+ }
+ }
+ "#,
+ )
+ .create();
+
+ // Flatten(path: "ad") {
+ // Fetch(service: "products") {
+ // {
+ // ... on Ad {
+ // __typename
+ // branch {
+ // contactOptions {
+ // email
+ // user {
+ // id
+ // name
+ // }
+ // }
+ // }
+ // id
+ // }
+ // } =>
+ // {
+ // ... on Ad {
+ // contactOptions {
+ // email
+ // }
+ // }
+ // }
+ // },
+ // },
+ // },
+ // },
+ let _products_entities_mock_valid_json = server
+ .mock("POST", "/products")
+ .match_request(|r| {
+ let body = r.body().unwrap();
+ let body_str = String::from_utf8(body.clone()).unwrap();
+ if !body_str.contains("$representations") {
+ return false;
+ }
+
+ sonic_rs::from_slice::(body).is_ok()
+ })
+ .expect(1)
+ .with_status(200)
+ .with_header("content-type", "application/json")
+ .with_body(
+ r#"{
+ "data": {
+ "_entities": [
+ { "__typename": "Ad", "contactOptions": null }
+ ]
+ }
+ }
+ "#,
+ )
+ .create();
+ let _products_entities_mock_invalid_json = server
+ .mock("POST", "/products")
+ .match_request(|r| {
+ let body = r.body().unwrap();
+ let body_str = String::from_utf8(body.clone()).unwrap();
+ if !body_str.contains("$representations") {
+ return false;
+ }
+
+ sonic_rs::from_slice::(body).is_err()
+ })
+ .expect(1)
+ .with_status(200)
+ .with_header("content-type", "application/json")
+ .with_body(
+ r#"{
+ "data": {
+ "_entities": [null]
+ },
+ "errors": [
+ { "message": "invalid json" }
+ ]
+ }
+ "#,
+ )
+ .create();
+
+ let res = router
+ .send_graphql_request(
+ "{ ad(id: \"1\") { id contactOptions { email } } }",
+ None,
+ None,
+ )
+ .await;
+
+ accounts_mock.assert();
+ products_query_mock.assert();
+
+ insta::assert_snapshot!(res.json_body_string_pretty().await, @r#"
+ {
+ "data": {
+ "ad": {
+ "id": "1",
+ "contactOptions": null
+ }
+ }
+ }
+ "#);
+ }
+}
diff --git a/e2e/src/issues/supergraph.880.graphql b/e2e/src/issues/supergraph.880.graphql
new file mode 100644
index 000000000..6435329f7
--- /dev/null
+++ b/e2e/src/issues/supergraph.880.graphql
@@ -0,0 +1,99 @@
+schema
+ @link(url: "https://specs.apollo.dev/link/v1.0")
+ @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
+ query: Query
+}
+
+directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
+
+directive @join__field(
+ graph: join__Graph
+ requires: join__FieldSet
+ provides: join__FieldSet
+ type: String
+ external: Boolean
+ override: String
+ usedOverridden: Boolean
+) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
+
+directive @join__graph(name: String!, url: String!) on ENUM_VALUE
+
+directive @join__implements(
+ graph: join__Graph!
+ interface: String!
+) repeatable on OBJECT | INTERFACE
+
+directive @join__type(
+ graph: join__Graph!
+ key: join__FieldSet
+ extension: Boolean! = false
+ resolvable: Boolean! = true
+ isInterfaceObject: Boolean! = false
+) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
+
+directive @join__unionMember(
+ graph: join__Graph!
+ member: String!
+) repeatable on UNION
+
+directive @link(
+ url: String
+ as: String
+ for: link__Purpose
+ import: [link__Import]
+) repeatable on SCHEMA
+
+type Ad @join__type(graph: PRODUCTS, key: "id") {
+ id: ID!
+ branch: Branch
+ contactOptions: ContactOptions
+ @join__field(
+ graph: PRODUCTS
+ requires: "branch { contactOptions { email user { id name } } }"
+ )
+}
+
+type Branch
+ @join__type(graph: ACCOUNTS, key: "id")
+ @join__type(graph: PRODUCTS, key: "id", extension: true, resolvable: false) {
+ id: ID!
+ contactOptions: ContactOptions
+ @join__field(graph: ACCOUNTS)
+ @join__field(graph: PRODUCTS, external: true)
+}
+
+type BranchUser @join__type(graph: ACCOUNTS) @join__type(graph: PRODUCTS) {
+ id: ID!
+ name: String
+}
+
+type ContactOptions @join__type(graph: ACCOUNTS) @join__type(graph: PRODUCTS) {
+ email: String
+ user: BranchUser
+}
+
+scalar join__FieldSet
+
+enum join__Graph {
+ ACCOUNTS @join__graph(name: "accounts", url: "http://0.0.0.0:4200/accounts")
+ PRODUCTS @join__graph(name: "products", url: "http://0.0.0.0:4200/products")
+}
+
+scalar link__Import
+
+enum link__Purpose {
+ """
+ `SECURITY` features provide metadata necessary to securely resolve fields.
+ """
+ SECURITY
+
+ """
+ `EXECUTION` features provide metadata necessary for operation execution.
+ """
+ EXECUTION
+}
+
+type Query @join__type(graph: ACCOUNTS) @join__type(graph: PRODUCTS) {
+ branch(id: ID!): Branch @join__field(graph: ACCOUNTS)
+ ad(id: ID!): Ad @join__field(graph: PRODUCTS)
+}
diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs
index 1ed27f53d..a8cc50597 100644
--- a/e2e/src/lib.rs
+++ b/e2e/src/lib.rs
@@ -23,6 +23,8 @@ mod http;
#[cfg(test)]
mod introspection;
#[cfg(test)]
+mod issues;
+#[cfg(test)]
mod jwt;
#[cfg(test)]
mod max_aliases;
diff --git a/lib/executor/src/projection/request.rs b/lib/executor/src/projection/request.rs
index f1ba213ba..a8ca6e202 100644
--- a/lib/executor/src/projection/request.rs
+++ b/lib/executor/src/projection/request.rs
@@ -7,8 +7,8 @@ use crate::{
projection::response::serialize_value_to_buffer,
response::value::Value,
utils::consts::{
- CLOSE_BRACE, CLOSE_BRACKET, COLON, COMMA, FALSE, OPEN_BRACE, OPEN_BRACKET, QUOTE, TRUE,
- TYPENAME, TYPENAME_FIELD_NAME,
+ CLOSE_BRACE, CLOSE_BRACKET, COLON, COMMA, FALSE, NULL, OPEN_BRACE, OPEN_BRACKET, QUOTE,
+ TRUE, TYPENAME, TYPENAME_FIELD_NAME,
},
};
@@ -143,14 +143,18 @@ fn project_requires_map_mut(
.ok()
.map(|idx| &entity_obj[idx].1)
}
- })
- .unwrap_or(&Value::Null);
+ });
- if original.is_null() {
+ let Some(original) = original else {
continue;
- }
+ };
+
+ // In most requests, required fields are present and projection succeeds.
+ // If projection ends up writing nothing, we rewind to this offset.
+ let mut object_start_offset = None;
if *first {
+ object_start_offset = Some(buffer.len());
write_response_key(parent_first, parent_response_key, buffer);
buffer.put(OPEN_BRACE);
// Write __typename only if the object has other fields
@@ -164,11 +168,18 @@ fn project_requires_map_mut(
buffer.put(QUOTE);
buffer.put(COLON);
write_and_escape_string(buffer, type_name);
- // We wrote the first field
*first = false;
}
}
+ if original.is_null() {
+ // The field exists and is null, so keep it in the representation.
+ write_response_key(*first, Some(response_key), buffer);
+ buffer.put(NULL);
+ *first = false;
+ continue;
+ }
+
let projected = project_requires(
possible_types,
&requires_selection.selections.items,
@@ -177,8 +188,15 @@ fn project_requires_map_mut(
*first,
Some(response_key),
);
+
if projected {
*first = false;
+ } else if *first {
+ // We opened '{' but produced no field output.
+ // Roll back to keep valid JSON and avoid malformed '{...'.
+ if let Some(offset) = object_start_offset {
+ buffer.truncate(offset);
+ }
}
}
SelectionItem::InlineFragment(requires_selection) => {
@@ -213,3 +231,161 @@ fn project_requires_map_mut(
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::project_requires;
+ use crate::{introspection::schema::PossibleTypes, response::value::Value};
+ use graphql_tools::parser::query;
+ use hive_router_query_planner::ast::{
+ selection_item::SelectionItem, selection_set::SelectionSet,
+ };
+ use hive_router_query_planner::utils::parsing::parse_operation;
+ use sonic_rs::json;
+
+ fn requires_from_str(requires: &str) -> Vec {
+ let operation = parse_operation(&format!("query {{ {requires} }}"));
+
+ let selection_set = operation
+ .definitions
+ .into_iter()
+ .find_map(|def| {
+ let query::Definition::Operation(op) = def else {
+ return None;
+ };
+
+ match op {
+ query::OperationDefinition::SelectionSet(sel) => Some(sel),
+ query::OperationDefinition::Query(q) => Some(q.selection_set),
+ query::OperationDefinition::Mutation(m) => Some(m.selection_set),
+ query::OperationDefinition::Subscription(s) => Some(s.selection_set),
+ }
+ })
+ .expect("operation must contain a selection set");
+
+ let selection_set: SelectionSet = selection_set.into();
+ selection_set.items
+ }
+
+ fn project_requires_pretty(requires: &str, entity_json: sonic_rs::Value) -> Option {
+ let requires = requires_from_str(requires);
+ let entity = Value::from(entity_json.as_ref());
+
+ let mut buffer = Vec::new();
+ let projected = project_requires(
+ &PossibleTypes::default(),
+ &requires,
+ &entity,
+ &mut buffer,
+ true,
+ None,
+ );
+
+ if !projected {
+ return None;
+ }
+
+ let json: Value = sonic_rs::from_slice(&buffer).unwrap();
+ Some(sonic_rs::to_string_pretty(&json).unwrap())
+ }
+
+ #[test]
+ fn project_requires_variants() {
+ insta::assert_snapshot!(
+ &project_requires_pretty(
+ "contactOptions id",
+ json!({
+ "__typename": "Ad",
+ "contactOptions": null,
+ "id": "1"
+ }),
+ )
+ .expect("projection should produce output"),
+ @r#"
+ {
+ "__typename": "Ad",
+ "contactOptions": null,
+ "id": "1"
+ }
+ "#);
+
+ insta::assert_snapshot!(
+ &project_requires_pretty(
+ "id contactOptions",
+ json!({
+ "__typename": "Ad",
+ "contactOptions": null,
+ "id": "1"
+ }),
+ ).expect("projection should produce output"),
+ @r#"
+ {
+ "__typename": "Ad",
+ "contactOptions": null,
+ "id": "1"
+ }
+ "#);
+
+ insta::assert_snapshot!(
+ &project_requires_pretty(
+ "contactOptions id",
+ json!({
+ "__typename": "Ad",
+ "id": "1"
+ }),
+ )
+ .expect("projection should produce output"),
+ @r#"
+ {
+ "__typename": "Ad",
+ "id": "1"
+ }
+ "#);
+
+ insta::assert_snapshot!(
+ &project_requires_pretty(
+ "branch { contactOptions { email } } id",
+ json!({
+ "__typename": "Ad",
+ "branch": {
+ "contactOptions": {}
+ },
+ "id": "1"
+ }),
+ )
+ .expect("projection should produce output"),
+ @r#"
+ {
+ "__typename": "Ad",
+ "id": "1"
+ }
+ "#);
+
+ insta::assert_snapshot!(
+ &project_requires_pretty(
+ "branch { contactOptions { email user { id name } } } id",
+ json!({
+ "__typename": "Ad",
+ "branch": {
+ "__typename": "Branch",
+ "contactOptions": null
+ },
+ "id": "1"
+ }),
+ )
+ .expect("projection should produce output"),
+ @r#"
+ {
+ "__typename": "Ad",
+ "branch": {
+ "__typename": "Branch",
+ "contactOptions": null
+ },
+ "id": "1"
+ }
+ "#);
+
+ let pretty = project_requires_pretty("contactOptions", json!({}));
+ assert_eq!(pretty, None);
+ }
+}
From 225587c70fdb5749b5d8b30e9f372b18e7171aa9 Mon Sep 17 00:00:00 2001
From: "knope-bot[bot]" <152252888+knope-bot[bot]@users.noreply.github.com>
Date: Sun, 29 Mar 2026 11:12:38 +0300
Subject: [PATCH 09/76] chore(release): router crates and artifacts (#883)
Co-authored-by: knope-bot[bot] <152252888+knope-bot[bot]@users.noreply.github.com>
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/requires_projection_null.md | 8 --------
Cargo.lock | 4 ++--
bin/router/CHANGELOG.md | 10 ++++++++++
bin/router/Cargo.toml | 4 ++--
lib/executor/CHANGELOG.md | 8 ++++++++
lib/executor/Cargo.toml | 2 +-
6 files changed, 23 insertions(+), 13 deletions(-)
delete mode 100644 .changeset/requires_projection_null.md
diff --git a/.changeset/requires_projection_null.md b/.changeset/requires_projection_null.md
deleted file mode 100644
index 7c3e3ac4d..000000000
--- a/.changeset/requires_projection_null.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-hive-router-plan-executor: patch
-hive-router: patch
----
-
-# Fix null field handling in entity request projection
-
-Fixed a bug in entity request projection where present `null` fields could be mishandled, which in some nested projection paths could also lead to malformed JSON output. [#880](https://github.com/graphql-hive/router/issues/880).
diff --git a/Cargo.lock b/Cargo.lock
index a622b34b1..4b0008f9f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2298,7 +2298,7 @@ dependencies = [
[[package]]
name = "hive-router"
-version = "0.0.43"
+version = "0.0.44"
dependencies = [
"ahash",
"anyhow",
@@ -2416,7 +2416,7 @@ dependencies = [
[[package]]
name = "hive-router-plan-executor"
-version = "6.9.0"
+version = "6.9.1"
dependencies = [
"ahash",
"async-trait",
diff --git a/bin/router/CHANGELOG.md b/bin/router/CHANGELOG.md
index 9c70cc1ce..71f453459 100644
--- a/bin/router/CHANGELOG.md
+++ b/bin/router/CHANGELOG.md
@@ -116,6 +116,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Other
- *(deps)* update release-plz/action action to v0.5.113 ([#389](https://github.com/graphql-hive/router/pull/389))
+## 0.0.44 (2026-03-29)
+
+### Fixes
+
+- fix null field handling in entity request projection and prevent malformed JSON (#881)
+
+#### Fix null field handling in entity request projection
+
+Fixed a bug in entity request projection where present `null` fields could be mishandled, which in some nested projection paths could also lead to malformed JSON output. [#880](https://github.com/graphql-hive/router/issues/880).
+
## 0.0.43 (2026-03-26)
### Features
diff --git a/bin/router/Cargo.toml b/bin/router/Cargo.toml
index 16f480246..792a8221d 100644
--- a/bin/router/Cargo.toml
+++ b/bin/router/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router"
-version = "0.0.43"
+version = "0.0.44"
edition = "2021"
description = "GraphQL router/gateway for Federation"
license = "MIT"
@@ -22,7 +22,7 @@ testing = []
[dependencies]
hive-router-query-planner = { path = "../../lib/query-planner", version = "2.5.0" }
-hive-router-plan-executor = { path = "../../lib/executor", version = "6.9.0" }
+hive-router-plan-executor = { path = "../../lib/executor", version = "6.9.1" }
hive-router-config = { path = "../../lib/router-config", version = "0.0.26" }
hive-router-internal = { path = "../../lib/internal", version = "0.0.15" }
hive-console-sdk = { path = "../../lib/hive-console-sdk", version = "0.3.8" }
diff --git a/lib/executor/CHANGELOG.md b/lib/executor/CHANGELOG.md
index dec2c0964..18c053c54 100644
--- a/lib/executor/CHANGELOG.md
+++ b/lib/executor/CHANGELOG.md
@@ -94,6 +94,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Other
- *(deps)* update release-plz/action action to v0.5.113 ([#389](https://github.com/graphql-hive/router/pull/389))
+## 6.9.1 (2026-03-29)
+
+### Fixes
+
+#### Fix null field handling in entity request projection
+
+Fixed a bug in entity request projection where present `null` fields could be mishandled, which in some nested projection paths could also lead to malformed JSON output. [#880](https://github.com/graphql-hive/router/issues/880).
+
## 6.9.0 (2026-03-26)
### Features
diff --git a/lib/executor/Cargo.toml b/lib/executor/Cargo.toml
index 62a23eca7..9ff5f9b45 100644
--- a/lib/executor/Cargo.toml
+++ b/lib/executor/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router-plan-executor"
-version = "6.9.0"
+version = "6.9.1"
edition = "2021"
description = "GraphQL query planner executor for Federation specification"
license = "MIT"
From 9637749a90560620f9621adc0e98b26e653a7093 Mon Sep 17 00:00:00 2001
From: Heriberto Gonzalez
Date: Tue, 31 Mar 2026 01:18:26 -0700
Subject: [PATCH 10/76] fix(qp): preserve client aliases in mismatch output
rewrites (#870)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes ROUTER-279
Closes: #876
# Generating conflicting aliases when conflicts are across fragments
Consider a schema like:
```graphql
interface CommonInterface {
id: ID!
}
type TypeA implements CommonInterface {
id: ID!
field: EnumA!
}
type TypeB implements CommonInterface {
id: ID!
field: EnumB!
}
type TypeC implements CommonInterface {
id: ID!
field: EnumC!
}
enum EnumA { VALUE_A1 VALUE_A2 VALUE_A3 }
enum EnumB { VALUE_B1 VALUE_B2 VALUE_B3 }
enum EnumC { VALUE_C1 VALUE_C2 VALUE_C3 }
type Query {
getTypes: CommonInterface
}
```
Consider a query like:
```graphql
query GetAllTypesWithConflictingField {
getTypes {
id # ✅ on CommonInterface — no conflict
... on TypeA { field } # field: EnumA
... on TypeB { userAlias1: field } # field: EnumB — user-provided alias
... on TypeC { userAlias2: field } # field: EnumC — user-provided alias
}
}
```
The Rust Planner Generates:
```graphql
QueryPlan {
Fetch(service: "service") {
{
getTypes {
id
__typename
... on TypeA {
field
}
... on TypeB {
_internal_qp_alias_0: field
}
... on TypeC {
_internal_qp_alias_0: field
}
... on TypeD {
_internal_qp_alias_0: field
}
}
}
},
},
```
The problem here is that this is invalid GraphQL syntax and causes the
subgraph query to be malformed. Resulting in a response from the
subgraph like below:
```graphql
GraphQLError: Fields "_internal_qp_alias_0" conflict because they return conflicting types "EnumB" and "EnumC". Use different aliases on the fields to fetch both if this was intentional.
```
---
### Explanation: Why `SafeSelectionSetMerger` Causes Identical Alias
Names:
The problem is about scope and state persistence of the merger's
internal counter.
``` rust
for (root_def_name, mismatch_path) in mismatches_paths {
let mut merger = SafeSelectionSetMerger::default(); // Line 61: RESET EACH ITERATION
// ...
let next_alias = merger.safe_next_alias_name(&selection_set.items);
}
```
A new `SafeSelectionSetMerger` is created for each mismatch in the list,
which means its aliases_counter (initialized to 0) resets every
iteration.
When processing multiple mismatches across fragments:
1. Mismatch # 1 → merger starts at counter 0 → generates
_internal_qp_alias_0
2. Mismatch # 2 → NEW merger starts at counter 0 → generates
_internal_qp_alias_0 (COLLISION!)
3. Mismatch # 3 → NEW merger starts at counter 0 → generates
_internal_qp_alias_0 (COLLISION!)
---
---------
Co-authored-by: Kamil Kisiela
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/alias-fix.md | 9 +
...ismatch-mix-enum-scalar.supergraph.graphql | 159 +++++++++++
lib/query-planner/src/ast/mismatch_finder.rs | 9 +-
.../planner/fetch/optimize/type_mismatches.rs | 7 +-
lib/query-planner/src/tests/alias.rs | 266 ++++++++++++++++++
5 files changed, 444 insertions(+), 6 deletions(-)
create mode 100644 .changeset/alias-fix.md
create mode 100644 lib/query-planner/fixture/tests/mismatch-mix-enum-scalar.supergraph.graphql
diff --git a/.changeset/alias-fix.md b/.changeset/alias-fix.md
new file mode 100644
index 000000000..c9220b782
--- /dev/null
+++ b/.changeset/alias-fix.md
@@ -0,0 +1,9 @@
+---
+hive-router-query-planner: patch
+hive-router-plan-executor: patch
+hive-router: patch
+---
+
+# Preserve client aliases in mismatch rewrites
+
+Fixed query planner mismatch handling so conflicting fields are tracked by response key (alias-aware), and internal alias rewrites restore the original client-facing key (alias-or-name) instead of always the schema field name.
diff --git a/lib/query-planner/fixture/tests/mismatch-mix-enum-scalar.supergraph.graphql b/lib/query-planner/fixture/tests/mismatch-mix-enum-scalar.supergraph.graphql
new file mode 100644
index 000000000..700ea7035
--- /dev/null
+++ b/lib/query-planner/fixture/tests/mismatch-mix-enum-scalar.supergraph.graphql
@@ -0,0 +1,159 @@
+schema
+@link(url: "https://specs.apollo.dev/link/v1.0")
+@link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
+{
+ query: Query
+}
+
+directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
+
+directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
+
+directive @join__graph(name: String!, url: String!) on ENUM_VALUE
+
+directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
+
+directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
+
+directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
+
+directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
+
+"""Root Query Type"""
+type Query @join__type(graph: SERVICE) {
+ """Get a root entity"""
+ getTypes: CommonInterface @join__field(graph: SERVICE)
+}
+
+"""
+Type A - field is an EnumA
+"""
+type TypeA implements CommonInterface
+@join__type(graph: SERVICE, key: "id")
+@join__implements(graph: SERVICE, interface: "CommonInterface")
+{
+ id: ID!
+ identifier: String!
+ code: String!
+ """Field with EnumA type"""
+ field: EnumA!
+ isScalar: Int
+ metadata: Data!
+}
+
+
+
+"""
+Type B - field is an EnumB
+"""
+type TypeB implements CommonInterface
+@join__type(graph: SERVICE, key: "id")
+@join__implements(graph: SERVICE, interface: "CommonInterface")
+{
+ id: ID!
+ identifier: String!
+ code: String!
+ """Field with EnumB type (different from EnumA)"""
+ field: EnumB!
+ isScalar: Boolean
+ metadata: Data!
+}
+
+"""
+Type C - field is an EnumC
+"""
+type TypeC implements CommonInterface
+@join__type(graph: SERVICE, key: "id")
+@join__implements(graph: SERVICE, interface: "CommonInterface")
+{
+ id: ID!
+ identifier: String!
+ code: String!
+ """Field with EnumC type (different from EnumA and EnumB)"""
+ field: EnumC!
+ isScalar: Float
+ metadata: Data!
+}
+
+"""
+Type D - field is an EnumD
+"""
+type TypeD implements CommonInterface
+@join__type(graph: SERVICE, key: "id")
+@join__implements(graph: SERVICE, interface: "CommonInterface")
+{
+ id: ID!
+ identifier: String!
+ code: String!
+ """Field with EnumD type (different from others)"""
+ field: EnumD!
+ isScalar: ID
+ metadata: Data!
+}
+
+"""
+Common interface for all types
+Note: The field field is intentionally NOT defined here to force the query planner
+to resolve conflicts when accessing it on the union type
+"""
+interface CommonInterface @join__type(graph: SERVICE, key: "id") {
+ id: ID!
+ identifier: String! @join__field(graph: SERVICE)
+ code: String! @join__field(graph: SERVICE)
+ metadata: Data! @join__field(graph: SERVICE)
+}
+
+"""Enum A"""
+enum EnumA {
+ VALUE_A1
+ VALUE_A2
+ VALUE_A3
+ VALUE_A4
+}
+
+"""Enum B - intentionally different from EnumA"""
+enum EnumB {
+ VALUE_B1
+ VALUE_B2
+ VALUE_B3
+ VALUE_B4
+}
+
+"""Enum C - intentionally different from EnumA and EnumB"""
+enum EnumC {
+ VALUE_C1
+ VALUE_C2
+ VALUE_C3
+ VALUE_C4
+}
+
+"""Enum D - intentionally different from all"""
+enum EnumD {
+ VALUE_D1
+ VALUE_D2
+ VALUE_D3
+ VALUE_D4
+}
+
+"""Data representation"""
+type Data @join__type(graph: SERVICE){
+ value: Float! @join__field(graph: SERVICE)
+ unit: String! @join__field(graph: SERVICE)
+}
+
+"""Link directive definition"""
+scalar link__Import
+
+enum link__Purpose {
+ SECURITY
+ EXECUTION
+}
+
+"""Join directive scalar for field set"""
+scalar join__FieldSet
+
+"""Graph enumeration"""
+enum join__Graph {
+ SERVICE @join__graph(name: "service", url: "")
+}
+
diff --git a/lib/query-planner/src/ast/mismatch_finder.rs b/lib/query-planner/src/ast/mismatch_finder.rs
index 74a6a474c..045fdad02 100644
--- a/lib/query-planner/src/ast/mismatch_finder.rs
+++ b/lib/query-planner/src/ast/mismatch_finder.rs
@@ -96,7 +96,7 @@ fn handle_selection_set<'field, 'schema>(
}
let next_path = path.push(Segment::Field(
- field.name.clone(),
+ field.selection_identifier().to_string(),
field.arguments_hash(),
field.into(),
));
@@ -165,6 +165,7 @@ fn handle_field<'field, 'schema>(
) -> Option<&'schema str> {
let parent_def_fields = parent_def.fields().unwrap();
let field_name = field.name.as_str();
+ let field_identifier = field.selection_identifier();
let field_type = parent_def_fields
.iter()
.find_map(|f| {
@@ -181,7 +182,7 @@ fn handle_field<'field, 'schema>(
})
.unwrap();
- if let Some(maybe_conflicting_type) = encountered_field_to_type.get(field_name) {
+ if let Some(maybe_conflicting_type) = encountered_field_to_type.get(field_identifier) {
if !maybe_conflicting_type.can_be_merged_with(field_type) {
let left_is_composite = state
.definitions
@@ -195,7 +196,7 @@ fn handle_field<'field, 'schema>(
if !left_is_composite || !right_is_composite {
trace!(
"found a conflicting type for a selection field '{}', conflict is: '{}' <-> '{}', path: {}",
- field_name,
+ field_identifier,
maybe_conflicting_type,
field_type,
field_path,
@@ -205,7 +206,7 @@ fn handle_field<'field, 'schema>(
}
}
} else {
- encountered_field_to_type.insert(field_name, field_type);
+ encountered_field_to_type.insert(field_identifier, field_type);
}
if field.is_leaf() {
diff --git a/lib/query-planner/src/planner/fetch/optimize/type_mismatches.rs b/lib/query-planner/src/planner/fetch/optimize/type_mismatches.rs
index 1e111e7ef..5a98c19e0 100644
--- a/lib/query-planner/src/planner/fetch/optimize/type_mismatches.rs
+++ b/lib/query-planner/src/planner/fetch/optimize/type_mismatches.rs
@@ -73,9 +73,12 @@ impl FetchGraph {
let item = selection_set
.items
.iter_mut()
- .find(|v| matches!(v, SelectionItem::Field(field) if field.name == *field_lookup && field.arguments_hash() == *args_hash_lookup && field_condition_equal(condition, field)));
+ .find(|v| matches!(v, SelectionItem::Field(field) if field.selection_identifier() == *field_lookup && field.arguments_hash() == *args_hash_lookup && field_condition_equal(condition, field)));
if let Some(SelectionItem::Field(field_to_alias)) = item {
+ let original_response_key =
+ field_to_alias.selection_identifier().to_string();
+
trace!(
"applying alias '{}' to existing field '{}' at path '{}'",
next_alias,
@@ -90,7 +93,7 @@ impl FetchGraph {
pending_output_rewrites.push((
node_index,
FetchRewrite::KeyRenamer(KeyRenamer {
- rename_key_to: field_to_alias.name.to_string(),
+ rename_key_to: original_response_key,
path: output_rewrite_path,
}),
));
diff --git a/lib/query-planner/src/tests/alias.rs b/lib/query-planner/src/tests/alias.rs
index c535d98ac..3723526ad 100644
--- a/lib/query-planner/src/tests/alias.rs
+++ b/lib/query-planner/src/tests/alias.rs
@@ -1320,3 +1320,269 @@ fn deeply_nested_no_conflicts() -> Result<(), Box> {
Ok(())
}
+
+#[test]
+fn multi_enum_mismatch_across_fragments() -> Result<(), Box> {
+ init_logger();
+ let document = parse_operation(
+ r#"
+ query GetAllTypesWithConflictingField {
+ getTypes {
+ id
+ identifier
+ code
+ metadata {
+ value
+ unit
+ }
+ ... on TypeA {
+ field # maps to EnumA
+ }
+ ... on TypeB {
+ typeBField: field # maps to EnumB
+ }
+ ... on TypeC {
+ typeCField: field # maps to EnumC
+ }
+ ... on TypeD {
+ typeDField: field # maps to EnumD
+ }
+ }
+ }
+ "#,
+ );
+ let query_plan = build_query_plan(
+ "fixture/tests/mismatch-mix-enum-scalar.supergraph.graphql",
+ document,
+ )?;
+
+ insta::assert_snapshot!(format!("{}", query_plan), @r#"
+ QueryPlan {
+ Fetch(service: "service") {
+ {
+ getTypes {
+ id
+ identifier
+ code
+ metadata {
+ value
+ unit
+ }
+ __typename
+ ... on TypeA {
+ field
+ }
+ ... on TypeB {
+ typeBField: field
+ }
+ ... on TypeC {
+ typeCField: field
+ }
+ ... on TypeD {
+ typeDField: field
+ }
+ }
+ }
+ },
+ },
+ "#);
+
+ insta::assert_snapshot!(format!("{}", sonic_rs::to_string_pretty(&query_plan).unwrap_or_default()), @r#"
+ {
+ "kind": "QueryPlan",
+ "node": {
+ "kind": "Fetch",
+ "serviceName": "service",
+ "operationKind": "query",
+ "operation": "{getTypes{id identifier code metadata{value unit} __typename ...on TypeA{field} ...on TypeB{typeBField: field} ...on TypeC{typeCField: field} ...on TypeD{typeDField: field}}}"
+ }
+ }
+ "#);
+
+ Ok(())
+}
+
+#[test]
+fn multi_scalar_mismatch_across_fragments() -> Result<(), Box> {
+ init_logger();
+ let document = parse_operation(
+ r#"
+ query GetAllTypesWithConflictingField {
+ getTypes {
+ id
+ identifier
+ code
+ metadata {
+ value
+ unit
+ }
+ ... on TypeA {
+ isScalar # is a Int
+ }
+ ... on TypeB {
+ scalarB: isScalar # is a Boolean
+ }
+ ... on TypeC {
+ scalarC: isScalar # is a Float
+ }
+ ... on TypeD {
+ scalarD: isScalar # is a Int
+ }
+ }
+ }
+ "#,
+ );
+ let query_plan = build_query_plan(
+ "fixture/tests/mismatch-mix-enum-scalar.supergraph.graphql",
+ document,
+ )?;
+
+ insta::assert_snapshot!(format!("{}", query_plan), @r#"
+ QueryPlan {
+ Fetch(service: "service") {
+ {
+ getTypes {
+ id
+ identifier
+ code
+ metadata {
+ value
+ unit
+ }
+ __typename
+ ... on TypeA {
+ isScalar
+ }
+ ... on TypeB {
+ scalarB: isScalar
+ }
+ ... on TypeC {
+ scalarC: isScalar
+ }
+ ... on TypeD {
+ scalarD: isScalar
+ }
+ }
+ }
+ },
+ },
+ "#);
+
+ insta::assert_snapshot!(format!("{}", sonic_rs::to_string_pretty(&query_plan).unwrap_or_default()), @r#"
+ {
+ "kind": "QueryPlan",
+ "node": {
+ "kind": "Fetch",
+ "serviceName": "service",
+ "operationKind": "query",
+ "operation": "{getTypes{id identifier code metadata{value unit} __typename ...on TypeA{isScalar} ...on TypeB{scalarB: isScalar} ...on TypeC{scalarC: isScalar} ...on TypeD{scalarD: isScalar}}}"
+ }
+ }
+ "#);
+
+ Ok(())
+}
+
+#[test]
+fn conflict_list_type_in_interface_preserves_client_alias() -> Result<(), Box> {
+ init_logger();
+ let document = parse_operation(
+ r#"
+ query {
+ i {
+ ... on TypeA {
+ clientStrFieldA: strField # [String]
+ }
+ ... on TypeB {
+ clientStrFieldB: strField # String
+ }
+ }
+ }
+"#,
+ );
+ let query_plan = build_query_plan("fixture/tests/mismatch-mix.supergraph.graphql", document)?;
+
+ insta::assert_snapshot!(format!("{}", query_plan), @r###"
+ QueryPlan {
+ Fetch(service: "a") {
+ {
+ i {
+ __typename
+ ... on TypeA {
+ clientStrFieldA: strField
+ }
+ ... on TypeB {
+ clientStrFieldB: strField
+ }
+ }
+ }
+ },
+ },
+ "###);
+
+ insta::assert_snapshot!(format!("{}", sonic_rs::to_string_pretty(&query_plan).unwrap_or_default()), @r#"
+ {
+ "kind": "QueryPlan",
+ "node": {
+ "kind": "Fetch",
+ "serviceName": "a",
+ "operationKind": "query",
+ "operation": "{i{__typename ...on TypeA{clientStrFieldA: strField} ...on TypeB{clientStrFieldB: strField}}}"
+ }
+ }
+ "#);
+
+ Ok(())
+}
+
+#[test]
+fn conflict_list_type_in_interface_with_distinct_response_keys() -> Result<(), Box> {
+ init_logger();
+ let document = parse_operation(
+ r#"
+ query {
+ i {
+ ... on TypeA {
+ typeAField: strField # [String]
+ }
+ ... on TypeB {
+ typeBField: strField # String
+ }
+ }
+ }
+"#,
+ );
+ let query_plan = build_query_plan("fixture/tests/mismatch-mix.supergraph.graphql", document)?;
+
+ insta::assert_snapshot!(format!("{}", query_plan), @r###"
+ QueryPlan {
+ Fetch(service: "a") {
+ {
+ i {
+ __typename
+ ... on TypeA {
+ typeAField: strField
+ }
+ ... on TypeB {
+ typeBField: strField
+ }
+ }
+ }
+ },
+ },
+ "###);
+
+ insta::assert_snapshot!(format!("{}", sonic_rs::to_string_pretty(&query_plan).unwrap_or_default()), @r#"
+ {
+ "kind": "QueryPlan",
+ "node": {
+ "kind": "Fetch",
+ "serviceName": "a",
+ "operationKind": "query",
+ "operation": "{i{__typename ...on TypeA{typeAField: strField} ...on TypeB{typeBField: strField}}}"
+ }
+ }
+ "#);
+
+ Ok(())
+}
From bbc8f75cf385ca4b6b156eda9d393d4b2eb5a1d7 Mon Sep 17 00:00:00 2001
From: "knope-bot[bot]" <152252888+knope-bot[bot]@users.noreply.github.com>
Date: Tue, 31 Mar 2026 20:44:58 +0300
Subject: [PATCH 11/76] chore(release): router crates and artifacts (#885)
Co-authored-by: knope-bot[bot] <152252888+knope-bot[bot]@users.noreply.github.com>
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/alias-fix.md | 9 ---------
Cargo.lock | 6 +++---
bin/router/CHANGELOG.md | 10 ++++++++++
bin/router/Cargo.toml | 6 +++---
lib/executor/CHANGELOG.md | 8 ++++++++
lib/executor/Cargo.toml | 4 ++--
lib/query-planner/CHANGELOG.md | 10 ++++++++++
lib/query-planner/Cargo.toml | 2 +-
8 files changed, 37 insertions(+), 18 deletions(-)
delete mode 100644 .changeset/alias-fix.md
diff --git a/.changeset/alias-fix.md b/.changeset/alias-fix.md
deleted file mode 100644
index c9220b782..000000000
--- a/.changeset/alias-fix.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-hive-router-query-planner: patch
-hive-router-plan-executor: patch
-hive-router: patch
----
-
-# Preserve client aliases in mismatch rewrites
-
-Fixed query planner mismatch handling so conflicting fields are tracked by response key (alias-aware), and internal alias rewrites restore the original client-facing key (alias-or-name) instead of always the schema field name.
diff --git a/Cargo.lock b/Cargo.lock
index 4b0008f9f..9e3f0776e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2298,7 +2298,7 @@ dependencies = [
[[package]]
name = "hive-router"
-version = "0.0.44"
+version = "0.0.45"
dependencies = [
"ahash",
"anyhow",
@@ -2416,7 +2416,7 @@ dependencies = [
[[package]]
name = "hive-router-plan-executor"
-version = "6.9.1"
+version = "6.9.2"
dependencies = [
"ahash",
"async-trait",
@@ -2456,7 +2456,7 @@ dependencies = [
[[package]]
name = "hive-router-query-planner"
-version = "2.5.0"
+version = "2.5.1"
dependencies = [
"bitflags 2.11.0",
"criterion",
diff --git a/bin/router/CHANGELOG.md b/bin/router/CHANGELOG.md
index 71f453459..966a8ae04 100644
--- a/bin/router/CHANGELOG.md
+++ b/bin/router/CHANGELOG.md
@@ -116,6 +116,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Other
- *(deps)* update release-plz/action action to v0.5.113 ([#389](https://github.com/graphql-hive/router/pull/389))
+## 0.0.45 (2026-03-31)
+
+### Fixes
+
+- preserve client aliases in mismatch output rewrites (#870)
+
+#### Preserve client aliases in mismatch rewrites
+
+Fixed query planner mismatch handling so conflicting fields are tracked by response key (alias-aware), and internal alias rewrites restore the original client-facing key (alias-or-name) instead of always the schema field name.
+
## 0.0.44 (2026-03-29)
### Fixes
diff --git a/bin/router/Cargo.toml b/bin/router/Cargo.toml
index 792a8221d..bfeb5521e 100644
--- a/bin/router/Cargo.toml
+++ b/bin/router/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router"
-version = "0.0.44"
+version = "0.0.45"
edition = "2021"
description = "GraphQL router/gateway for Federation"
license = "MIT"
@@ -21,8 +21,8 @@ noop_otlp_exporter = ["hive-router-internal/noop_otlp_exporter"]
testing = []
[dependencies]
-hive-router-query-planner = { path = "../../lib/query-planner", version = "2.5.0" }
-hive-router-plan-executor = { path = "../../lib/executor", version = "6.9.1" }
+hive-router-query-planner = { path = "../../lib/query-planner", version = "2.5.1" }
+hive-router-plan-executor = { path = "../../lib/executor", version = "6.9.2" }
hive-router-config = { path = "../../lib/router-config", version = "0.0.26" }
hive-router-internal = { path = "../../lib/internal", version = "0.0.15" }
hive-console-sdk = { path = "../../lib/hive-console-sdk", version = "0.3.8" }
diff --git a/lib/executor/CHANGELOG.md b/lib/executor/CHANGELOG.md
index 18c053c54..124ca02eb 100644
--- a/lib/executor/CHANGELOG.md
+++ b/lib/executor/CHANGELOG.md
@@ -94,6 +94,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Other
- *(deps)* update release-plz/action action to v0.5.113 ([#389](https://github.com/graphql-hive/router/pull/389))
+## 6.9.2 (2026-03-31)
+
+### Fixes
+
+#### Preserve client aliases in mismatch rewrites
+
+Fixed query planner mismatch handling so conflicting fields are tracked by response key (alias-aware), and internal alias rewrites restore the original client-facing key (alias-or-name) instead of always the schema field name.
+
## 6.9.1 (2026-03-29)
### Fixes
diff --git a/lib/executor/Cargo.toml b/lib/executor/Cargo.toml
index 9ff5f9b45..8485eaaaa 100644
--- a/lib/executor/Cargo.toml
+++ b/lib/executor/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router-plan-executor"
-version = "6.9.1"
+version = "6.9.2"
edition = "2021"
description = "GraphQL query planner executor for Federation specification"
license = "MIT"
@@ -15,7 +15,7 @@ authors = ["The Guild"]
doctest = false
[dependencies]
-hive-router-query-planner = { path = "../query-planner", version = "2.5.0" }
+hive-router-query-planner = { path = "../query-planner", version = "2.5.1" }
hive-router-config = { path = "../router-config", version = "0.0.26" }
hive-router-internal = { path = "../internal", version = "0.0.15" }
graphql-tools = { path = "../graphql-tools", version = "0.5.3" }
diff --git a/lib/query-planner/CHANGELOG.md b/lib/query-planner/CHANGELOG.md
index 06f6a65e3..630570ca7 100644
--- a/lib/query-planner/CHANGELOG.md
+++ b/lib/query-planner/CHANGELOG.md
@@ -30,6 +30,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Other
- *(deps)* update release-plz/action action to v0.5.113 ([#389](https://github.com/graphql-hive/router/pull/389))
+## 2.5.1 (2026-03-31)
+
+### Fixes
+
+- preserve client aliases in mismatch output rewrites (#870)
+
+#### Preserve client aliases in mismatch rewrites
+
+Fixed query planner mismatch handling so conflicting fields are tracked by response key (alias-aware), and internal alias rewrites restore the original client-facing key (alias-or-name) instead of always the schema field name.
+
## 2.5.0 (2026-03-16)
### Features
diff --git a/lib/query-planner/Cargo.toml b/lib/query-planner/Cargo.toml
index bf8e00cb9..0675deadd 100644
--- a/lib/query-planner/Cargo.toml
+++ b/lib/query-planner/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router-query-planner"
-version = "2.5.0"
+version = "2.5.1"
edition = "2021"
description = "GraphQL query planner for Federation specification"
license = "MIT"
From a109ad8b74aeed5a4f738297f1bb7b205493ebf9 Mon Sep 17 00:00:00 2001
From: Arda TANRIKULU
Date: Wed, 1 Apr 2026 07:00:49 -0400
Subject: [PATCH 12/76] chore(node-addon): changeset for the fixes in the query
planner (#886)
This patch includes the fixes in the query planner including the fixes
for mismatch handling so conflicting fields are tracked by response key
(alias-aware), and internal alias rewrites restore the original
client-facing key (alias-or-name) instead of always the schema field
name.
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/update-node-addon.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/update-node-addon.md
diff --git a/.changeset/update-node-addon.md b/.changeset/update-node-addon.md
new file mode 100644
index 000000000..a5c5ee8f9
--- /dev/null
+++ b/.changeset/update-node-addon.md
@@ -0,0 +1,5 @@
+---
+node-addon: patch
+---
+
+This patch includes the fixes in the query planner including the fixes for mismatch handling so conflicting fields are tracked by response key (alias-aware), and internal alias rewrites restore the original client-facing key (alias-or-name) instead of always the schema field name.
\ No newline at end of file
From bfd41c220119620c3eae84d9d2faaa0f8493a96f Mon Sep 17 00:00:00 2001
From: "knope-bot[bot]" <152252888+knope-bot[bot]@users.noreply.github.com>
Date: Wed, 1 Apr 2026 14:01:49 +0300
Subject: [PATCH 13/76] chore(release): router crates and artifacts (#887)
> [!IMPORTANT]
> Merging this pull request will create these releases
# node-addon 0.0.17 (2026-04-01)
## Fixes
- This patch includes the fixes in the query planner including the fixes
for mismatch handling so conflicting fields are tracked by response key
(alias-aware), and internal alias rewrites restore the original
client-facing key (alias-or-name) instead of always the schema field
name.
Co-authored-by: knope-bot[bot] <152252888+knope-bot[bot]@users.noreply.github.com>
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/update-node-addon.md | 5 -----
Cargo.lock | 2 +-
lib/node-addon/CHANGELOG.md | 6 ++++++
lib/node-addon/Cargo.toml | 2 +-
lib/node-addon/package.json | 2 +-
5 files changed, 9 insertions(+), 8 deletions(-)
delete mode 100644 .changeset/update-node-addon.md
diff --git a/.changeset/update-node-addon.md b/.changeset/update-node-addon.md
deleted file mode 100644
index a5c5ee8f9..000000000
--- a/.changeset/update-node-addon.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-node-addon: patch
----
-
-This patch includes the fixes in the query planner including the fixes for mismatch handling so conflicting fields are tracked by response key (alias-aware), and internal alias rewrites restore the original client-facing key (alias-or-name) instead of always the schema field name.
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 9e3f0776e..4761c81a0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3475,7 +3475,7 @@ dependencies = [
[[package]]
name = "node-addon"
-version = "0.0.16"
+version = "0.0.17"
dependencies = [
"graphql-tools",
"hive-router-query-planner",
diff --git a/lib/node-addon/CHANGELOG.md b/lib/node-addon/CHANGELOG.md
index 79a92acf8..1f55ba689 100644
--- a/lib/node-addon/CHANGELOG.md
+++ b/lib/node-addon/CHANGELOG.md
@@ -1,4 +1,10 @@
# @graphql-hive/router-query-planner changelog
+## 0.0.17 (2026-04-01)
+
+### Fixes
+
+- This patch includes the fixes in the query planner including the fixes for mismatch handling so conflicting fields are tracked by response key (alias-aware), and internal alias rewrites restore the original client-facing key (alias-or-name) instead of always the schema field name.
+
## 0.0.16 (2026-03-16)
### Fixes
diff --git a/lib/node-addon/Cargo.toml b/lib/node-addon/Cargo.toml
index 0db1cf877..9cae9bb25 100644
--- a/lib/node-addon/Cargo.toml
+++ b/lib/node-addon/Cargo.toml
@@ -1,6 +1,6 @@
[package]
edition = "2021"
-version = "0.0.16"
+version = "0.0.17"
name = "node-addon"
publish = false
diff --git a/lib/node-addon/package.json b/lib/node-addon/package.json
index dec47c871..2af38147c 100644
--- a/lib/node-addon/package.json
+++ b/lib/node-addon/package.json
@@ -1,6 +1,6 @@
{
"name": "@graphql-hive/router-query-planner",
- "version": "0.0.16",
+ "version": "0.0.17",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
From 5630f4371642ab3837ebe32b109597e26b4bd1ec Mon Sep 17 00:00:00 2001
From: Michael Skorokhodov
Date: Wed, 1 Apr 2026 20:20:13 +0200
Subject: [PATCH 14/76] Replace graphiql with laboratory (#791)
Co-authored-by: Dotan Simha
Co-authored-by: Denis Badurina
Co-authored-by: Denis Badurina
Co-authored-by: Arda TANRIKULU
Co-authored-by: theguild-bot
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.../replace_graphiql_with_hive_laboratory.md | 27 +
bin/router/build.rs | 203 ++
bin/router/src/http_utils/landing_page.rs | 2 +-
bin/router/src/lib.rs | 2 +-
bin/router/src/pipeline/header.rs | 16 +-
bin/router/src/pipeline/mod.rs | 8 +-
bin/router/static/graphiql.html | 75 -
bin/router/static/landing-page.html | 4 +-
docs/README.md | 46 +-
lib/router-config/src/env_overrides.rs | 12 +-
lib/router-config/src/graphiql.rs | 24 -
lib/router-config/src/laboratory.rs | 24 +
lib/router-config/src/lib.rs | 8 +-
package-lock.json | 2465 ++++++++++++++---
package.json | 3 +-
15 files changed, 2380 insertions(+), 539 deletions(-)
create mode 100644 .changeset/replace_graphiql_with_hive_laboratory.md
create mode 100644 bin/router/build.rs
delete mode 100644 bin/router/static/graphiql.html
delete mode 100644 lib/router-config/src/graphiql.rs
create mode 100644 lib/router-config/src/laboratory.rs
diff --git a/.changeset/replace_graphiql_with_hive_laboratory.md b/.changeset/replace_graphiql_with_hive_laboratory.md
new file mode 100644
index 000000000..85ddaa171
--- /dev/null
+++ b/.changeset/replace_graphiql_with_hive_laboratory.md
@@ -0,0 +1,27 @@
+---
+hive-router: patch
+hive-router-config: patch
+---
+
+# Replace GraphiQL with Hive Laboratory
+
+The Laboratory is Hive's powerful GraphQL playground that provides a comprehensive environment for exploring, testing, and experimenting with your GraphQL APIs. Whether you're developing new queries, debugging issues, or sharing operations with your team, the Laboratory offers all the tools you need.
+
+Read more about Hive Laboratory in [the introduction blog post](https://the-guild.dev/graphql/hive/product-updates/2026-01-28-new-laboratory) or [the documentation](https://the-guild.dev/graphql/hive/docs/new-laboratory).
+
+### Breaking Changes:
+
+The top-level config option has been renamed.
+
+```diff
+- graphiql:
++ laboratory:
+ enabled: true
+```
+
+So was the environment variable override.
+
+```diff
+- GRAPHIQL_ENABLED=true
++ LABORATORY_ENABLED=true
+```
diff --git a/bin/router/build.rs b/bin/router/build.rs
new file mode 100644
index 000000000..95e3fa721
--- /dev/null
+++ b/bin/router/build.rs
@@ -0,0 +1,203 @@
+use std::{
+ env, fs,
+ path::{Path, PathBuf},
+ process::Command,
+};
+
+fn main() {
+ let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing manifest dir"));
+ let out_dir = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR"));
+ let output_file = out_dir.join("laboratory.html");
+ let product_logo = manifest_dir.join("static/product_logo.svg");
+ let node_modules_dist = manifest_dir.join("../../node_modules/@graphql-hive/laboratory/dist");
+
+ println!("cargo:rerun-if-changed={}", product_logo.display());
+ println!(
+ "cargo:rerun-if-changed={}",
+ manifest_dir.join("../../package.json").display()
+ );
+ println!(
+ "cargo:rerun-if-changed={}",
+ manifest_dir.join("../../package-lock.json").display()
+ );
+
+ if !node_modules_dist.exists() {
+ let status = Command::new("npm")
+ .args(["install", "--include=dev"]) // NODE_ENV=production will skip dev deps - make sure they're in
+ .current_dir(manifest_dir.join("../../"))
+ .status()
+ .expect("Failed to execute npm install");
+
+ if !status.success() {
+ panic!("npm install failed");
+ }
+ }
+
+ println!(
+ "cargo:rerun-if-changed={}",
+ node_modules_dist.join("hive-laboratory.umd.js").display()
+ );
+ println!(
+ "cargo:rerun-if-changed={}",
+ node_modules_dist
+ .join("monacoeditorwork/editor.worker.bundle.js")
+ .display()
+ );
+ println!(
+ "cargo:rerun-if-changed={}",
+ node_modules_dist
+ .join("monacoeditorwork/graphql.worker.bundle.js")
+ .display()
+ );
+ println!(
+ "cargo:rerun-if-changed={}",
+ node_modules_dist
+ .join("monacoeditorwork/json.worker.bundle.js")
+ .display()
+ );
+ println!(
+ "cargo:rerun-if-changed={}",
+ node_modules_dist
+ .join("monacoeditorwork/ts.worker.bundle.js")
+ .display()
+ );
+
+ let html = build_inline_laboratory_html(&node_modules_dist, &product_logo);
+
+ fs::write(output_file, html).expect("failed to write generated laboratory.html");
+}
+
+fn build_inline_laboratory_html(dist_dir: &Path, product_logo: &Path) -> String {
+ let js_contents = fs::read_to_string(dist_dir.join("hive-laboratory.umd.js"))
+ .expect("failed to read hive-laboratory.umd.js");
+ let editor_worker =
+ fs::read_to_string(dist_dir.join("monacoeditorwork/editor.worker.bundle.js"))
+ .expect("failed to read editor worker");
+ let graphql_worker =
+ fs::read_to_string(dist_dir.join("monacoeditorwork/graphql.worker.bundle.js"))
+ .expect("failed to read graphql worker");
+ let json_worker = fs::read_to_string(dist_dir.join("monacoeditorwork/json.worker.bundle.js"))
+ .expect("failed to read json worker");
+ let typescript_worker =
+ fs::read_to_string(dist_dir.join("monacoeditorwork/ts.worker.bundle.js"))
+ .expect("failed to read typescript worker");
+ let product_logo_data_url = format!(
+ "data:image/svg+xml;base64,{}",
+ base64_encode(&fs::read(product_logo).expect("failed to read product logo"))
+ );
+
+ format!(
+ r##"
+
+
+
+ Hive Router Laboratory
+
+
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
+
+"##,
+ product_logo_data_url = product_logo_data_url,
+ editor_worker = js_string_literal(&editor_worker),
+ typescript_worker = js_string_literal(&typescript_worker),
+ json_worker = js_string_literal(&json_worker),
+ graphql_worker = js_string_literal(&graphql_worker),
+ js_contents = escape_inline_script(&js_contents),
+ )
+}
+
+fn escape_inline_script(value: &str) -> String {
+ value
+ .replace(" String {
+ let mut escaped = String::with_capacity(value.len() + 2);
+ escaped.push('"');
+ for ch in value.chars() {
+ match ch {
+ '\\' => escaped.push_str("\\\\"),
+ '"' => escaped.push_str("\\\""),
+ '\n' => escaped.push_str("\\n"),
+ '\r' => escaped.push_str("\\r"),
+ '\t' => escaped.push_str("\\t"),
+ '\u{2028}' => escaped.push_str("\\u2028"),
+ '\u{2029}' => escaped.push_str("\\u2029"),
+ _ => escaped.push(ch),
+ }
+ }
+ escaped.push('"');
+ escape_inline_script(&escaped)
+}
+
+fn base64_encode(bytes: &[u8]) -> String {
+ const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ let mut output = String::with_capacity(bytes.len().div_ceil(3) * 4);
+
+ let mut chunks = bytes.chunks_exact(3);
+ for chunk in &mut chunks {
+ let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
+ output.push(TABLE[((n >> 18) & 0x3f) as usize] as char);
+ output.push(TABLE[((n >> 12) & 0x3f) as usize] as char);
+ output.push(TABLE[((n >> 6) & 0x3f) as usize] as char);
+ output.push(TABLE[(n & 0x3f) as usize] as char);
+ }
+
+ let remainder = chunks.remainder();
+ if !remainder.is_empty() {
+ let first = remainder[0] as u32;
+ let second = remainder.get(1).copied().unwrap_or_default() as u32;
+ let n = (first << 16) | (second << 8);
+
+ output.push(TABLE[((n >> 18) & 0x3f) as usize] as char);
+ output.push(TABLE[((n >> 12) & 0x3f) as usize] as char);
+ if remainder.len() == 2 {
+ output.push(TABLE[((n >> 6) & 0x3f) as usize] as char);
+ output.push('=');
+ } else {
+ output.push('=');
+ output.push('=');
+ }
+ }
+
+ output
+}
diff --git a/bin/router/src/http_utils/landing_page.rs b/bin/router/src/http_utils/landing_page.rs
index 30585f3a6..1806bda6a 100644
--- a/bin/router/src/http_utils/landing_page.rs
+++ b/bin/router/src/http_utils/landing_page.rs
@@ -6,7 +6,7 @@ static PRODUCT_LOGO_SVG: &str = include_str!("../../static/product_logo.svg");
pub async fn landing_page_handler(graphql_endpoint: String) -> impl Responder {
let rendered_html = LANDING_PAGE_HTML
- .replace("__GRAPHIQL_LINK__", &graphql_endpoint)
+ .replace("__LABORATORY_LINK__", &graphql_endpoint)
.replace("__PRODUCT_NAME__", "Hive Router")
.replace(
"__PRODUCT_DESCRIPTION__",
diff --git a/bin/router/src/lib.rs b/bin/router/src/lib.rs
index 945cfaad4..b00a8d0a8 100644
--- a/bin/router/src/lib.rs
+++ b/bin/router/src/lib.rs
@@ -71,7 +71,7 @@ pub use tokio;
pub use tracing;
use tracing::{info, warn, Instrument};
-static GRAPHIQL_HTML: &str = include_str!("../static/graphiql.html");
+static LABORATORY_HTML: &str = include_str!(concat!(env!("OUT_DIR"), "/laboratory.html"));
async fn graphql_endpoint_handler(
request: HttpRequest,
diff --git a/bin/router/src/pipeline/header.rs b/bin/router/src/pipeline/header.rs
index 7ffc0ea9c..116a282dc 100644
--- a/bin/router/src/pipeline/header.rs
+++ b/bin/router/src/pipeline/header.rs
@@ -12,7 +12,7 @@ use tracing::error;
use crate::pipeline::error::PipelineError;
-/// Non-GraphQL content type, used to detect if the client can accept GraphiQL responses.
+/// Non-GraphQL content type, used to detect if the client can accept Laboratory responses.
pub const TEXT_HTML_MIME: &str = "text/html";
// IMPORTANT: make sure that the serialized string representations are valid because
@@ -175,9 +175,9 @@ pub enum ResponseMode {
StreamOnly(StreamContentType),
/// Will always respond, queries are single responses, subscriptions are streams. errors are single responses.
Dual(SingleContentType, StreamContentType),
- /// Render the GraphiQL IDE for the client. Used when the client prefers accepting HTML responses.
+ /// Render the Laboratory IDE for the client. Used when the client prefers accepting HTML responses.
/// It is different from the other modes because it does not represent a GraphQL response mode.
- GraphiQL,
+ Laboratory,
}
// `#[default]` attribute may only be used on unit enum variants, so we have to implement it
@@ -251,14 +251,14 @@ impl RequestAccepts for HttpRequest {
if self.method() == Method::GET {
// if the client GETs we negotiate with the all supported media type, including HTML
- // to see if the client wants GraphiQL. we negotiate with everything because browsers
+ // to see if the client wants Laboratory. we negotiate with everything because browsers
// tend to send very broad accept headers that include text/html with highest q-weight,
// but would also accept */* which we would interpret as "I want normal GraphQL responses"
- let has_agreed_graphiql = accept
+ let has_agreed_laboratory = accept
.negotiate(ALL_RESPONSE_MODES_CONTENT_TYPE_MEDIA_TYPES.iter())
.is_some_and(|t| *t == HTML_MEDIA_TYPE);
- if has_agreed_graphiql {
- return Ok(ResponseMode::GraphiQL);
+ if has_agreed_laboratory {
+ return Ok(ResponseMode::Laboratory);
}
}
@@ -342,7 +342,7 @@ mod tests {
// actual browser request loading a page
Method::GET,
r#"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"#,
- ResponseMode::GraphiQL,
+ ResponseMode::Laboratory,
),
(
// browser accept header snippet but for a POST request
diff --git a/bin/router/src/pipeline/mod.rs b/bin/router/src/pipeline/mod.rs
index ab00898ce..ee6722dca 100644
--- a/bin/router/src/pipeline/mod.rs
+++ b/bin/router/src/pipeline/mod.rs
@@ -49,7 +49,7 @@ use crate::{
},
schema_state::SchemaState,
shared_state::{RouterRequestDedupeHeaderPolicy, RouterSharedState, SharedRouterResponse},
- GRAPHIQL_HTML,
+ LABORATORY_HTML,
};
use hive_router_internal::telemetry::metrics::catalog::values::GraphQLResponseStatus;
@@ -94,11 +94,11 @@ pub async fn graphql_request_handler(
// agree on the response content type
*response_mode = req.negotiate()?;
- if *response_mode == ResponseMode::GraphiQL {
- if shared_state.router_config.graphiql.enabled {
+ if *response_mode == ResponseMode::Laboratory {
+ if shared_state.router_config.laboratory.enabled {
return Ok(web::HttpResponse::Ok()
.header(CONTENT_TYPE, TEXT_HTML_MIME)
- .body(GRAPHIQL_HTML));
+ .body(LABORATORY_HTML));
} else {
return Ok(web::HttpResponse::NotFound().into());
}
diff --git a/bin/router/static/graphiql.html b/bin/router/static/graphiql.html
deleted file mode 100644
index d11b430d3..000000000
--- a/bin/router/static/graphiql.html
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
- Hive Router GraphiQL
-
-
-
-
-
-
-
- You need to enable JavaScript to run this app.
- Loading GraphiQL...
-
-
-
-
-
\ No newline at end of file
diff --git a/bin/router/static/landing-page.html b/bin/router/static/landing-page.html
index efa0976f2..a40109eb2 100644
--- a/bin/router/static/landing-page.html
+++ b/bin/router/static/landing-page.html
@@ -161,7 +161,7 @@ __PRODUCT_NAME__
@@ -173,7 +173,7 @@ ℹ️ Not the Page You Expected To See?
this
page to be the GraphQL route, you need to configure Hive Router. Currently,
the GraphQL route is configured to be on
- __GRAPHIQL_LINK__ .
+ __LABORATORY_LINK__ .
diff --git a/docs/README.md b/docs/README.md
index 3b53337ae..f8c4c205f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -7,11 +7,11 @@
|[**authorization**](#authorization)|`object`|Default: `{"directives":{"enabled":true,"unauthorized":{"mode":"filter"}}}` |yes|
|[**cors**](#cors)|`object`|Configuration for CORS (Cross-Origin Resource Sharing). Default: `{"allow_any_origin":false,"allow_credentials":false,"enabled":false,"policies":[]}` |yes|
|[**csrf**](#csrf)|`object`|Configuration for CSRF prevention. Default: `{"enabled":false,"required_headers":[]}` ||
-|[**graphiql**](#graphiql)|`object`|Configuration for the GraphiQL interface. Default: `{"enabled":true}` ||
|[**headers**](#headers)|`object`|Configuration for the headers. Default: `{}` ||
|[**http**](#http)|`object`|Configuration for the HTTP server/listener. Default: `{"graphql_endpoint":"/graphql","host":"0.0.0.0","port":4000}` ||
|**introspection**||Configuration to enable or disable introspection queries. ||
|[**jwt**](#jwt)|`object`|Configuration for JWT authentication plugin. |yes|
+|[**laboratory**](#laboratory)|`object`|Configuration for the Hive Laboratory interface. Default: `{"enabled":true}` ||
|[**limits**](#limits)|`object`|Configuration for checking the limits such as query depth, complexity, etc. Default: `{"max_request_body_size":"2 MB"}` ||
|[**log**](#log)|`object`|The router logger configuration. Default: `{"filter":null,"format":"json","level":"info"}` ||
|[**override\_labels**](#override_labels)|`object`|Configuration for overriding labels. ||
@@ -48,8 +48,6 @@ csrf:
enabled: true
required_headers:
- x-csrf-token
-graphiql:
- enabled: true
headers:
all:
request:
@@ -93,6 +91,8 @@ jwt:
- name: authorization
prefix: Bearer
source: header
+laboratory:
+ enabled: true
limits:
max_request_body_size: 2 MB
log:
@@ -501,26 +501,6 @@ A valid HTTP header name, according to RFC 7230.
**Item Type:** `string`
**Item Pattern:** `^[A-Za-z0-9!#$%&'*+\-.^_\`\|~]+$`
-
-## graphiql: object
-
-Configuration for the GraphiQL interface.
-
-
-**Properties**
-
-|Name|Type|Description|Required|
-|----|----|-----------|--------|
-|**enabled**|`boolean`|Enables/disables the GraphiQL interface. By default, the GraphiQL interface is enabled. You can override this setting by setting the `GRAPHIQL_ENABLED` environment variable to `true` or `false`. Default: `true` ||
-
-**Additional Properties:** not allowed
-**Example**
-
-```yaml
-enabled: true
-
-```
-
## headers: object
@@ -1710,6 +1690,26 @@ The first one that is found will be used.
```
+
+## laboratory: object
+
+Configuration for the Hive Laboratory interface.
+
+
+**Properties**
+
+|Name|Type|Description|Required|
+|----|----|-----------|--------|
+|**enabled**|`boolean`|Enables/disables the Hive Laboratory interface. By default, the Hive Laboratory interface is enabled. You can override this setting by setting the `LABORATORY_ENABLED` environment variable to `true` or `false`. Default: `true` ||
+
+**Additional Properties:** not allowed
+**Example**
+
+```yaml
+enabled: true
+
+```
+
## limits: object
diff --git a/lib/router-config/src/env_overrides.rs b/lib/router-config/src/env_overrides.rs
index 8d437219b..4bb3aaeb3 100644
--- a/lib/router-config/src/env_overrides.rs
+++ b/lib/router-config/src/env_overrides.rs
@@ -14,9 +14,9 @@ pub struct EnvVarOverrides {
#[envconfig(from = "LOG_FILTER")]
pub log_filter: Option,
- // GraphiQL overrides
- #[envconfig(from = "GRAPHIQL_ENABLED")]
- pub graphiql_enabled: Option,
+ // Laboratory overrides
+ #[envconfig(from = "LABORATORY_ENABLED")]
+ pub laboratory_enabled: Option,
// HTTP overrides
#[envconfig(from = "PORT")]
@@ -132,9 +132,9 @@ impl EnvVarOverrides {
config = config.set_override("telemetry.hive.target", hive_target)?;
}
- // GraphiQL overrides
- if let Some(graphiql_enabled) = self.graphiql_enabled.take() {
- config = config.set_override("graphiql.enabled", graphiql_enabled)?;
+ // Laboratory overrides
+ if let Some(laboratory_enabled) = self.laboratory_enabled.take() {
+ config = config.set_override("laboratory.enabled", laboratory_enabled)?;
}
Ok(config)
diff --git a/lib/router-config/src/graphiql.rs b/lib/router-config/src/graphiql.rs
deleted file mode 100644
index 6d453c26d..000000000
--- a/lib/router-config/src/graphiql.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-
-#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
-#[serde(deny_unknown_fields)]
-pub struct GraphiQLConfig {
- /// Enables/disables the GraphiQL interface. By default, the GraphiQL interface is enabled.
- ///
- /// You can override this setting by setting the `GRAPHIQL_ENABLED` environment variable to `true` or `false`.
- #[serde(default = "default_graphiql_enabled")]
- pub enabled: bool,
-}
-
-fn default_graphiql_enabled() -> bool {
- true
-}
-
-impl Default for GraphiQLConfig {
- fn default() -> Self {
- Self {
- enabled: default_graphiql_enabled(),
- }
- }
-}
diff --git a/lib/router-config/src/laboratory.rs b/lib/router-config/src/laboratory.rs
new file mode 100644
index 000000000..28b556a9f
--- /dev/null
+++ b/lib/router-config/src/laboratory.rs
@@ -0,0 +1,24 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
+#[serde(deny_unknown_fields)]
+pub struct LaboratoryConfig {
+ /// Enables/disables the Hive Laboratory interface. By default, the Hive Laboratory interface is enabled.
+ ///
+ /// You can override this setting by setting the `LABORATORY_ENABLED` environment variable to `true` or `false`.
+ #[serde(default = "default_laboratory_enabled")]
+ pub enabled: bool,
+}
+
+fn default_laboratory_enabled() -> bool {
+ true
+}
+
+impl Default for LaboratoryConfig {
+ fn default() -> Self {
+ Self {
+ enabled: default_laboratory_enabled(),
+ }
+ }
+}
diff --git a/lib/router-config/src/lib.rs b/lib/router-config/src/lib.rs
index 2f7ebd852..26e5da516 100644
--- a/lib/router-config/src/lib.rs
+++ b/lib/router-config/src/lib.rs
@@ -2,11 +2,11 @@ pub mod authorization;
pub mod cors;
pub mod csrf;
mod env_overrides;
-pub mod graphiql;
pub mod headers;
pub mod http_server;
pub mod introspection_policy;
pub mod jwt_auth;
+pub mod laboratory;
pub mod limits;
pub mod log;
pub mod override_labels;
@@ -28,9 +28,9 @@ use std::{collections::HashMap, convert::Infallible};
use crate::{
env_overrides::{EnvVarOverrides, EnvVarOverridesError},
- graphiql::GraphiQLConfig,
http_server::HttpServerConfig,
introspection_policy::IntrospectionPermissionConfig,
+ laboratory::LaboratoryConfig,
log::LoggingConfig,
override_labels::OverrideLabelsConfig,
primitives::file_path::with_start_path,
@@ -51,9 +51,9 @@ pub struct HiveRouterConfig {
#[serde(default)]
pub log: LoggingConfig,
- /// Configuration for the GraphiQL interface.
+ /// Configuration for the Hive Laboratory interface.
#[serde(default)]
- pub graphiql: GraphiQLConfig,
+ pub laboratory: LaboratoryConfig,
/// Configuration for the Federation supergraph source. By default, the router will use a local file-based supergraph source (`./supergraph.graphql`).
/// Each source has a different set of configuration, depending on the source type.
diff --git a/package-lock.json b/package-lock.json
index a8cd767f0..ce36ec4d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"bench"
],
"devDependencies": {
+ "@graphql-hive/laboratory": "0.1.2",
"cross-spawn": "^7.0.6",
"qs": "^6.15.0"
}
@@ -123,7 +124,7 @@
},
"lib/node-addon": {
"name": "@graphql-hive/router-query-planner",
- "version": "0.0.14",
+ "version": "0.0.17",
"license": "MIT",
"devDependencies": {
"@napi-rs/cli": "3.5.1",
@@ -136,23 +137,132 @@
},
"node_modules/@apollo/cache-control-types": {
"version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz",
- "integrity": "sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"graphql": "14.x || 15.x || 16.x"
}
},
- "node_modules/@apollo/composition": {
- "version": "2.13.2",
- "resolved": "https://registry.npmjs.org/@apollo/composition/-/composition-2.13.2.tgz",
- "integrity": "sha512-E44BbSKuKkDdsNBtQzSU12D9FdggiOZzNR1TghqPjSPEyB1fl9Io1m2gMP0nkwrJUaftsBSP6ZSFF3SgYJfTVg==",
+ "node_modules/@babel/runtime": {
+ "version": "7.28.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@envelop/core": {
+ "version": "5.5.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@envelop/instrumentation": "^1.0.0",
+ "@envelop/types": "^5.2.1",
+ "@whatwg-node/promise-helpers": "^1.2.4",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@envelop/instrumentation": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@whatwg-node/promise-helpers": "^1.2.1",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@envelop/types": {
+ "version": "5.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@whatwg-node/promise-helpers": "^1.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@fastify/busboy": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@graphql-hive/federation-gateway-audit": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@graphql-hive/federation-gateway-audit/-/federation-gateway-audit-0.0.2.tgz",
+ "integrity": "sha512-9+jHme3iNfUHksM20N+ODkzz3t6wkICOBTkge/w29+U7qWRirXn9VLUXs3pbOs1/PpOvoLJXVvJpE7+3mVQN1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@apollo/composition": "^2.13.2",
+ "@apollo/subgraph": "^2.13.2",
+ "async-retry": "^1.3.3",
+ "detect-port": "^2.1.0",
+ "fets": "^0.8.5",
+ "get-port": "^7.1.0",
+ "graphql-yoga": "^5.18.1",
+ "jest-diff": "^30.3.0",
+ "kill-port-process": "^4.0.2",
+ "wait-on": "^9.0.4",
+ "yargs": "^18.0.0"
+ },
+ "bin": {
+ "graphql-federation-audit": "dist/cli.js"
+ },
+ "peerDependencies": {
+ "graphql": "*"
+ }
+ },
+ "node_modules/@graphql-hive/federation-gateway-audit/node_modules/@apollo/composition": {
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/@apollo/composition/-/composition-2.13.3.tgz",
+ "integrity": "sha512-0/vr6vbk1BynEKY6/UV0SZUHBV33p5Ok0y6RhkNnX5BA+ysyOkcyTLPlEHo+ce62nqWx4ml3iOxeKIEHqbKelQ==",
"dev": true,
"license": "Elastic-2.0",
"dependencies": {
- "@apollo/federation-internals": "2.13.2",
- "@apollo/query-graphs": "2.13.2"
+ "@apollo/federation-internals": "2.13.3",
+ "@apollo/query-graphs": "2.13.3"
},
"engines": {
"node": ">=18"
@@ -161,10 +271,10 @@
"graphql": "^16.5.0"
}
},
- "node_modules/@apollo/federation-internals": {
- "version": "2.13.2",
- "resolved": "https://registry.npmjs.org/@apollo/federation-internals/-/federation-internals-2.13.2.tgz",
- "integrity": "sha512-m9waen8iZhrzn23qkemlytqBCPoKU6AMv5k3eFYnu6yzoJ5/ONh6SMGE7CvrWsECcpnOLYOinxD19igEHm6Azw==",
+ "node_modules/@graphql-hive/federation-gateway-audit/node_modules/@apollo/federation-internals": {
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/@apollo/federation-internals/-/federation-internals-2.13.3.tgz",
+ "integrity": "sha512-4zHgqznZza5Qx2CZy1qlrh83BB/3Yd8BqD/dmhGzLCvHJQ1LBTyZbWN7xwKs5z5FbxdSPkL5TvPoLm5Rek8/4g==",
"dev": true,
"license": "Elastic-2.0",
"dependencies": {
@@ -180,14 +290,14 @@
"graphql": "^16.5.0"
}
},
- "node_modules/@apollo/query-graphs": {
- "version": "2.13.2",
- "resolved": "https://registry.npmjs.org/@apollo/query-graphs/-/query-graphs-2.13.2.tgz",
- "integrity": "sha512-iXb7Ut72BxOIKYT1Xbmmt5jONa/kB0iuQyWKhKIJdwIw+p8VSwJvaSFtzjDZ7tYTo/QH9l8yGzAdliJVR+/7cA==",
+ "node_modules/@graphql-hive/federation-gateway-audit/node_modules/@apollo/query-graphs": {
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/@apollo/query-graphs/-/query-graphs-2.13.3.tgz",
+ "integrity": "sha512-X/bgsIVmamAzd8IFMDo9upO5Oi/uhAZlcBmna6yEFlN0fzdQZMHL5pp1fyzRG1wxCFi3lVjDXw2dc/380uWB9g==",
"dev": true,
"license": "Elastic-2.0",
"dependencies": {
- "@apollo/federation-internals": "2.13.2",
+ "@apollo/federation-internals": "2.13.3",
"deep-equal": "^2.0.5",
"ts-graphviz": "^1.5.4",
"uuid": "^9.0.0"
@@ -199,15 +309,15 @@
"graphql": "^16.5.0"
}
},
- "node_modules/@apollo/subgraph": {
- "version": "2.13.2",
- "resolved": "https://registry.npmjs.org/@apollo/subgraph/-/subgraph-2.13.2.tgz",
- "integrity": "sha512-hNJv6kcGhtWdBfGWnSZwgfmSm4ZAJ3AoPmDFOXaloSE1wKmKz0NmPsef4nkC2/AUD9/FrQMKm5xzyBYTDrpNSw==",
+ "node_modules/@graphql-hive/federation-gateway-audit/node_modules/@apollo/subgraph": {
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/@apollo/subgraph/-/subgraph-2.13.3.tgz",
+ "integrity": "sha512-nmdACpdTe+kgmNF0Yow3MoDkE0SMfvK6tTsEs3h3M9GLYCgBWZWBoDWQfNdr7kWRW6aan2SylU0K0ktF643h3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@apollo/cache-control-types": "^1.0.2",
- "@apollo/federation-internals": "2.13.2"
+ "@apollo/federation-internals": "2.13.3"
},
"engines": {
"node": ">=14.15.0"
@@ -216,85 +326,90 @@
"graphql": "^16.5.0"
}
},
- "node_modules/@babel/runtime": {
- "version": "7.28.4",
+ "node_modules/@graphql-hive/federation-gateway-audit/node_modules/@jest/diff-sequences": {
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz",
+ "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=6.9.0"
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
- "node_modules/@envelop/core": {
- "version": "5.5.1",
- "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.5.1.tgz",
- "integrity": "sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw==",
+ "node_modules/@graphql-hive/federation-gateway-audit/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "@envelop/instrumentation": "^1.0.0",
- "@envelop/types": "^5.2.1",
- "@whatwg-node/promise-helpers": "^1.2.4",
- "tslib": "^2.5.0"
- },
"engines": {
- "node": ">=18.0.0"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/@envelop/instrumentation": {
- "version": "1.0.0",
+ "node_modules/@graphql-hive/federation-gateway-audit/node_modules/jest-diff": {
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz",
+ "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@whatwg-node/promise-helpers": "^1.2.1",
- "tslib": "^2.5.0"
+ "@jest/diff-sequences": "30.3.0",
+ "@jest/get-type": "30.1.0",
+ "chalk": "^4.1.2",
+ "pretty-format": "30.3.0"
},
"engines": {
- "node": ">=18.0.0"
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
- "node_modules/@envelop/types": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/@envelop/types/-/types-5.2.1.tgz",
- "integrity": "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==",
+ "node_modules/@graphql-hive/federation-gateway-audit/node_modules/pretty-format": {
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
+ "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@whatwg-node/promise-helpers": "^1.0.0",
- "tslib": "^2.5.0"
+ "@jest/schemas": "30.0.5",
+ "ansi-styles": "^5.2.0",
+ "react-is": "^18.3.1"
},
"engines": {
- "node": ">=18.0.0"
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
- "node_modules/@fastify/busboy": {
- "version": "3.2.0",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@graphql-hive/federation-gateway-audit": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/@graphql-hive/federation-gateway-audit/-/federation-gateway-audit-0.0.2.tgz",
- "integrity": "sha512-9+jHme3iNfUHksM20N+ODkzz3t6wkICOBTkge/w29+U7qWRirXn9VLUXs3pbOs1/PpOvoLJXVvJpE7+3mVQN1w==",
+ "node_modules/@graphql-hive/laboratory": {
+ "version": "0.1.2",
"dev": true,
"license": "MIT",
"dependencies": {
- "@apollo/composition": "^2.13.2",
- "@apollo/subgraph": "^2.13.2",
- "async-retry": "^1.3.3",
- "detect-port": "^2.1.0",
- "fets": "^0.8.5",
- "get-port": "^7.1.0",
- "graphql-yoga": "^5.18.1",
- "jest-diff": "^30.3.0",
- "kill-port-process": "^4.0.2",
- "wait-on": "^9.0.4",
- "yargs": "^18.0.0"
- },
- "bin": {
- "graphql-federation-audit": "dist/cli.js"
+ "radix-ui": "^1.4.3",
+ "uuid": "^13.0.0"
},
"peerDependencies": {
- "graphql": "*"
+ "@tanstack/react-form": "^1.23.8",
+ "date-fns": "^4.1.0",
+ "graphql-ws": "^6.0.6",
+ "lucide-react": "^0.548.0",
+ "lz-string": "^1.5.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0",
+ "tslib": "^2.8.1",
+ "zod": "^4.1.12"
+ }
+ },
+ "node_modules/@graphql-hive/laboratory/node_modules/uuid": {
+ "version": "13.0.0",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
}
},
"node_modules/@graphql-hive/router-query-planner": {
@@ -303,8 +418,6 @@
},
"node_modules/@graphql-tools/executor": {
"version": "1.5.1",
- "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.5.1.tgz",
- "integrity": "sha512-n94Qcu875Mji9GQ52n5UbgOTxlgvFJicBPYD+FRks9HKIQpdNPjkkrKZUYNG51XKa+bf03rxNflm4+wXhoHHrA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -324,8 +437,6 @@
},
"node_modules/@graphql-tools/executor/node_modules/@graphql-tools/utils": {
"version": "11.0.0",
- "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz",
- "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -343,8 +454,6 @@
},
"node_modules/@graphql-tools/merge": {
"version": "9.1.7",
- "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.7.tgz",
- "integrity": "sha512-Y5E1vTbTabvcXbkakdFUt4zUIzB1fyaEnVmIWN0l0GMed2gdD01TpZWLUm4RNAxpturvolrb24oGLQrBbPLSoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -360,8 +469,6 @@
},
"node_modules/@graphql-tools/merge/node_modules/@graphql-tools/utils": {
"version": "11.0.0",
- "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz",
- "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -379,8 +486,6 @@
},
"node_modules/@graphql-tools/schema": {
"version": "10.0.31",
- "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.31.tgz",
- "integrity": "sha512-ZewRgWhXef6weZ0WiP7/MV47HXiuFbFpiDUVLQl6mgXsWSsGELKFxQsyUCBos60Qqy1JEFAIu3Ns6GGYjGkqkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -397,8 +502,6 @@
},
"node_modules/@graphql-tools/schema/node_modules/@graphql-tools/utils": {
"version": "11.0.0",
- "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz",
- "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -416,8 +519,6 @@
},
"node_modules/@graphql-tools/utils": {
"version": "10.11.0",
- "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.11.0.tgz",
- "integrity": "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -435,8 +536,6 @@
},
"node_modules/@graphql-typed-document-node/core": {
"version": "3.2.0",
- "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
- "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -445,8 +544,6 @@
},
"node_modules/@graphql-yoga/logger": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@graphql-yoga/logger/-/logger-2.0.1.tgz",
- "integrity": "sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -458,8 +555,6 @@
},
"node_modules/@graphql-yoga/subscription": {
"version": "5.0.5",
- "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-5.0.5.tgz",
- "integrity": "sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -474,8 +569,6 @@
},
"node_modules/@graphql-yoga/typed-event-target": {
"version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@graphql-yoga/typed-event-target/-/typed-event-target-3.0.2.tgz",
- "integrity": "sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -840,16 +933,6 @@
}
}
},
- "node_modules/@jest/diff-sequences": {
- "version": "30.3.0",
- "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz",
- "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
- }
- },
"node_modules/@jest/get-type": {
"version": "30.1.0",
"dev": true,
@@ -860,8 +943,6 @@
},
"node_modules/@jest/schemas": {
"version": "30.0.5",
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
- "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1001,21 +1082,6 @@
"@napi-rs/lzma-win32-x64-msvc": "1.4.5"
}
},
- "node_modules/@napi-rs/lzma-linux-x64-gnu": {
- "version": "1.4.5",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@napi-rs/tar": {
"version": "1.1.0",
"dev": true,
@@ -1042,21 +1108,6 @@
"@napi-rs/tar-win32-x64-msvc": "1.1.0"
}
},
- "node_modules/@napi-rs/tar-linux-x64-gnu": {
- "version": "1.1.0",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@napi-rs/wasm-tools": {
"version": "1.0.1",
"dev": true,
@@ -1080,21 +1131,6 @@
"@napi-rs/wasm-tools-win32-x64-msvc": "1.0.1"
}
},
- "node_modules/@napi-rs/wasm-tools-linux-x64-gnu": {
- "version": "1.0.1",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@octokit/auth-token": {
"version": "6.0.0",
"dev": true,
@@ -1229,50 +1265,1588 @@
"node": ">= 20"
}
},
- "node_modules/@octokit/types": {
- "version": "16.0.0",
+ "node_modules/@octokit/types": {
+ "version": "16.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^27.0.0"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-accessible-icon": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.2.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collapsible": "1.1.12",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-alert-dialog": {
+ "version": "1.1.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dialog": "1.1.15",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-aspect-ratio": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context-menu": {
+ "version": "2.2.16",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-form": {
+ "version": "0.1.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-label": "2.1.7",
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.1.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menubar": {
+ "version": "1.1.16",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-navigation-menu": {
+ "version": "1.2.14",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-one-time-password-field": {
+ "version": "0.1.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-password-toggle-field": {
+ "version": "0.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-is-hydrated": "0.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-radio-group": {
+ "version": "1.3.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slider": {
+ "version": "1.3.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.2.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle": {
+ "version": "1.1.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle-group": {
+ "version": "1.1.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-toggle": "1.1.10",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toolbar": {
+ "version": "1.1.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-separator": "1.1.7",
+ "@radix-ui/react-toggle-group": "1.1.11"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@repeaterjs/repeater": {
+ "version": "3.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sec-ant/readable-stream": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.34.41",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sindresorhus/merge-streams": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tanstack/devtools-event-client": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.4.3.tgz",
+ "integrity": "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "intent": "bin/intent.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/form-core": {
+ "version": "1.28.6",
+ "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.28.6.tgz",
+ "integrity": "sha512-4zroxL6VDj5O+w7l3dYZnUeL/h30KtNSV7UWzKAL7cl+8clMFdISPDlDlluS37As7oqvPVKo8B83VlIBvgmRog==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
- "@octokit/openapi-types": "^27.0.0"
+ "@tanstack/devtools-event-client": "^0.4.1",
+ "@tanstack/pacer-lite": "^0.1.1",
+ "@tanstack/store": "^0.9.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
}
},
- "node_modules/@repeaterjs/repeater": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz",
- "integrity": "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@sec-ant/readable-stream": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
- "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
+ "node_modules/@tanstack/pacer-lite": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/pacer-lite/-/pacer-lite-0.1.1.tgz",
+ "integrity": "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
},
- "node_modules/@sinclair/typebox": {
- "version": "0.34.41",
+ "node_modules/@tanstack/react-form": {
+ "version": "1.28.6",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.28.6.tgz",
+ "integrity": "sha512-dRxwKeNW3uuJvf0sXsIQ2compFMnIJNk9B436Lx0fqkqK+CBvA1tNmEdX+faoCpuQ5Wua3c8ahVibJ65cpkijA==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@tanstack/form-core": "1.28.6",
+ "@tanstack/react-store": "^0.9.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@tanstack/react-start": {
+ "optional": true
+ }
+ }
},
- "node_modules/@sindresorhus/merge-streams": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
- "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
+ "node_modules/@tanstack/react-store": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz",
+ "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=18"
+ "peer": true,
+ "dependencies": {
+ "@tanstack/store": "0.9.3",
+ "use-sync-external-store": "^1.6.0"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
- "node_modules/@standard-schema/spec": {
- "version": "1.0.0",
+ "node_modules/@tanstack/store": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz",
+ "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
},
"node_modules/@types/k6": {
"version": "1.6.0",
@@ -1293,8 +2867,6 @@
},
"node_modules/@types/uuid": {
"version": "9.0.8",
- "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
- "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"dev": true,
"license": "MIT"
},
@@ -1324,8 +2896,6 @@
},
"node_modules/@whatwg-node/events": {
"version": "0.1.2",
- "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.2.tgz",
- "integrity": "sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1429,10 +2999,19 @@
"version": "2.0.1",
"license": "Python-2.0"
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/array-buffer-byte-length": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
- "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1461,8 +3040,6 @@
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
- "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
- "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1477,8 +3054,6 @@
},
"node_modules/axios": {
"version": "1.13.5",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
- "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1503,8 +3078,6 @@
},
"node_modules/call-bind": {
"version": "1.0.8",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
- "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1644,8 +3217,6 @@
},
"node_modules/cross-inspect": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz",
- "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1657,8 +3228,6 @@
},
"node_modules/cross-spawn": {
"version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1672,14 +3241,24 @@
},
"node_modules/cross-spawn/node_modules/path-key": {
"version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"dev": true,
@@ -1698,8 +3277,6 @@
},
"node_modules/deep-equal": {
"version": "2.2.3",
- "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
- "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1731,8 +3308,6 @@
},
"node_modules/define-data-property": {
"version": "1.1.4",
- "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
- "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1749,8 +3324,6 @@
},
"node_modules/define-properties": {
"version": "1.2.1",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
- "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1773,6 +3346,11 @@
"node": ">=0.4.0"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/detect-port": {
"version": "2.1.0",
"dev": true,
@@ -1846,8 +3424,6 @@
},
"node_modules/es-get-iterator": {
"version": "1.1.3",
- "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
- "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1923,8 +3499,6 @@
},
"node_modules/execa": {
"version": "9.6.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
- "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1950,8 +3524,6 @@
},
"node_modules/execa/node_modules/human-signals": {
"version": "8.0.1",
- "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
- "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -1960,8 +3532,6 @@
},
"node_modules/execa/node_modules/strip-final-newline": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
- "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2007,8 +3577,6 @@
},
"node_modules/figures": {
"version": "6.1.0",
- "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
- "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2042,8 +3610,6 @@
},
"node_modules/for-each": {
"version": "0.3.5",
- "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
- "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2086,8 +3652,6 @@
},
"node_modules/functions-have-names": {
"version": "1.2.3",
- "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
- "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
"dev": true,
"license": "MIT",
"funding": {
@@ -2136,6 +3700,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-port": {
"version": "7.1.0",
"dev": true,
@@ -2161,8 +3733,6 @@
},
"node_modules/get-stream": {
"version": "9.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
- "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2194,18 +3764,42 @@
},
"node_modules/graphql": {
"version": "16.13.1",
- "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz",
- "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
},
+ "node_modules/graphql-ws": {
+ "version": "6.0.8",
+ "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.8.tgz",
+ "integrity": "sha512-m3EOaNsUBXwAnkBWbzPfe0Nq8pXUfxsWnolC54sru3FzHvhTZL0Ouf/BoQsaGAXqM+YPerXOJ47BUnmgmoupCw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@fastify/websocket": "^10 || ^11",
+ "crossws": "~0.3",
+ "graphql": "^15.10.1 || ^16",
+ "ws": "^8"
+ },
+ "peerDependenciesMeta": {
+ "@fastify/websocket": {
+ "optional": true
+ },
+ "crossws": {
+ "optional": true
+ },
+ "ws": {
+ "optional": true
+ }
+ }
+ },
"node_modules/graphql-yoga": {
"version": "5.18.1",
- "resolved": "https://registry.npmjs.org/graphql-yoga/-/graphql-yoga-5.18.1.tgz",
- "integrity": "sha512-GiDSlvibfY/vyurbkBOKMO+adF6bTCzKr80FYWta59s1Lx87oVo8T6Nu/0hrxrGv0SrAUkqUpcpr4Ee7XlYmBw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2231,8 +3825,6 @@
},
"node_modules/has-bigints": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
- "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2252,8 +3844,6 @@
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
- "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2329,8 +3919,6 @@
},
"node_modules/internal-slot": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
- "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2344,8 +3932,6 @@
},
"node_modules/is-arguments": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
- "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2361,8 +3947,6 @@
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
- "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
- "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2379,8 +3963,6 @@
},
"node_modules/is-bigint": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
- "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2395,8 +3977,6 @@
},
"node_modules/is-boolean-object": {
"version": "1.2.2",
- "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
- "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2412,8 +3992,6 @@
},
"node_modules/is-callable": {
"version": "1.2.7",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
- "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2425,8 +4003,6 @@
},
"node_modules/is-date-object": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
- "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2442,8 +4018,6 @@
},
"node_modules/is-map": {
"version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
- "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2455,8 +4029,6 @@
},
"node_modules/is-number-object": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
- "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2472,8 +4044,6 @@
},
"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==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2485,8 +4055,6 @@
},
"node_modules/is-regex": {
"version": "1.2.1",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
- "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2504,8 +4072,6 @@
},
"node_modules/is-set": {
"version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
- "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2517,8 +4083,6 @@
},
"node_modules/is-shared-array-buffer": {
"version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
- "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2533,8 +4097,6 @@
},
"node_modules/is-stream": {
"version": "4.0.1",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
- "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2546,8 +4108,6 @@
},
"node_modules/is-string": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
- "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2563,8 +4123,6 @@
},
"node_modules/is-symbol": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
- "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2581,8 +4139,6 @@
},
"node_modules/is-unicode-supported": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
- "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2594,8 +4150,6 @@
},
"node_modules/is-weakmap": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
- "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2607,8 +4161,6 @@
},
"node_modules/is-weakset": {
"version": "2.0.4",
- "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
- "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2624,34 +4176,14 @@
},
"node_modules/isarray": {
"version": "2.0.5",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
- "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true,
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
- "node_modules/jest-diff": {
- "version": "30.3.0",
- "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz",
- "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/diff-sequences": "30.3.0",
- "@jest/get-type": "30.1.0",
- "chalk": "^4.1.2",
- "pretty-format": "30.3.0"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
- }
- },
"node_modules/joi": {
"version": "18.0.2",
"dev": true,
@@ -2671,8 +4203,6 @@
},
"node_modules/js-levenshtein": {
"version": "1.1.6",
- "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
- "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2703,8 +4233,6 @@
},
"node_modules/kill-port-process": {
"version": "4.0.2",
- "resolved": "https://registry.npmjs.org/kill-port-process/-/kill-port-process-4.0.2.tgz",
- "integrity": "sha512-fO8gc45EYJQUQWozPBmdTpsR0GDvldsmrhP2I4FPoNejwyBY4Liiwj9Is7P/5rj6k07ZQ5Ob0g0k2dqQcslW/w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -2728,18 +4256,36 @@
},
"node_modules/lodash": {
"version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"dev": true,
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
+ "node_modules/lucide-react": {
+ "version": "0.548.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.548.0.tgz",
+ "integrity": "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
"node_modules/map-stream": {
"version": "0.1.0",
"dev": true
@@ -2806,8 +4352,6 @@
},
"node_modules/npm-run-path": {
"version": "6.0.0",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
- "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2834,8 +4378,6 @@
},
"node_modules/object-is": {
"version": "1.1.6",
- "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
- "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2851,8 +4393,6 @@
},
"node_modules/object-keys": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2861,8 +4401,6 @@
},
"node_modules/object.assign": {
"version": "4.1.7",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
- "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2905,8 +4443,6 @@
},
"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==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2918,8 +4454,6 @@
},
"node_modules/path-key": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
- "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2942,8 +4476,6 @@
},
"node_modules/pid-port": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pid-port/-/pid-port-2.0.1.tgz",
- "integrity": "sha512-pnLo01AmMclw8l+/gfknsP2N351oe8VkVmCLFUvJZ11NRPPmghJrv0OcwsdgPQxsZkFYwm6hPWW0JKmXYCaXAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2958,46 +4490,14 @@
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
- "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
- "node_modules/pretty-format": {
- "version": "30.3.0",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
- "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "30.0.5",
- "ansi-styles": "^5.2.0",
- "react-is": "^18.3.1"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
- }
- },
- "node_modules/pretty-format/node_modules/ansi-styles": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
- "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
"node_modules/pretty-ms": {
"version": "9.3.0",
- "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
- "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3031,8 +4531,6 @@
},
"node_modules/qs": {
"version": "6.15.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
- "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -3045,17 +4543,180 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/radix-ui": {
+ "version": "1.4.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-accessible-icon": "1.1.7",
+ "@radix-ui/react-accordion": "1.2.12",
+ "@radix-ui/react-alert-dialog": "1.1.15",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-aspect-ratio": "1.1.7",
+ "@radix-ui/react-avatar": "1.1.10",
+ "@radix-ui/react-checkbox": "1.3.3",
+ "@radix-ui/react-collapsible": "1.1.12",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-context-menu": "2.2.16",
+ "@radix-ui/react-dialog": "1.1.15",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-dropdown-menu": "2.1.16",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-form": "0.1.8",
+ "@radix-ui/react-hover-card": "1.1.15",
+ "@radix-ui/react-label": "2.1.7",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-menubar": "1.1.16",
+ "@radix-ui/react-navigation-menu": "1.2.14",
+ "@radix-ui/react-one-time-password-field": "0.1.8",
+ "@radix-ui/react-password-toggle-field": "0.1.3",
+ "@radix-ui/react-popover": "1.1.15",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-progress": "1.1.7",
+ "@radix-ui/react-radio-group": "1.3.8",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-scroll-area": "1.2.10",
+ "@radix-ui/react-select": "2.2.6",
+ "@radix-ui/react-separator": "1.1.7",
+ "@radix-ui/react-slider": "1.3.6",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-switch": "1.2.6",
+ "@radix-ui/react-tabs": "1.1.13",
+ "@radix-ui/react-toast": "1.2.15",
+ "@radix-ui/react-toggle": "1.1.10",
+ "@radix-ui/react-toggle-group": "1.1.11",
+ "@radix-ui/react-toolbar": "1.1.11",
+ "@radix-ui/react-tooltip": "1.2.8",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-escape-keydown": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
- "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
- "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
- "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3095,8 +4756,6 @@
},
"node_modules/safe-regex-test": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
- "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3116,6 +4775,14 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/semver": {
"version": "7.7.3",
"dev": true,
@@ -3129,8 +4796,6 @@
},
"node_modules/set-function-length": {
"version": "1.2.2",
- "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
- "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3147,8 +4812,6 @@
},
"node_modules/set-function-name": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
- "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3163,8 +4826,6 @@
},
"node_modules/shebang-command": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3176,8 +4837,6 @@
},
"node_modules/shebang-regex": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3276,8 +4935,6 @@
},
"node_modules/start-server-and-test": {
"version": "2.1.5",
- "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.1.5.tgz",
- "integrity": "sha512-A/SbXpgXE25ScSkpLLqvGvVZT0ykN6+AzS8tVqMBCTxbJy2Nwuen59opT+afalK5aS+AuQmZs0EsLwjnuDN+/g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3301,8 +4958,6 @@
},
"node_modules/start-server-and-test/node_modules/execa": {
"version": "5.1.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
- "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3325,8 +4980,6 @@
},
"node_modules/start-server-and-test/node_modules/get-stream": {
"version": "6.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
- "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3338,8 +4991,6 @@
},
"node_modules/start-server-and-test/node_modules/is-stream": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
- "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3351,8 +5002,6 @@
},
"node_modules/start-server-and-test/node_modules/npm-run-path": {
"version": "4.0.1",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
- "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3364,8 +5013,6 @@
},
"node_modules/start-server-and-test/node_modules/path-key": {
"version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3374,15 +5021,11 @@
},
"node_modules/start-server-and-test/node_modules/signal-exit": {
"version": "3.0.7",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
- "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true,
"license": "ISC"
},
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
- "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3462,8 +5105,6 @@
},
"node_modules/ts-graphviz": {
"version": "1.8.2",
- "resolved": "https://registry.npmjs.org/ts-graphviz/-/ts-graphviz-1.8.2.tgz",
- "integrity": "sha512-5YhbFoHmjxa7pgQLkB07MtGnGJ/yhvjmc9uhsnDBEICME6gkPf83SBwLDQqGDoCa3XzUMWLk1AU2Wn1u1naDtA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3513,8 +5154,6 @@
},
"node_modules/unicorn-magic": {
"version": "0.3.0",
- "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
- "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3534,10 +5173,57 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/uuid": {
"version": "9.0.1",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
- "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa",
@@ -3550,8 +5236,6 @@
},
"node_modules/wait-on": {
"version": "9.0.4",
- "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz",
- "integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3570,8 +5254,6 @@
},
"node_modules/which": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -3586,8 +5268,6 @@
},
"node_modules/which-boxed-primitive": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
- "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3606,8 +5286,6 @@
},
"node_modules/which-collection": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
- "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3625,8 +5303,6 @@
},
"node_modules/which-typed-array": {
"version": "1.1.20",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
- "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3706,8 +5382,6 @@
},
"node_modules/yoctocolors": {
"version": "2.1.2",
- "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
- "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3716,6 +5390,17 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/package.json b/package.json
index 82e9c6c80..6ea2a8485 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
],
"packageManager": "npm@11.11.1",
"devDependencies": {
+ "@graphql-hive/laboratory": "0.1.2",
"cross-spawn": "^7.0.6",
"qs": "^6.15.0"
},
@@ -17,4 +18,4 @@
"cross-spawn": "^7.0.6",
"qs": "^6.15.0"
}
-}
\ No newline at end of file
+}
From 4f1be62af5041b23a5f68889c49ba7f7949c8446 Mon Sep 17 00:00:00 2001
From: Kamil Kisiela
Date: Fri, 3 Apr 2026 16:14:33 +0200
Subject: [PATCH 15/76] chore: refactor install script for better UX (#892)
Introduce color support, progress indicators, and more user-friendly
messages. Update documentation links.
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
install.sh | 116 ++++++++++++++++++++++++++++++++++-------------------
1 file changed, 74 insertions(+), 42 deletions(-)
diff --git a/install.sh b/install.sh
index 0368dab8f..70cfc39b6 100755
--- a/install.sh
+++ b/install.sh
@@ -1,6 +1,23 @@
#!/bin/sh
set -e
+# Color support: check NO_COLOR or non-TTY (stdout)
+if [ -n "${NO_COLOR+x}" ] || [ ! -t 1 ]; then
+ BLUE=''
+ GREEN=''
+ RED=''
+ BOLD=''
+ DIM=''
+ NC=''
+else
+ BLUE='\033[0;34m'
+ GREEN='\033[0;32m'
+ RED='\033[0;31m'
+ BOLD='\033[1m'
+ DIM='\033[2m'
+ NC='\033[0m'
+fi
+
# ==============================================================================
# Hive Router
#
@@ -14,40 +31,32 @@ GH_REPO="router"
BINARY_NAME="hive_router"
CARGO_PKG_NAME="hive-router"
-info() {
- echo "\033[34m[INFO]\033[0m $1"
+print_step() {
+ printf '%b\n' "${BLUE}▸${NC} $1"
+}
+
+print_success() {
+ # Move up one line and clear it
+ printf '%b' "\033[1A\033[2K\r"
+ printf '%b\n' "${GREEN}✓${NC} $1"
}
-error() {
- echo "\033[31m[ERROR]\033[0m $1" >&2
- exit 1
+print_error() {
+ printf '%b\n' "${RED}✗${NC} $1"
}
check_tool() {
if ! command -v "$1" >/dev/null 2>&1; then
- error "'$1' is required but it's not installed. Please install it to continue."
+ print_error "'$1' is required but it's not installed. Please install it to continue."
+ exit 1
fi
}
-banner() {
- echo " @@@@@@@@@@@@@ "
- echo " @@ "
- echo " @@@ #++++# +@@ @@@ @@@ @@ @@@@@@@@@ @@# "
- echo " @@@ @@@@@@@@@ @@@ @@@ =@@ @@ =@@ @@@ "
- echo " @@@ @@@ @@ @@@ @@@ @@ @@ @@# @@@ @@@@@@@ @@ @@ @@@@@@@ @@ @@ @@@@@+ @@@@@@@ @@#@@@ "
- echo " @@@ @@@ @@@ @@@ @@@@@@@@@@@ @@ @@ @@ @@ @@ @@@@@@@@@ @@# @@ @@ @@ @@% @@ @@ @@@ "
- echo " @@@ @@ @@@ @@@ @@@ @@ @@ @@ @@ @@@@@@@@@ @@+ @@@ @@ @@ @@ @@ @@@ @@@@@@@@@ @@ "
- echo " @@@ @@@@@@@@@ @@@ @@@ =@@ @@ @@ @@ @@ @@ @@* @@ @@@ @@ @@ @@ @@% @@ @@ @@ "
- echo " @ @@ @@@ @@@ @@ @@@ @@@@@@@ @@@ @@ :@@@@@@ @@@@@@@@ @@@* #@@@@@@ @@= "
- echo " @@@@@@@@@@@@@@@ "
- echo " @@@@@@@@@@@@ "
-}
-
detect_arch() {
OS_TYPE=$(uname -s)
ARCH=$(uname -m)
- info "Detecting operating system and architecture..."
+ print_step "Detecting system architecture..."
case $OS_TYPE in
Linux)
@@ -57,7 +66,8 @@ detect_arch() {
OS="macos"
;;
*)
- error "Unsupported operating system: '$OS_TYPE'. You may use Hive Router using Docker by following the instructions at https://github.com/graphql-hive/router#docker"
+ print_error "Unsupported operating system: '$OS_TYPE'. You may use Hive Router using Docker by following the instructions at https://the-guild.dev/graphql/hive/docs/router/getting-started"
+ exit 1
;;
esac
@@ -69,19 +79,20 @@ detect_arch() {
ARCH="arm64"
;;
*)
- error "Unsupported architecture: '$ARCH'. You may use Hive Router using Docker by following the instructions at https://github.com/graphql-hive/router#docker"
+ print_error "Unsupported architecture: '$ARCH'. You may use Hive Router using Docker by following the instructions at https://the-guild.dev/graphql/hive/docs/router/getting-started"
+ exit 1
;;
esac
- info "System detected: ${OS}-${ARCH}"
+ print_success "Detected ${OS}-${ARCH}"
}
get_version() {
+ print_step "Resolving version..."
+
if [ -n "$1" ]; then
VERSION="$1"
- info "Installing specified version: $VERSION"
+ print_success "Using specified version: $VERSION"
else
- info "No version specified. Fetching the latest version from crates.io index..."
-
# Uses index.crates.io which is more reliable than the rate-limited Crates API
# Path structure: /first_two_chars/next_two_chars/full_name
CRATE_FIRST_TWO=$(echo "${CARGO_PKG_NAME}" | cut -c1-2)
@@ -93,44 +104,65 @@ get_version() {
VERSION="v$(curl -sL "$INDEX_URL" | tail -1 | grep -o '"vers":"[^"]*"' | sed 's/"vers":"\([^"]*\)"/\1/')"
if [ -z "$VERSION" ] || [ "$VERSION" = "v" ]; then
- error "Could not determine the latest version from crates.io index. Please check the crate name or specify a version manually."
+ print_error "Could not determine the latest version from crates.io index. Please check the crate name or specify a version manually."
+ exit 1
fi
- info "Latest version found: $VERSION"
+ print_success "Latest version found: $VERSION"
fi
}
-download_and_install() {
+download_binary() {
ASSET_NAME="${BINARY_NAME}_${OS}_${ARCH}"
DOWNLOAD_URL="https://github.com/${GH_OWNER}/${GH_REPO}/releases/download/hive-router%2F${VERSION}/${ASSET_NAME}"
- info "Downloading binary from: ${DOWNLOAD_URL}"
+ print_step "Downloading Hive Router binary..."
+ printf '%b\n' "${DIM} Download URL: ${DOWNLOAD_URL}${NC}"
# -f: Fail silently on server errors (like 404)
# -L: Follow redirects
- if ! curl -fL -o "./${BINARY_NAME}" "${DOWNLOAD_URL}"; then
- error "Download failed. Please check if the version '$VERSION' and architecture '$OS_TYPE/$ARCH' exist for this release."
+ if ! curl -fL --progress-bar -o "./${BINARY_NAME}" "${DOWNLOAD_URL}"; then
+ print_error "Download failed. Please check if the version '$VERSION' and architecture '$OS_TYPE/$ARCH' exist for this release."
+ exit 1
fi
- chmod +x "./${BINARY_NAME}"
+ if [ -t 1 ]; then
+ printf '%b' "\033[1A\033[2K\r"
+ printf '%b' "\033[1A\033[2K\r"
+ fi
- info "✅ Successfully installed '${BINARY_NAME}' to the current directory."
- info "You can now run it with: ./${BINARY_NAME}"
- info ""
- info "Getting started instructions: https://github.com/graphql-hive/router#try-it-out"
- info "Config file reference: https://github.com/graphql-hive/router/blob/main/docs/README.md"
+ print_success "Binary downloaded"
+}
+
+install_binary() {
+ print_step "Finalizing installation..."
+ chmod +x "./${BINARY_NAME}"
+ print_success "Binary made executable"
}
main() {
+ printf '\n'
+ printf '%b\n' "${BOLD}Hive Router Installer${NC}"
+ printf '\n'
+
check_tool "curl"
check_tool "grep"
check_tool "sed"
check_tool "uname"
- banner
-
detect_arch
get_version "$1"
- download_and_install
+ download_binary
+ install_binary
+
+ printf '\n'
+ printf '%b\n' "${BOLD}${GREEN}✨ Installation Complete!${NC}"
+ printf '\n'
+ printf '%b\n' "${BOLD}Start using Hive Router:${NC}"
+ printf '%b\n' " ${BOLD}${BINARY_NAME}${NC}"
+ printf '\n'
+ printf '%b\n' "${BOLD}Documentation:${NC}"
+ printf '%b\n' " https://the-guild.dev/graphql/hive/docs/router"
+ printf '\n'
}
main "$@"
From fc1b7281920ed7a681d3db3a965a1cd6b23d33a1 Mon Sep 17 00:00:00 2001
From: Denis Badurina
Date: Sun, 12 Apr 2026 10:34:42 +0200
Subject: [PATCH 16/76] fix(deps): update ntex and its code, retry e2e tests
before failing ci, update npm lockfile, deflakefy tests (#899)
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.github/workflows/ci.yaml | 7 +-
Cargo.lock | 148 +-
Cargo.toml | 2 +-
e2e/src/hive_cdn_supergraph.rs | 37 +-
e2e/src/http.rs | 22 +-
e2e/src/probes.rs | 8 +-
e2e/src/supergraph.rs | 137 +-
e2e/src/telemetry/metrics.rs | 28 +-
e2e/src/telemetry/tracing/hive.rs | 50 +-
e2e/src/telemetry/tracing/otlp_attributes.rs | 12 +-
e2e/src/telemetry/tracing/otlp_basic.rs | 73 +-
e2e/src/telemetry/tracing/otlp_propagation.rs | 19 +-
e2e/src/telemetry/tracing/otlp_sampling.rs | 26 +-
e2e/src/testkit/mod.rs | 55 +-
e2e/src/testkit/otel.rs | 54 +-
lib/internal/src/background_tasks/mod.rs | 17 +-
package-lock.json | 2416 ++++++++++++++---
17 files changed, 2423 insertions(+), 688 deletions(-)
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index dab46ced5..57bd1073f 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -38,11 +38,14 @@ jobs:
- name: setup rust
uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
- name: test e2e
- run: cargo test_e2e
- timeout-minutes: 10
+ uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
env:
RUST_BACKTRACE: full
RUST_LOG: debug
+ with:
+ timeout_minutes: 10
+ max_attempts: 3
+ command: cargo test_e2e
plugin_examples:
name: test / plugin examples
diff --git a/Cargo.lock b/Cargo.lock
index 4761c81a0..860693372 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -12,6 +12,15 @@ dependencies = [
"regex",
]
+[[package]]
+name = "addr2line"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
+dependencies = [
+ "gimli",
+]
+
[[package]]
name = "adler2"
version = "2.0.1"
@@ -472,6 +481,21 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "backtrace"
+version = "0.3.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-link",
+]
+
[[package]]
name = "base16"
version = "0.2.1"
@@ -1404,6 +1428,28 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.116",
+ "unicode-xid",
+]
+
[[package]]
name = "diff"
version = "0.1.13"
@@ -2088,6 +2134,12 @@ dependencies = [
"wasip3",
]
+[[package]]
+name = "gimli"
+version = "0.32.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
+
[[package]]
name = "glob"
version = "0.3.3"
@@ -3536,12 +3588,13 @@ dependencies = [
[[package]]
name = "ntex"
-version = "3.4.0"
+version = "3.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36a4416847c6b284a2ce38952e0883cee3e20599b201d7b6a2d92afafb0fbc3e"
+checksum = "f4735978b410d8496a1d89bac416af3277f6d36e9ae56d1e3977b96b81ab8048"
dependencies = [
"base64",
"bitflags 2.11.0",
+ "derive_more",
"encoding_rs",
"env_logger",
"httparse",
@@ -3552,6 +3605,7 @@ dependencies = [
"ntex-bytes",
"ntex-codec",
"ntex-dispatcher",
+ "ntex-error",
"ntex-h2",
"ntex-http",
"ntex-io",
@@ -3577,9 +3631,9 @@ dependencies = [
[[package]]
name = "ntex-bytes"
-version = "1.5.0"
+version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "733600524409c09fdcbe17da91e832dd03f7690de727d844341ece58e867b3d6"
+checksum = "11f03e42e23f0ab33b86f4af78f19c39a0cc253b20c073e5e9b92408ba0e91ff"
dependencies = [
"bytes",
"serde",
@@ -3609,11 +3663,23 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "ntex-error"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ba4b4124a2b50218182d3420428ccf56ee95c4f28a15efba1be9e97cc271d9a"
+dependencies = [
+ "backtrace",
+ "foldhash 0.2.0",
+ "ntex-bytes",
+ "thiserror 2.0.18",
+]
+
[[package]]
name = "ntex-h2"
-version = "3.7.1"
+version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff063201f811d97557a5053dc75cbbd417adf4234e57ed8ec11fb05e3f2fdc3d"
+checksum = "2924795c85efb587dffe38b2d7f9295d8dfacc5cb6d00270dd96d7c34d0eb0eb"
dependencies = [
"bitflags 2.11.0",
"foldhash 0.2.0",
@@ -3622,6 +3688,7 @@ dependencies = [
"ntex-bytes",
"ntex-codec",
"ntex-dispatcher",
+ "ntex-error",
"ntex-http",
"ntex-io",
"ntex-net",
@@ -3634,9 +3701,9 @@ dependencies = [
[[package]]
name = "ntex-http"
-version = "1.0.0"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffb1f0643064d8ab21b70e0c9660b513ad3253744700c874668e6d7693ab3a39"
+checksum = "f7a02a55ca3286342030ba369d4176e280989e97958455963efce846e8abdca6"
dependencies = [
"foldhash 0.2.0",
"futures-core",
@@ -3650,9 +3717,9 @@ dependencies = [
[[package]]
name = "ntex-io"
-version = "3.9.0"
+version = "3.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de281bab9b3f6330a30b620e1b7cbca4bb92de4c4abe634c4f08c472faaa4e01"
+checksum = "e6c1d3d9a67a9abcad8981967bb1f3c59ec2c34e442771d16ed33acf663f0361"
dependencies = [
"bitflags 2.11.0",
"log",
@@ -3677,26 +3744,27 @@ dependencies = [
[[package]]
name = "ntex-macros"
-version = "3.1.0"
+version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c1d0933027d63b26d4e841e91a572a796824b522f5c077feba1ab2c7cbd3d78"
+checksum = "51138717dfe591b9b4063bf167ddcdc6fa8e3552157316f29f12c321493e3710"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.116",
]
[[package]]
name = "ntex-net"
-version = "3.7.0"
+version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28bdb6cd6694011070753d228ba76f7683a0dd78d30ea0d0354702296f5f78bf"
+checksum = "83c7c631404d704766913028124c60712457b1157923d85156aaf49fbd2551e9"
dependencies = [
"bitflags 2.11.0",
"cfg-if",
"libc",
"log",
"ntex-bytes",
+ "ntex-error",
"ntex-http",
"ntex-io",
"ntex-io-uring",
@@ -3740,9 +3808,9 @@ dependencies = [
[[package]]
name = "ntex-rt"
-version = "3.7.1"
+version = "3.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eacccab337bc61d01092d2a3fbf27f1c1c8696fbb691ffcd5e5d6c355d358511"
+checksum = "5a0f8d34b0f586e276ffdd34a4265db243672c6fe7e90b100e573319a7d3eddf"
dependencies = [
"async-channel",
"async-task",
@@ -3751,8 +3819,9 @@ dependencies = [
"foldhash 0.2.0",
"futures-timer",
"log",
+ "ntex-error",
"ntex-service",
- "oneshot",
+ "oneshot 0.2.1",
"scoped-tls",
"swap-buffer-queue",
"tokio",
@@ -3760,9 +3829,9 @@ dependencies = [
[[package]]
name = "ntex-server"
-version = "3.8.0"
+version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1235b8aaa4e01a6b54d30d37065b6f57c4ec82cdae72b51e56423d13e45292a"
+checksum = "85ac36e4b11c0cf0ae53fea574a1ab37e0c54331eb78f0b5a5e02cf6604a1f04"
dependencies = [
"async-channel",
"atomic-waker",
@@ -3775,7 +3844,7 @@ dependencies = [
"ntex-rt",
"ntex-service",
"ntex-util",
- "oneshot",
+ "oneshot 0.1.13",
"signal-hook",
"socket2",
"uuid",
@@ -3783,9 +3852,9 @@ dependencies = [
[[package]]
name = "ntex-service"
-version = "4.5.0"
+version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecc9eb453f1ce1e1561edd50892249fe8ce35e5740f347ff09e53c079ba7784e"
+checksum = "8f69442f89962c8c76a76f563c8d5ec0585fe645770d62a42e551f91ccc62278"
dependencies = [
"foldhash 0.2.0",
"log",
@@ -3794,12 +3863,13 @@ dependencies = [
[[package]]
name = "ntex-tls"
-version = "3.3.0"
+version = "3.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df010ac89eab9dc4ab3ec7507cea08788f964c476b6c0c50de5a88869ac7dc6b"
+checksum = "2e987231c62973660d743d599ddc26e82ed4f2f54050ce3e0f6d4bf8d3586106"
dependencies = [
"log",
"ntex-bytes",
+ "ntex-error",
"ntex-io",
"ntex-net",
"ntex-service",
@@ -3808,9 +3878,9 @@ dependencies = [
[[package]]
name = "ntex-util"
-version = "3.4.0"
+version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "092a46f60e64950809011a95d5ec6f634394b80d984bc44d8057b89a6a78c1f1"
+checksum = "1d799e658d04ad8be6d750b09a82fa01ad11dde264d9ec40fac873f835e87e85"
dependencies = [
"bitflags 2.11.0",
"foldhash 0.2.0",
@@ -3818,6 +3888,7 @@ dependencies = [
"futures-timer",
"log",
"ntex-bytes",
+ "ntex-error",
"ntex-rt",
"ntex-service",
"pin-project-lite",
@@ -3961,6 +4032,15 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+[[package]]
+name = "object"
+version = "0.37.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "octseq"
version = "0.5.2"
@@ -4002,6 +4082,12 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107"
+[[package]]
+name = "oneshot"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c"
+
[[package]]
name = "onig"
version = "6.5.1"
@@ -5419,6 +5505,12 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "rustc-demangle"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
+
[[package]]
name = "rustc-hash"
version = "2.1.1"
@@ -6031,7 +6123,7 @@ version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451"
dependencies = [
- "heck 0.4.1",
+ "heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.116",
diff --git a/Cargo.toml b/Cargo.toml
index 4b6edc678..7c6a3addd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -66,7 +66,7 @@ tokio = { version = "1.47.1", features = ["full"] }
tokio-util = { version = "0.7.16" }
rand = "0.10.0"
jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] }
-ntex = { version = "3.4.0", features = ["tokio"] }
+ntex = { version = "3.7.2", features = ["tokio"] }
tonic = { version = "0.14.2", features = ["tls-aws-lc"] }
reqwest = { version = "0.12.23", default-features = false, features = ["http2", "rustls-tls"] }
reqwest-retry = "0.8.0"
diff --git a/e2e/src/hive_cdn_supergraph.rs b/e2e/src/hive_cdn_supergraph.rs
index c56b366bb..7f8b67f5a 100644
--- a/e2e/src/hive_cdn_supergraph.rs
+++ b/e2e/src/hive_cdn_supergraph.rs
@@ -8,7 +8,9 @@ mod hive_cdn_supergraph_e2e_tests {
use sonic_rs::{JsonContainerTrait, JsonValueTrait};
use tokio::time::sleep;
- use crate::testkit::{ClientResponseExt, EnvVarsGuard, TestRouter, TestSubgraphs};
+ use crate::testkit::{
+ wait_until_mock_matched, ClientResponseExt, EnvVarsGuard, TestRouter, TestSubgraphs,
+ };
#[ntex::test]
async fn should_load_supergraph_from_endpoint() {
@@ -182,21 +184,12 @@ mod hive_cdn_supergraph_e2e_tests {
let host = server.host_with_port();
let mock1 = server
- .mock("GET", "/supergraph")
- .expect(1)
- .match_header("x-hive-cdn-key", "dummy_key")
- .with_status(200)
- .with_header("content-type", "text/plain")
- .with_body("type Query { dummy: String }")
- .create();
-
- let mock2 = server
.mock("GET", "/supergraph")
.expect_at_least(1)
.match_header("x-hive-cdn-key", "dummy_key")
.with_status(200)
.with_header("content-type", "text/plain")
- .with_body(include_str!("../supergraph.graphql"))
+ .with_body("type Query { dummy: String }")
.create();
let router = TestRouter::builder()
@@ -206,15 +199,13 @@ mod hive_cdn_supergraph_e2e_tests {
source: hive
endpoint: http://{host}/supergraph
key: dummy_key
- poll_interval: 800ms
+ poll_interval: 500ms
"#,
))
.build()
.start()
.await;
- mock1.assert();
-
let res = router
.send_graphql_request("{ __schema { types { name } } }", None, None)
.await;
@@ -233,9 +224,21 @@ mod hive_cdn_supergraph_e2e_tests {
.unwrap();
assert_eq!(types_arr.len(), 14);
- // Now wait for the schema to be reloaded and updated
- sleep(Duration::from_millis(900)).await;
- mock2.assert();
+ // Remove first mock and register the new supergraph mock
+ mock1.remove();
+ let mock2 = server
+ .mock("GET", "/supergraph")
+ .expect_at_least(1)
+ .match_header("x-hive-cdn-key", "dummy_key")
+ .with_status(200)
+ .with_header("content-type", "text/plain")
+ .with_body(include_str!("../supergraph.graphql"))
+ .create();
+
+ // Wait for the poller to pick up the new supergraph
+ wait_until_mock_matched(&mock2)
+ .await
+ .expect("Expected mock2 to be matched");
let res = router
.send_graphql_request("{ __schema { types { name } } }", None, None)
diff --git a/e2e/src/http.rs b/e2e/src/http.rs
index 069abc9a7..cce589075 100644
--- a/e2e/src/http.rs
+++ b/e2e/src/http.rs
@@ -7,26 +7,12 @@ mod http_tests {
use futures::{stream::FuturesUnordered, StreamExt};
use hive_router::pipeline::execution::EXPOSE_QUERY_PLAN_HEADER;
- use mockito::Mock;
use ntex::time;
use sonic_rs::JsonValueTrait;
- use crate::testkit::{some_header_map, ClientResponseExt, TestRouter, TestSubgraphs};
-
- async fn wait_until_mock_matched(mock: &Mock, timeout: Duration) -> Result<(), String> {
- let started = Instant::now();
- loop {
- if mock.matched_async().await {
- return Ok(());
- }
-
- time::sleep(Duration::from_millis(10)).await;
-
- if started.elapsed() > timeout {
- return Err(format!("timeout after {:?}", started.elapsed()));
- }
- }
- }
+ use crate::testkit::{
+ some_header_map, wait_until_mock_matched, ClientResponseExt, TestRouter, TestSubgraphs,
+ };
#[ntex::test]
async fn should_allow_to_customize_graphql_endpoint() {
@@ -511,7 +497,7 @@ mod http_tests {
.with_body(changed_supergraph)
.create();
- wait_until_mock_matched(&mock_changed, Duration::from_secs(2))
+ wait_until_mock_matched(&mock_changed)
.await
.expect("expected schema reload poll to fetch changed supergraph");
diff --git a/e2e/src/probes.rs b/e2e/src/probes.rs
index b3bc7bf62..a7749ff28 100644
--- a/e2e/src/probes.rs
+++ b/e2e/src/probes.rs
@@ -40,11 +40,15 @@ mod probes_e2e_tests {
// At the point, if supergraph is not loaded yet, health should be OK 200
let res = router.serv().post("/health").send().await.unwrap();
- assert!(res.status().is_success());
+ assert_eq!(res.status(), 200);
// And readiness should be 500 with server error
let res = router.serv().post("/readiness").send().await.unwrap();
- assert!(res.status().is_server_error());
+ assert!(
+ res.status().is_server_error(),
+ "Expected response status to be 5XX, but got {}",
+ res.status()
+ );
router.wait_for_ready(None).await;
diff --git a/e2e/src/supergraph.rs b/e2e/src/supergraph.rs
index 56e26ec76..16435f5c1 100644
--- a/e2e/src/supergraph.rs
+++ b/e2e/src/supergraph.rs
@@ -1,32 +1,22 @@
#[cfg(test)]
mod supergraph_e2e_tests {
- use std::time::{Duration, Instant};
+ use std::time::Duration;
use hive_router::invoke_shutdown_hooks;
- use mockito::Mock;
- use ntex::time;
use sonic_rs::JsonValueTrait;
- use crate::testkit::{ClientResponseExt, EnvVarsGuard, TestRouter, TestSubgraphs};
+ use crate::testkit::{wait_until_mock_matched, ClientResponseExt, TestRouter, TestSubgraphs};
#[ntex::test]
async fn should_clear_internal_caches_when_supergraph_changes() {
let mut server = mockito::Server::new_async().await;
let host = server.host_with_port();
let mock1 = server
- .mock("GET", "/supergraph")
- .expect(1)
- .with_status(200)
- .with_header("content-type", "text/plain")
- .with_body("type Query { dummy: String }")
- .create();
-
- let mock2 = server
.mock("GET", "/supergraph")
.expect_at_least(1)
.with_status(200)
.with_header("content-type", "text/plain")
- .with_body("type Query { dummyNew: NewType } type NewType { id: ID! }")
+ .with_body("type Query { dummy: String }")
.create();
let router = TestRouter::builder()
@@ -43,8 +33,6 @@ mod supergraph_e2e_tests {
.start()
.await;
- mock1.assert();
-
assert_eq!(router.schema_state().plan_cache.entry_count(), 0);
assert_eq!(router.schema_state().normalize_cache.entry_count(), 0);
@@ -63,13 +51,27 @@ mod supergraph_e2e_tests {
router.schema_state().plan_cache.run_pending_tasks().await;
invoke_shutdown_hooks(router.shared_state()).await;
+ ntex::time::sleep(Duration::from_millis(100)).await;
+
// Now it should have the record
assert_eq!(router.schema_state().plan_cache.entry_count(), 1);
assert_eq!(router.schema_state().normalize_cache.entry_count(), 1);
- // Now let's wait a bit and let the service re-load and get the new supergraph
- time::sleep(Duration::from_millis(600)).await;
- mock2.assert();
+ // Remove the first mock and register the new supergraph so the poller picks it up
+ mock1.remove();
+ let mock2 = server
+ .mock("GET", "/supergraph")
+ .expect_at_least(1)
+ .with_status(200)
+ .with_header("content-type", "text/plain")
+ .with_body("type Query { dummyNew: NewType } type NewType { id: ID! }")
+ .create();
+
+ // Wait for the poller to pick up the new supergraph
+ wait_until_mock_matched(&mock2)
+ .await
+ .expect("Expected mock2 to be matched");
+
router
.schema_state()
.normalize_cache
@@ -95,13 +97,12 @@ mod supergraph_e2e_tests {
/// 6. New request should use the new supergraph and new state, so running the same query should fail now with a validation error.
#[ntex::test]
async fn should_not_change_supergraph_for_in_flight_requests() {
- let _delay_guard = EnvVarsGuard::new()
- .set("SUBGRAPH_DELAY_MS", "500")
- .apply()
+ let subgraphs = TestSubgraphs::builder()
+ .with_delay(Duration::from_millis(500))
+ .build()
+ .start()
.await;
- let subgraphs = TestSubgraphs::builder().build().start().await;
-
let mut server = mockito::Server::new_async().await;
let host = server.host_with_port();
@@ -109,14 +110,44 @@ mod supergraph_e2e_tests {
let supergraph1_sdl = subgraphs.supergraph(include_str!("../supergraph.graphql"));
let mock1 = server
.mock("GET", "/supergraph")
- .expect(1)
+ .expect_at_least(1)
.with_status(200)
.with_header("content-type", "text/plain")
.with_header("etag", "1")
.with_body(supergraph1_sdl)
.create();
- // Second supergraph
+ let router = TestRouter::builder()
+ .inline_config(format!(
+ r#"
+ supergraph:
+ source: hive
+ endpoint: http://{host}/supergraph
+ key: dummy_key
+ poll_interval: 100ms
+ "#,
+ ))
+ .build()
+ .start()
+ .await;
+
+ mock1.assert();
+
+ let res = router
+ .send_graphql_request("{ users { id name reviews { id body } } }", None, None)
+ .await;
+
+ assert!(res.status().is_success(), "Expected 200 OK");
+
+ let body_json = res.json_body().await;
+
+ assert!(body_json["data"].is_object());
+ assert!(body_json["errors"].is_null());
+
+ mock1.remove();
+
+ // Second supergraph - only registered after the first request completes so the poller
+ // cannot swap the schema while the first request is in flight
let supergraph2_sdl = subgraphs.supergraph(
r#"schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@@ -204,34 +235,9 @@ mod supergraph_e2e_tests {
.with_body(supergraph2_sdl)
.create();
- let router = TestRouter::builder()
- .inline_config(format!(
- r#"
- supergraph:
- source: hive
- endpoint: http://{host}/supergraph
- key: dummy_key
- poll_interval: 300ms
- "#,
- ))
- .build()
- .start()
- .await;
-
- mock1.assert();
-
- let res = router
- .send_graphql_request("{ users { id name reviews { id body } } }", None, None)
- .await;
-
- assert!(res.status().is_success(), "Expected 200 OK");
-
- let body_json = res.json_body().await;
-
- assert!(body_json["data"].is_object());
- assert!(body_json["errors"].is_null());
-
- mock2.assert();
+ wait_until_mock_matched(&mock2)
+ .await
+ .expect("Expected mock2 to be matched");
let res_new_supergraph = router
.send_graphql_request("{ users { id name reviews { id body } } }", None, None)
@@ -264,7 +270,7 @@ mod supergraph_e2e_tests {
source: hive
endpoint: http://{host}/supergraph
key: dummy_key
- poll_interval: 50ms
+ poll_interval: 200ms
"#,
))
.build()
@@ -304,13 +310,10 @@ mod supergraph_e2e_tests {
.with_status(404)
.create();
- wait_until_mock_matched(&mock_404, Duration::from_millis(500))
+ wait_until_mock_matched(&mock_404)
.await
.expect("Expected to match 404 mock");
- // Give it some time to process it
- time::sleep(Duration::from_millis(50)).await;
-
// Router should still be using the initial supergraph
let res = router
.send_graphql_request(
@@ -344,12 +347,10 @@ mod supergraph_e2e_tests {
.with_body("type Query { updated: String }")
.create();
- wait_until_mock_matched(&mock_final, Duration::from_millis(120))
+ wait_until_mock_matched(&mock_final)
.await
.expect("Expected to match final mock");
mock_final.assert();
- // Give it some time to process it
- time::sleep(Duration::from_millis(200)).await;
// Check if final supergraph is working
let res = router
@@ -374,18 +375,4 @@ mod supergraph_e2e_tests {
}
"#);
}
-
- async fn wait_until_mock_matched(mock: &Mock, timeout: Duration) -> Result<(), String> {
- let now = Instant::now();
- loop {
- if mock.matched_async().await {
- return Ok(());
- }
- time::sleep(Duration::from_millis(10)).await;
-
- if now.elapsed() > timeout {
- return Err(format!("timeout after {:?}", now.elapsed()));
- }
- }
- }
}
diff --git a/e2e/src/telemetry/metrics.rs b/e2e/src/telemetry/metrics.rs
index 31311d19b..ea03997ca 100644
--- a/e2e/src/telemetry/metrics.rs
+++ b/e2e/src/telemetry/metrics.rs
@@ -14,7 +14,7 @@ use hive_router::{
use hive_router_internal::telemetry::metrics::catalog::{labels, labels_for, names, values};
async fn wait_for_metrics_export() {
- tokio::time::sleep(Duration::from_millis(100)).await;
+ tokio::time::sleep(Duration::from_millis(500)).await;
}
fn assert_counter_eq(
@@ -90,7 +90,7 @@ async fn test_otlp_http_metrics_export_with_graphql_request() {
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
supergraph_path.to_str().unwrap(),
otlp_endpoint
@@ -163,7 +163,7 @@ async fn test_otlp_cache_size_metrics_exported_as_gauges() {
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
supergraph_path.to_str().unwrap(),
otlp_endpoint
@@ -233,7 +233,7 @@ async fn test_otlp_http_server_semconv_metrics_for_graphql_handler() {
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
supergraph_path.to_str().unwrap(),
otlp_endpoint
@@ -304,7 +304,7 @@ async fn test_otlp_http_client_semconv_metrics_for_subgraph_request() {
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
temporality: cumulative
"#,
supergraph_path.to_str().unwrap(),
@@ -370,7 +370,7 @@ async fn test_otlp_all_metrics_path_attribute_names() {
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
supergraph_path.to_str().unwrap(),
otlp_endpoint
@@ -467,7 +467,7 @@ async fn test_otlp_all_metrics_happy_path_attribute_names() {
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
supergraph_path.to_str().unwrap(),
otlp_endpoint
@@ -567,7 +567,7 @@ async fn test_otlp_metric_can_be_disabled_via_instrumentation_config() {
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
instrumentation:
instruments:
http.server.request.duration: false
@@ -628,7 +628,7 @@ async fn test_otlp_metric_attribute_can_be_opted_out() {
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
instrumentation:
instruments:
http.server.request.duration:
@@ -700,7 +700,7 @@ async fn test_otlp_metric_attribute_true_override_is_noop() {
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
instrumentation:
instruments:
http.server.request.duration:
@@ -771,7 +771,7 @@ async fn test_otlp_graphql_errors_total_for_parsing_error() {
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
supergraph_path.to_str().unwrap(),
otlp_endpoint
@@ -861,7 +861,7 @@ async fn test_otlp_graphql_errors_total_uses_post_plugin_error_codes() {
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
plugins:
test_graphql_error_mapping:
@@ -943,7 +943,7 @@ async fn test_otlp_http_server_bad_request_sets_graphql_status_and_error_type()
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
supergraph_path.to_str().unwrap(),
otlp_endpoint
@@ -1005,7 +1005,7 @@ async fn test_otlp_http_client_transport_failure_sets_graphql_status_and_error_t
endpoint: {}
protocol: http
interval: 30ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
supergraph_path.to_str().unwrap(),
otlp_endpoint
diff --git a/e2e/src/telemetry/tracing/hive.rs b/e2e/src/telemetry/tracing/hive.rs
index cb715cedb..ddeac0ad1 100644
--- a/e2e/src/telemetry/tracing/hive.rs
+++ b/e2e/src/telemetry/tracing/hive.rs
@@ -34,7 +34,7 @@ async fn test_hive_http_export() {
enabled: true
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
usage_reporting:
enabled: false
"#,
@@ -72,28 +72,48 @@ async fn test_hive_http_export() {
assert_eq!(authorization_header, Some(format!("Bearer {}", token)));
assert_eq!(target_ref_header, Some(target));
- let all_traces = otlp_collector.traces().await;
- let trace = all_traces.first().expect("Failed to get first trace");
-
// Hive Console requires to drop the http.server span
// and make the graphql.operation the root span.
+ // Wait for all expected spans first, then snapshot traces for the http.server absence check.
+ let operation_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.operation")
+ .await;
+ let parse_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.parse")
+ .await;
+ let validate_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.validate")
+ .await;
+ let variable_coercion_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.variable_coercion")
+ .await;
+ let normalization_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.normalize")
+ .await;
+ let plan_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.plan")
+ .await;
+ let execution_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.execute")
+ .await;
+ let subgraph_operation_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.subgraph.operation")
+ .await;
+ let http_inflight_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("http.inflight")
+ .await;
+ let http_client_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("http.client")
+ .await;
+
+ let all_traces = otlp_collector.traces().await;
+ let trace = all_traces.first().expect("Failed to get first trace");
assert_eq!(
trace.has_span_by_hive_kind("http.server"),
false,
"Unexpected http.server spans"
);
- let operation_span = trace.span_by_hive_kind_one("graphql.operation");
- let parse_span = trace.span_by_hive_kind_one("graphql.parse");
- let validate_span = trace.span_by_hive_kind_one("graphql.validate");
- let variable_coercion_span = trace.span_by_hive_kind_one("graphql.variable_coercion");
- let normalization_span = trace.span_by_hive_kind_one("graphql.normalize");
- let plan_span = trace.span_by_hive_kind_one("graphql.plan");
- let execution_span = trace.span_by_hive_kind_one("graphql.execute");
- let subgraph_operation_span = trace.span_by_hive_kind_one("graphql.subgraph.operation");
- let http_inflight_span = trace.span_by_hive_kind_one("http.inflight");
- let http_client_span = trace.span_by_hive_kind_one("http.client");
-
insta::assert_snapshot!(
operation_span,
@r"
diff --git a/e2e/src/telemetry/tracing/otlp_attributes.rs b/e2e/src/telemetry/tracing/otlp_attributes.rs
index f519e3a54..74aac22fc 100644
--- a/e2e/src/telemetry/tracing/otlp_attributes.rs
+++ b/e2e/src/telemetry/tracing/otlp_attributes.rs
@@ -35,7 +35,7 @@ async fn test_deprecated_span_attributes() {
protocol: grpc
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -165,7 +165,7 @@ async fn test_spec_and_deprecated_span_attributes() {
protocol: grpc
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -318,7 +318,7 @@ async fn test_default_client_identification() {
custom-header: custom-value
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -398,7 +398,7 @@ async fn test_custom_client_identification() {
custom-header: custom-value
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -471,7 +471,7 @@ async fn test_default_resource_attributes() {
protocol: http
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -542,7 +542,7 @@ async fn test_custom_resource_attributes() {
protocol: http
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
diff --git a/e2e/src/telemetry/tracing/otlp_basic.rs b/e2e/src/telemetry/tracing/otlp_basic.rs
index cc1931837..f936e3d69 100644
--- a/e2e/src/telemetry/tracing/otlp_basic.rs
+++ b/e2e/src/telemetry/tracing/otlp_basic.rs
@@ -35,7 +35,7 @@ async fn test_otlp_http_export_with_graphql_request() {
protocol: http
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -50,20 +50,39 @@ async fn test_otlp_http_export_with_graphql_request() {
assert!(res.status().is_success());
// Wait for exports to be sent
- let all_traces = otlp_collector.wait_for_traces_count(1).await;
- let trace = all_traces.first().unwrap();
-
- let http_server_span = trace.span_by_hive_kind_one("http.server");
- let operation_span = trace.span_by_hive_kind_one("graphql.operation");
- let parse_span = trace.span_by_hive_kind_one("graphql.parse");
- let validate_span = trace.span_by_hive_kind_one("graphql.validate");
- let variable_coercion_span = trace.span_by_hive_kind_one("graphql.variable_coercion");
- let normalization_span = trace.span_by_hive_kind_one("graphql.normalize");
- let plan_span = trace.span_by_hive_kind_one("graphql.plan");
- let execution_span = trace.span_by_hive_kind_one("graphql.execute");
- let subgraph_operation_span = trace.span_by_hive_kind_one("graphql.subgraph.operation");
- let http_inflight_span = trace.span_by_hive_kind_one("http.inflight");
- let http_client_span = trace.span_by_hive_kind_one("http.client");
+ let http_server_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("http.server")
+ .await;
+ let operation_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.operation")
+ .await;
+ let parse_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.parse")
+ .await;
+ let validate_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.validate")
+ .await;
+ let variable_coercion_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.variable_coercion")
+ .await;
+ let normalization_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.normalize")
+ .await;
+ let plan_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.plan")
+ .await;
+ let execution_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.execute")
+ .await;
+ let subgraph_operation_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("graphql.subgraph.operation")
+ .await;
+ let http_inflight_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("http.inflight")
+ .await;
+ let http_client_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("http.client")
+ .await;
insta::assert_snapshot!(
http_server_span,
@@ -273,7 +292,7 @@ async fn test_otlp_grpc_export_with_graphql_request() {
protocol: grpc
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -531,14 +550,14 @@ async fn test_otlp_disabled() {
enabled: false
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
- kind: otlp
endpoint: {otlp_http_endpoint}
enabled: false
protocol: http
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -594,7 +613,7 @@ async fn test_otlp_http_headers() {
custom-header: custom-value
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -658,7 +677,7 @@ async fn test_otlp_grpc_metadata() {
custom-header: custom-value
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -720,7 +739,7 @@ async fn test_otlp_cache_hits() {
protocol: http
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -734,7 +753,9 @@ async fn test_otlp_cache_hits() {
assert!(res.status().is_success());
// Wait for exports to be sent
- otlp_collector.wait_for_traces_count(1).await;
+ otlp_collector
+ .wait_for_traces_with_span(1, "graphql.validate")
+ .await;
// Should hit the caches
let res = router
@@ -742,8 +763,10 @@ async fn test_otlp_cache_hits() {
.await;
assert!(res.status().is_success());
- // Wait for exports to be sent
- let all_traces = otlp_collector.wait_for_traces_count(2).await;
+ // Wait for both traces to have all expected spans
+ let all_traces = otlp_collector
+ .wait_for_traces_with_span(2, "graphql.validate")
+ .await;
let first_trace = all_traces.first().unwrap();
let second_trace = all_traces.get(1).unwrap();
@@ -815,7 +838,7 @@ async fn test_otlp_no_trace_id_collision() {
protocol: http
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
diff --git a/e2e/src/telemetry/tracing/otlp_propagation.rs b/e2e/src/telemetry/tracing/otlp_propagation.rs
index 4d06dcdd4..2a3f9aa76 100644
--- a/e2e/src/telemetry/tracing/otlp_propagation.rs
+++ b/e2e/src/telemetry/tracing/otlp_propagation.rs
@@ -33,7 +33,7 @@ async fn test_otlp_http_trace_context_propagation() {
protocol: http
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -147,7 +147,7 @@ async fn test_otlp_http_baggage_propagation() {
protocol: http
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -239,7 +239,7 @@ async fn test_otlp_http_b3_propagation() {
protocol: http
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -359,7 +359,7 @@ async fn test_otlp_http_jaeger_propagation() {
protocol: http
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -381,11 +381,12 @@ async fn test_otlp_http_jaeger_propagation() {
assert!(res.status().is_success());
// Wait for exports to be sent
- let all_traces = otlp_collector.wait_for_traces_count(1).await;
- let trace = all_traces.first().unwrap();
-
- let http_server_span = trace.span_by_hive_kind_one("http.server");
- let http_client_span = trace.span_by_hive_kind_one("http.client");
+ let http_server_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("http.server")
+ .await;
+ let http_client_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("http.client")
+ .await;
// Verify that http.server has corrent parent span,
// the one from upstream traceparent
diff --git a/e2e/src/telemetry/tracing/otlp_sampling.rs b/e2e/src/telemetry/tracing/otlp_sampling.rs
index a087030fb..245117de6 100644
--- a/e2e/src/telemetry/tracing/otlp_sampling.rs
+++ b/e2e/src/telemetry/tracing/otlp_sampling.rs
@@ -38,7 +38,7 @@ async fn test_otlp_parent_based_sampler() {
protocol: http
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
@@ -95,24 +95,10 @@ async fn test_otlp_parent_based_sampler() {
assert!(res.status().is_success());
- // Verify trace was collected
- let all_traces = otlp_collector.wait_for_traces_count(1).await;
- let trace = all_traces.first().unwrap();
-
- assert_eq!(
- trace.id, upstream_trace_id,
- "Trace should have correct trace_id"
- );
-
- // Verify we have spans in the trace
- let spans = &trace.spans;
- assert!(
- !spans.is_empty(),
- "Trace should contain spans when parent is sampled"
- );
-
- // Find http.server span by hive.kind attribute
- let http_server_span = trace.span_by_hive_kind_one("http.server");
+ // Verify trace was collected, and find the http.server span
+ let http_server_span = otlp_collector
+ .wait_for_span_by_hive_kind_one("http.server")
+ .await;
assert_eq!(
http_server_span.trace_id, upstream_trace_id,
@@ -151,7 +137,7 @@ async fn test_otlp_zero_sample_rate() {
protocol: http
batch_processor:
scheduled_delay: 50ms
- max_export_timeout: 50ms
+ max_export_timeout: 2s
"#,
))
.with_subgraphs(&subgraphs)
diff --git a/e2e/src/testkit/mod.rs b/e2e/src/testkit/mod.rs
index 43b33d15e..d74727052 100644
--- a/e2e/src/testkit/mod.rs
+++ b/e2e/src/testkit/mod.rs
@@ -5,6 +5,7 @@ use bytes::Bytes;
use dashmap::DashMap;
use hive_router_plan_executor::plugin_trait::RouterPlugin;
use lazy_static::lazy_static;
+use mockito::Mock;
use ntex::{
client::ClientResponse,
web::{self, test},
@@ -12,13 +13,19 @@ use ntex::{
use reqwest::header::{ACCEPT, CONTENT_TYPE};
use sonic_rs::json;
use std::{
- any::Any, future::Future, marker::PhantomData, net::SocketAddr, path::PathBuf, sync::Arc,
- time::Duration,
+ any::Any,
+ future::Future,
+ marker::PhantomData,
+ net::SocketAddr,
+ path::PathBuf,
+ sync::Arc,
+ time::{Duration, Instant},
};
use tempfile::{NamedTempFile, TempPath};
use tokio::{
net::TcpListener,
sync::{oneshot, Semaphore},
+ time,
};
use tracing::{info, warn};
@@ -234,11 +241,15 @@ type OnRequest = dyn Fn(RequestLike) -> Option + Send + Sync;
pub struct TestSubgraphsBuilder {
on_request: Option>,
+ delay: Option,
}
impl TestSubgraphsBuilder {
pub fn new() -> Self {
- Self { on_request: None }
+ Self {
+ on_request: None,
+ delay: None,
+ }
}
#[allow(unused)]
@@ -250,9 +261,20 @@ impl TestSubgraphsBuilder {
self
}
+ /// Adds a cooperative async delay to every subgraph request.
+ /// Unlike `with_on_request` with `std::thread::sleep`, this yields
+ /// back to the tokio runtime, allowing other tasks (like schema
+ /// pollers) to make progress during the delay.
+ #[allow(unused)]
+ pub fn with_delay(mut self, delay: Duration) -> Self {
+ self.delay = Some(delay);
+ self
+ }
+
pub fn build(self) -> TestSubgraphs {
TestSubgraphs {
on_request: self.on_request,
+ delay: self.delay,
handle: None,
_state: PhantomData,
}
@@ -273,6 +295,7 @@ struct TestSubgraphsHandle {
pub struct TestSubgraphs {
on_request: Option>,
+ delay: Option,
handle: Option,
_state: PhantomData,
}
@@ -369,6 +392,14 @@ impl TestSubgraphs {
handle_on_request,
));
}
+ if let Some(delay) = self.delay {
+ app = app.layer(axum::middleware::from_fn(
+ move |req, next: axum::middleware::Next| async move {
+ tokio::time::sleep(delay).await;
+ next.run(req).await
+ },
+ ));
+ }
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
tokio::spawn(async move {
@@ -382,6 +413,7 @@ impl TestSubgraphs {
TestSubgraphs {
on_request: self.on_request,
+ delay: self.delay,
handle: Some(TestSubgraphsHandle {
shutdown_tx: Some(shutdown_tx),
addr,
@@ -838,3 +870,20 @@ impl ClientResponseExt for ClientResponse {
.expect("failed to pretty print JSON body")
}
}
+
+pub async fn wait_until_mock_matched(mock: &Mock) -> Result<(), String> {
+ let now = Instant::now();
+ let timeout = Duration::from_secs(5); // always a sane default
+ loop {
+ if mock.matched_async().await {
+ return Ok(());
+ }
+
+ // anything less will congest the router, keep the interval chill
+ time::sleep(Duration::from_millis(100)).await;
+
+ if now.elapsed() > timeout {
+ return Err(format!("timeout after {:?}", now.elapsed()));
+ }
+ }
+}
diff --git a/e2e/src/testkit/otel.rs b/e2e/src/testkit/otel.rs
index 644e2fd9f..9903519cb 100644
--- a/e2e/src/testkit/otel.rs
+++ b/e2e/src/testkit/otel.rs
@@ -2,6 +2,7 @@ use hive_router_internal::telemetry::traces::spans::attributes::HIVE_KIND;
use opentelemetry_proto::tonic::resource::v1::Resource;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt::Display;
+use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{oneshot, Mutex};
@@ -93,19 +94,28 @@ impl<'a> TraceParent<'a> {
}
pub fn random_trace_id() -> String {
- let random: u128 = std::time::SystemTime::now()
+ // combine a monotonic counter with wall clock nanos to guarantee
+ // uniqueness even when multiple calls land in the same nanosecond
+ static COUNTER: AtomicU64 = AtomicU64::new(0);
+ let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
+ let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
- .as_nanos();
- format!("{:032x}", random)
+ .as_nanos() as u64;
+ let hi = nanos as u128;
+ let lo = seq as u128;
+ format!("{:016x}{:016x}", hi, lo)
}
pub fn random_span_id() -> String {
- let random: u64 = std::time::SystemTime::now()
+ // same counter trick; span ids only need 8 bytes so xor the two halves
+ static COUNTER: AtomicU64 = AtomicU64::new(0);
+ let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
+ let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
- .as_micros() as u64;
- format!("{:016x}", random)
+ .as_nanos() as u64;
+ format!("{:016x}", nanos ^ seq.wrapping_add(1))
}
}
@@ -704,6 +714,38 @@ impl OtlpCollector {
.expect("waiting for traces with count timed out")
}
+ /// Waits for at least `count` traces where each has a span with the given hive.kind.
+ /// This is more robust than `wait_for_traces_count` because spans may arrive in
+ /// separate batches after the trace ID is first seen.
+ pub async fn wait_for_traces_with_span(
+ &self,
+ count: usize,
+ hive_kind: &str,
+ ) -> Vec {
+ tokio::time::timeout(Duration::from_secs(5), async {
+ loop {
+ let traces = self.traces().await;
+ let matching = traces
+ .iter()
+ .filter(|t| t.has_span_by_hive_kind(hive_kind))
+ .count();
+
+ if matching >= count {
+ return traces;
+ }
+
+ tokio::time::sleep(Duration::from_millis(100)).await;
+ }
+ })
+ .await
+ .unwrap_or_else(|_| {
+ panic!(
+ "waiting for {} traces with hive.kind={} timed out",
+ count, hive_kind
+ )
+ })
+ }
+
pub async fn wait_for_span_by_hive_kind_one(&self, hive_kind: &str) -> CollectedSpan {
tokio::time::timeout(Duration::from_secs(5), async {
loop {
diff --git a/lib/internal/src/background_tasks/mod.rs b/lib/internal/src/background_tasks/mod.rs
index 00ed97c0f..d6d0907e1 100644
--- a/lib/internal/src/background_tasks/mod.rs
+++ b/lib/internal/src/background_tasks/mod.rs
@@ -1,5 +1,5 @@
use async_trait::async_trait;
-use ntex::rt::Arbiter;
+use ntex::rt::{spawn, JoinHandle};
use std::future::Future;
pub use tokio_util::sync::CancellationToken;
use tracing::info;
@@ -12,7 +12,7 @@ pub trait BackgroundTask: Send + Sync {
pub struct BackgroundTasksManager {
cancellation_token: CancellationToken,
- arbiter: Arbiter,
+ handles: Vec>,
}
impl Default for BackgroundTasksManager {
@@ -25,7 +25,7 @@ impl BackgroundTasksManager {
pub fn new() -> Self {
Self {
cancellation_token: CancellationToken::new(),
- arbiter: Arbiter::new(),
+ handles: Vec::new(),
}
}
@@ -35,22 +35,25 @@ impl BackgroundTasksManager {
{
info!("registering background task: {}", task.id());
let child_token = self.cancellation_token.clone();
- self.arbiter.spawn(async move {
+ let handle = spawn(async move {
task.run(child_token).await;
});
+ self.handles.push(handle);
}
pub fn register_handle(&mut self, f: F)
where
F: Future + Send + 'static,
{
- self.arbiter.spawn(f);
+ self.handles.push(spawn(f));
}
- pub fn shutdown(&self) {
+ pub fn shutdown(&mut self) {
info!("shutdown triggered, stopping all background tasks...");
self.cancellation_token.cancel();
- self.arbiter.stop();
+ for handle in self.handles.drain(..) {
+ handle.cancel();
+ }
info!("all background tasks have been shut down gracefully.");
}
}
diff --git a/package-lock.json b/package-lock.json
index ce36ec4d5..a2b3cbd75 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,18 +27,14 @@
"typescript": "5.9.3"
}
},
- "audits/node_modules/graphql-http": {
- "version": "1.22.4",
+ "audits/node_modules/graphql": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz",
+ "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==",
"dev": true,
"license": "MIT",
- "workspaces": [
- "implementations/**/*"
- ],
"engines": {
- "node": ">=12"
- },
- "peerDependencies": {
- "graphql": ">=0.11 <=16"
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
},
"bench": {
@@ -54,97 +50,133 @@
"jsonschema2mk": "2.1.1"
}
},
- "docs/generator/node_modules/explain-json-schema": {
- "version": "1.1.1",
+ "lib/node-addon": {
+ "name": "@graphql-hive/router-query-planner",
+ "version": "0.0.17",
"license": "MIT",
- "dependencies": {
- "minimist": "^1.2.5"
+ "devDependencies": {
+ "@napi-rs/cli": "3.5.1",
+ "@types/node": "25.5.0"
},
- "bin": {
- "explain-json-schema": "cli.js"
+ "engines": {
+ "bun": "^1",
+ "node": ">=20"
}
},
- "docs/generator/node_modules/handlebars": {
- "version": "4.7.8",
+ "node_modules/@apollo/cache-control-types": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz",
+ "integrity": "sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g==",
+ "dev": true,
"license": "MIT",
+ "peerDependencies": {
+ "graphql": "14.x || 15.x || 16.x"
+ }
+ },
+ "node_modules/@apollo/composition": {
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/@apollo/composition/-/composition-2.13.3.tgz",
+ "integrity": "sha512-0/vr6vbk1BynEKY6/UV0SZUHBV33p5Ok0y6RhkNnX5BA+ysyOkcyTLPlEHo+ce62nqWx4ml3iOxeKIEHqbKelQ==",
+ "dev": true,
+ "license": "Elastic-2.0",
"dependencies": {
- "minimist": "^1.2.5",
- "neo-async": "^2.6.2",
- "source-map": "^0.6.1",
- "wordwrap": "^1.0.0"
- },
- "bin": {
- "handlebars": "bin/handlebars"
+ "@apollo/federation-internals": "2.13.3",
+ "@apollo/query-graphs": "2.13.3"
},
"engines": {
- "node": ">=0.4.7"
+ "node": ">=18"
},
- "optionalDependencies": {
- "uglify-js": "^3.1.4"
+ "peerDependencies": {
+ "graphql": "^16.5.0"
}
},
- "docs/generator/node_modules/jsonschema2mk": {
- "version": "2.1.1",
- "license": "MIT",
+ "node_modules/@apollo/federation-internals": {
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/@apollo/federation-internals/-/federation-internals-2.13.3.tgz",
+ "integrity": "sha512-4zHgqznZza5Qx2CZy1qlrh83BB/3Yd8BqD/dmhGzLCvHJQ1LBTyZbWN7xwKs5z5FbxdSPkL5TvPoLm5Rek8/4g==",
+ "dev": true,
+ "license": "Elastic-2.0",
"dependencies": {
- "explain-json-schema": "^1.1.1",
- "handlebars": "^4.7.7",
- "js-yaml": "^4.1.0",
- "minimist": "^1.2.6"
+ "@types/uuid": "^9.0.0",
+ "chalk": "^4.1.0",
+ "js-levenshtein": "^1.1.6",
+ "uuid": "^9.0.0"
},
- "bin": {
- "jsonschema2mk": "cli.js"
- }
- },
- "docs/generator/node_modules/neo-async": {
- "version": "2.6.2",
- "license": "MIT"
- },
- "docs/generator/node_modules/source-map": {
- "version": "0.6.1",
- "license": "BSD-3-Clause",
"engines": {
- "node": ">=0.10.0"
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "graphql": "^16.5.0"
}
},
- "docs/generator/node_modules/uglify-js": {
- "version": "3.19.3",
- "license": "BSD-2-Clause",
- "optional": true,
+ "node_modules/@apollo/federation-internals/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
"bin": {
- "uglifyjs": "bin/uglifyjs"
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/@apollo/query-graphs": {
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/@apollo/query-graphs/-/query-graphs-2.13.3.tgz",
+ "integrity": "sha512-X/bgsIVmamAzd8IFMDo9upO5Oi/uhAZlcBmna6yEFlN0fzdQZMHL5pp1fyzRG1wxCFi3lVjDXw2dc/380uWB9g==",
+ "dev": true,
+ "license": "Elastic-2.0",
+ "dependencies": {
+ "@apollo/federation-internals": "2.13.3",
+ "deep-equal": "^2.0.5",
+ "ts-graphviz": "^1.5.4",
+ "uuid": "^9.0.0"
},
"engines": {
- "node": ">=0.8.0"
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "graphql": "^16.5.0"
}
},
- "docs/generator/node_modules/wordwrap": {
- "version": "1.0.0",
- "license": "MIT"
- },
- "lib/node-addon": {
- "name": "@graphql-hive/router-query-planner",
- "version": "0.0.17",
+ "node_modules/@apollo/query-graphs/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
"license": "MIT",
- "devDependencies": {
- "@napi-rs/cli": "3.5.1",
- "@types/node": "25.5.0"
- },
- "engines": {
- "bun": "^1",
- "node": ">=20"
+ "bin": {
+ "uuid": "dist/bin/uuid"
}
},
- "node_modules/@apollo/cache-control-types": {
- "version": "1.0.3",
+ "node_modules/@apollo/subgraph": {
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/@apollo/subgraph/-/subgraph-2.13.3.tgz",
+ "integrity": "sha512-nmdACpdTe+kgmNF0Yow3MoDkE0SMfvK6tTsEs3h3M9GLYCgBWZWBoDWQfNdr7kWRW6aan2SylU0K0ktF643h3g==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "@apollo/cache-control-types": "^1.0.2",
+ "@apollo/federation-internals": "2.13.3"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
"peerDependencies": {
- "graphql": "14.x || 15.x || 16.x"
+ "graphql": "^16.5.0"
}
},
"node_modules/@babel/runtime": {
- "version": "7.28.4",
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -153,6 +185,8 @@
},
"node_modules/@envelop/core": {
"version": "5.5.1",
+ "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.5.1.tgz",
+ "integrity": "sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -167,6 +201,8 @@
},
"node_modules/@envelop/instrumentation": {
"version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@envelop/instrumentation/-/instrumentation-1.0.0.tgz",
+ "integrity": "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -179,6 +215,8 @@
},
"node_modules/@envelop/types": {
"version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/@envelop/types/-/types-5.2.1.tgz",
+ "integrity": "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -191,11 +229,15 @@
},
"node_modules/@fastify/busboy": {
"version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz",
+ "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==",
"dev": true,
"license": "MIT"
},
"node_modules/@floating-ui/core": {
"version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -204,6 +246,8 @@
},
"node_modules/@floating-ui/dom": {
"version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -213,6 +257,8 @@
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
+ "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -225,6 +271,8 @@
},
"node_modules/@floating-ui/utils": {
"version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"dev": true,
"license": "MIT"
},
@@ -254,238 +302,112 @@
"graphql": "*"
}
},
- "node_modules/@graphql-hive/federation-gateway-audit/node_modules/@apollo/composition": {
- "version": "2.13.3",
- "resolved": "https://registry.npmjs.org/@apollo/composition/-/composition-2.13.3.tgz",
- "integrity": "sha512-0/vr6vbk1BynEKY6/UV0SZUHBV33p5Ok0y6RhkNnX5BA+ysyOkcyTLPlEHo+ce62nqWx4ml3iOxeKIEHqbKelQ==",
+ "node_modules/@graphql-hive/laboratory": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@graphql-hive/laboratory/-/laboratory-0.1.2.tgz",
+ "integrity": "sha512-Memzux8zIWGzyY7FPHTmf6GUrnL+AixpWYOCJg5AnSfWCjakM7rc+yB4RHgcm04Gbcqy7C3Zb+CcPn7YOgNNJA==",
"dev": true,
- "license": "Elastic-2.0",
+ "license": "MIT",
"dependencies": {
- "@apollo/federation-internals": "2.13.3",
- "@apollo/query-graphs": "2.13.3"
- },
- "engines": {
- "node": ">=18"
+ "radix-ui": "^1.4.3",
+ "uuid": "^13.0.0"
},
"peerDependencies": {
- "graphql": "^16.5.0"
+ "@tanstack/react-form": "^1.23.8",
+ "date-fns": "^4.1.0",
+ "graphql-ws": "^6.0.6",
+ "lucide-react": "^0.548.0",
+ "lz-string": "^1.5.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0",
+ "tslib": "^2.8.1",
+ "zod": "^4.1.12"
}
},
- "node_modules/@graphql-hive/federation-gateway-audit/node_modules/@apollo/federation-internals": {
- "version": "2.13.3",
- "resolved": "https://registry.npmjs.org/@apollo/federation-internals/-/federation-internals-2.13.3.tgz",
- "integrity": "sha512-4zHgqznZza5Qx2CZy1qlrh83BB/3Yd8BqD/dmhGzLCvHJQ1LBTyZbWN7xwKs5z5FbxdSPkL5TvPoLm5Rek8/4g==",
+ "node_modules/@graphql-hive/router-query-planner": {
+ "resolved": "lib/node-addon",
+ "link": true
+ },
+ "node_modules/@graphql-tools/executor": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.5.1.tgz",
+ "integrity": "sha512-n94Qcu875Mji9GQ52n5UbgOTxlgvFJicBPYD+FRks9HKIQpdNPjkkrKZUYNG51XKa+bf03rxNflm4+wXhoHHrA==",
"dev": true,
- "license": "Elastic-2.0",
+ "license": "MIT",
"dependencies": {
- "@types/uuid": "^9.0.0",
- "chalk": "^4.1.0",
- "js-levenshtein": "^1.1.6",
- "uuid": "^9.0.0"
+ "@graphql-tools/utils": "^11.0.0",
+ "@graphql-typed-document-node/core": "^3.2.0",
+ "@repeaterjs/repeater": "^3.0.4",
+ "@whatwg-node/disposablestack": "^0.0.6",
+ "@whatwg-node/promise-helpers": "^1.0.0",
+ "tslib": "^2.4.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=16.0.0"
},
"peerDependencies": {
- "graphql": "^16.5.0"
+ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
- "node_modules/@graphql-hive/federation-gateway-audit/node_modules/@apollo/query-graphs": {
- "version": "2.13.3",
- "resolved": "https://registry.npmjs.org/@apollo/query-graphs/-/query-graphs-2.13.3.tgz",
- "integrity": "sha512-X/bgsIVmamAzd8IFMDo9upO5Oi/uhAZlcBmna6yEFlN0fzdQZMHL5pp1fyzRG1wxCFi3lVjDXw2dc/380uWB9g==",
+ "node_modules/@graphql-tools/executor/node_modules/@graphql-tools/utils": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz",
+ "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==",
"dev": true,
- "license": "Elastic-2.0",
+ "license": "MIT",
"dependencies": {
- "@apollo/federation-internals": "2.13.3",
- "deep-equal": "^2.0.5",
- "ts-graphviz": "^1.5.4",
- "uuid": "^9.0.0"
+ "@graphql-typed-document-node/core": "^3.1.1",
+ "@whatwg-node/promise-helpers": "^1.0.0",
+ "cross-inspect": "1.0.1",
+ "tslib": "^2.4.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=16.0.0"
},
"peerDependencies": {
- "graphql": "^16.5.0"
+ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
- "node_modules/@graphql-hive/federation-gateway-audit/node_modules/@apollo/subgraph": {
- "version": "2.13.3",
- "resolved": "https://registry.npmjs.org/@apollo/subgraph/-/subgraph-2.13.3.tgz",
- "integrity": "sha512-nmdACpdTe+kgmNF0Yow3MoDkE0SMfvK6tTsEs3h3M9GLYCgBWZWBoDWQfNdr7kWRW6aan2SylU0K0ktF643h3g==",
+ "node_modules/@graphql-tools/merge": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.7.tgz",
+ "integrity": "sha512-Y5E1vTbTabvcXbkakdFUt4zUIzB1fyaEnVmIWN0l0GMed2gdD01TpZWLUm4RNAxpturvolrb24oGLQrBbPLSoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@apollo/cache-control-types": "^1.0.2",
- "@apollo/federation-internals": "2.13.3"
+ "@graphql-tools/utils": "^11.0.0",
+ "tslib": "^2.4.0"
},
"engines": {
- "node": ">=14.15.0"
+ "node": ">=16.0.0"
},
"peerDependencies": {
- "graphql": "^16.5.0"
- }
- },
- "node_modules/@graphql-hive/federation-gateway-audit/node_modules/@jest/diff-sequences": {
- "version": "30.3.0",
- "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz",
- "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
- "node_modules/@graphql-hive/federation-gateway-audit/node_modules/ansi-styles": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
- "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "node_modules/@graphql-tools/merge/node_modules/@graphql-tools/utils": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz",
+ "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "@graphql-typed-document-node/core": "^3.1.1",
+ "@whatwg-node/promise-helpers": "^1.0.0",
+ "cross-inspect": "1.0.1",
+ "tslib": "^2.4.0"
+ },
"engines": {
- "node": ">=10"
+ "node": ">=16.0.0"
},
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ "peerDependencies": {
+ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
- "node_modules/@graphql-hive/federation-gateway-audit/node_modules/jest-diff": {
- "version": "30.3.0",
- "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz",
- "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/diff-sequences": "30.3.0",
- "@jest/get-type": "30.1.0",
- "chalk": "^4.1.2",
- "pretty-format": "30.3.0"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
- }
- },
- "node_modules/@graphql-hive/federation-gateway-audit/node_modules/pretty-format": {
- "version": "30.3.0",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
- "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "30.0.5",
- "ansi-styles": "^5.2.0",
- "react-is": "^18.3.1"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
- }
- },
- "node_modules/@graphql-hive/laboratory": {
- "version": "0.1.2",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "radix-ui": "^1.4.3",
- "uuid": "^13.0.0"
- },
- "peerDependencies": {
- "@tanstack/react-form": "^1.23.8",
- "date-fns": "^4.1.0",
- "graphql-ws": "^6.0.6",
- "lucide-react": "^0.548.0",
- "lz-string": "^1.5.0",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0",
- "tslib": "^2.8.1",
- "zod": "^4.1.12"
- }
- },
- "node_modules/@graphql-hive/laboratory/node_modules/uuid": {
- "version": "13.0.0",
- "dev": true,
- "funding": [
- "https://github.com/sponsors/broofa",
- "https://github.com/sponsors/ctavan"
- ],
- "license": "MIT",
- "bin": {
- "uuid": "dist-node/bin/uuid"
- }
- },
- "node_modules/@graphql-hive/router-query-planner": {
- "resolved": "lib/node-addon",
- "link": true
- },
- "node_modules/@graphql-tools/executor": {
- "version": "1.5.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@graphql-tools/utils": "^11.0.0",
- "@graphql-typed-document-node/core": "^3.2.0",
- "@repeaterjs/repeater": "^3.0.4",
- "@whatwg-node/disposablestack": "^0.0.6",
- "@whatwg-node/promise-helpers": "^1.0.0",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.0.0"
- },
- "peerDependencies": {
- "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
- }
- },
- "node_modules/@graphql-tools/executor/node_modules/@graphql-tools/utils": {
- "version": "11.0.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@graphql-typed-document-node/core": "^3.1.1",
- "@whatwg-node/promise-helpers": "^1.0.0",
- "cross-inspect": "1.0.1",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.0.0"
- },
- "peerDependencies": {
- "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
- }
- },
- "node_modules/@graphql-tools/merge": {
- "version": "9.1.7",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@graphql-tools/utils": "^11.0.0",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.0.0"
- },
- "peerDependencies": {
- "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
- }
- },
- "node_modules/@graphql-tools/merge/node_modules/@graphql-tools/utils": {
- "version": "11.0.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@graphql-typed-document-node/core": "^3.1.1",
- "@whatwg-node/promise-helpers": "^1.0.0",
- "cross-inspect": "1.0.1",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.0.0"
- },
- "peerDependencies": {
- "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
- }
- },
- "node_modules/@graphql-tools/schema": {
- "version": "10.0.31",
+ "node_modules/@graphql-tools/schema": {
+ "version": "10.0.31",
+ "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.31.tgz",
+ "integrity": "sha512-ZewRgWhXef6weZ0WiP7/MV47HXiuFbFpiDUVLQl6mgXsWSsGELKFxQsyUCBos60Qqy1JEFAIu3Ns6GGYjGkqkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -502,6 +424,8 @@
},
"node_modules/@graphql-tools/schema/node_modules/@graphql-tools/utils": {
"version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz",
+ "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -519,6 +443,8 @@
},
"node_modules/@graphql-tools/utils": {
"version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.11.0.tgz",
+ "integrity": "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -536,6 +462,8 @@
},
"node_modules/@graphql-typed-document-node/core": {
"version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
+ "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -544,6 +472,8 @@
},
"node_modules/@graphql-yoga/logger": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@graphql-yoga/logger/-/logger-2.0.1.tgz",
+ "integrity": "sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -555,6 +485,8 @@
},
"node_modules/@graphql-yoga/subscription": {
"version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-5.0.5.tgz",
+ "integrity": "sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -569,6 +501,8 @@
},
"node_modules/@graphql-yoga/typed-event-target": {
"version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@graphql-yoga/typed-event-target/-/typed-event-target-3.0.2.tgz",
+ "integrity": "sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -581,6 +515,8 @@
},
"node_modules/@hapi/address": {
"version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
+ "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -592,21 +528,29 @@
},
"node_modules/@hapi/formula": {
"version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz",
+ "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/hoek": {
"version": "11.0.7",
+ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz",
+ "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/pinpoint": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz",
+ "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/tlds": {
- "version": "1.1.4",
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz",
+ "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -615,6 +559,8 @@
},
"node_modules/@hapi/topo": {
"version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz",
+ "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -622,7 +568,9 @@
}
},
"node_modules/@inquirer/ansi": {
- "version": "2.0.2",
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz",
+ "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -630,14 +578,16 @@
}
},
"node_modules/@inquirer/checkbox": {
- "version": "5.0.2",
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.3.tgz",
+ "integrity": "sha512-+G7I8CT+EHv/hasNfUl3P37DVoMoZfpA+2FXmM54dA8MxYle1YqucxbacxHalw1iAFSdKNEDTGNV7F+j1Ldqcg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@inquirer/ansi": "^2.0.2",
- "@inquirer/core": "^11.0.2",
- "@inquirer/figures": "^2.0.2",
- "@inquirer/type": "^4.0.2"
+ "@inquirer/ansi": "^2.0.5",
+ "@inquirer/core": "^11.1.8",
+ "@inquirer/figures": "^2.0.5",
+ "@inquirer/type": "^4.0.5"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -652,12 +602,14 @@
}
},
"node_modules/@inquirer/confirm": {
- "version": "6.0.2",
+ "version": "6.0.11",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.11.tgz",
+ "integrity": "sha512-pTpHjg0iEIRMYV/7oCZUMf27/383E6Wyhfc/MY+AVQGEoUobffIYWOK9YLP2XFRGz/9i6WlTQh1CkFVIo2Y7XA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^11.0.2",
- "@inquirer/type": "^4.0.2"
+ "@inquirer/core": "^11.1.8",
+ "@inquirer/type": "^4.0.5"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -672,17 +624,19 @@
}
},
"node_modules/@inquirer/core": {
- "version": "11.0.2",
+ "version": "11.1.8",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.8.tgz",
+ "integrity": "sha512-/u+yJk2pOKNDOh1ZgdUH2RQaRx6OOH4I0uwL95qPvTFTIL38YBsuSC4r1yXBB3Q6JvNqFFc202gk0Ew79rrcjA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@inquirer/ansi": "^2.0.2",
- "@inquirer/figures": "^2.0.2",
- "@inquirer/type": "^4.0.2",
+ "@inquirer/ansi": "^2.0.5",
+ "@inquirer/figures": "^2.0.5",
+ "@inquirer/type": "^4.0.5",
"cli-width": "^4.1.0",
+ "fast-wrap-ansi": "^0.2.0",
"mute-stream": "^3.0.0",
- "signal-exit": "^4.1.0",
- "wrap-ansi": "^9.0.2"
+ "signal-exit": "^4.1.0"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -697,13 +651,15 @@
}
},
"node_modules/@inquirer/editor": {
- "version": "5.0.2",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.1.0.tgz",
+ "integrity": "sha512-6wlkYl65Qfayy48gPCfU4D7li6KCAGN79mLXa/tYHZH99OfZ820yY+HA+DgE88r8YwwgeuY6PQgNqMeK6LuMmw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^11.0.2",
- "@inquirer/external-editor": "^2.0.2",
- "@inquirer/type": "^4.0.2"
+ "@inquirer/core": "^11.1.8",
+ "@inquirer/external-editor": "^3.0.0",
+ "@inquirer/type": "^4.0.5"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -718,12 +674,14 @@
}
},
"node_modules/@inquirer/expand": {
- "version": "5.0.2",
+ "version": "5.0.11",
+ "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.11.tgz",
+ "integrity": "sha512-yxSO89MQ7t4LTCwtsXQ/ppcfw2otLsum6nF+TM9pKesy3k2AhVDUIkaiJIwG6lzm/csc5n38MaFKLY0TrSHzEA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^11.0.2",
- "@inquirer/type": "^4.0.2"
+ "@inquirer/core": "^11.1.8",
+ "@inquirer/type": "^4.0.5"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -738,12 +696,14 @@
}
},
"node_modules/@inquirer/external-editor": {
- "version": "2.0.2",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.0.tgz",
+ "integrity": "sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chardet": "^2.1.1",
- "iconv-lite": "^0.7.0"
+ "iconv-lite": "^0.7.2"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -758,7 +718,9 @@
}
},
"node_modules/@inquirer/figures": {
- "version": "2.0.2",
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz",
+ "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -766,12 +728,14 @@
}
},
"node_modules/@inquirer/input": {
- "version": "5.0.2",
+ "version": "5.0.11",
+ "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.11.tgz",
+ "integrity": "sha512-twUWidn4ocPO8qi6fRM7tNWt7W1FOnOZqQ+/+PsfLUacMR5rFLDPK9ql0nBPwxi0oELbo8T5NhRs8B2+qQEqFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^11.0.2",
- "@inquirer/type": "^4.0.2"
+ "@inquirer/core": "^11.1.8",
+ "@inquirer/type": "^4.0.5"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -786,12 +750,14 @@
}
},
"node_modules/@inquirer/number": {
- "version": "4.0.2",
+ "version": "4.0.11",
+ "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.11.tgz",
+ "integrity": "sha512-Vscmim9TCksQsfjPtka/JwPUcbLhqWYrgfPf1cHrCm24X/F2joFwnageD50yMKsaX14oNGOyKf/RNXAFkNjWpA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^11.0.2",
- "@inquirer/type": "^4.0.2"
+ "@inquirer/core": "^11.1.8",
+ "@inquirer/type": "^4.0.5"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -806,13 +772,15 @@
}
},
"node_modules/@inquirer/password": {
- "version": "5.0.2",
+ "version": "5.0.11",
+ "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.11.tgz",
+ "integrity": "sha512-9KZFeRaNHIcejtPb0wN4ddFc7EvobVoAFa049eS3LrDZFxI8O7xUXiITEOinBzkZFAIwY5V4yzQae/QfO9cbbg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@inquirer/ansi": "^2.0.2",
- "@inquirer/core": "^11.0.2",
- "@inquirer/type": "^4.0.2"
+ "@inquirer/ansi": "^2.0.5",
+ "@inquirer/core": "^11.1.8",
+ "@inquirer/type": "^4.0.5"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -827,20 +795,22 @@
}
},
"node_modules/@inquirer/prompts": {
- "version": "8.0.2",
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.0.tgz",
+ "integrity": "sha512-Z3pFkae4WSzK95tvbaxR3rD9JlScFIh6/Ufw60H8Ck7GugdzYCe/3FwZCfvXwHZXjyk671w8FnVuwvxx1eP7ug==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@inquirer/checkbox": "^5.0.2",
- "@inquirer/confirm": "^6.0.2",
- "@inquirer/editor": "^5.0.2",
- "@inquirer/expand": "^5.0.2",
- "@inquirer/input": "^5.0.2",
- "@inquirer/number": "^4.0.2",
- "@inquirer/password": "^5.0.2",
- "@inquirer/rawlist": "^5.0.2",
- "@inquirer/search": "^4.0.2",
- "@inquirer/select": "^5.0.2"
+ "@inquirer/checkbox": "^5.1.3",
+ "@inquirer/confirm": "^6.0.11",
+ "@inquirer/editor": "^5.1.0",
+ "@inquirer/expand": "^5.0.11",
+ "@inquirer/input": "^5.0.11",
+ "@inquirer/number": "^4.0.11",
+ "@inquirer/password": "^5.0.11",
+ "@inquirer/rawlist": "^5.2.7",
+ "@inquirer/search": "^4.1.7",
+ "@inquirer/select": "^5.1.3"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -855,12 +825,14 @@
}
},
"node_modules/@inquirer/rawlist": {
- "version": "5.0.2",
+ "version": "5.2.7",
+ "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.7.tgz",
+ "integrity": "sha512-AqRMiD9+uE1lskDPrdqHwrV/EUmxKEBLX44SR7uxK3vD2413AmVfE5EQaPeNzYf5Pq5SitHJDYUFVF0poIr09w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^11.0.2",
- "@inquirer/type": "^4.0.2"
+ "@inquirer/core": "^11.1.8",
+ "@inquirer/type": "^4.0.5"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -875,13 +847,15 @@
}
},
"node_modules/@inquirer/search": {
- "version": "4.0.2",
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.7.tgz",
+ "integrity": "sha512-1y7+0N65AWk5RdlXH/Kn13txf3IjIQ7OEfhCEkDTU+h5wKMLq8DUF3P6z+/kLSxDGDtQT1dRBWEUC3o/VvImsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^11.0.2",
- "@inquirer/figures": "^2.0.2",
- "@inquirer/type": "^4.0.2"
+ "@inquirer/core": "^11.1.8",
+ "@inquirer/figures": "^2.0.5",
+ "@inquirer/type": "^4.0.5"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -896,14 +870,16 @@
}
},
"node_modules/@inquirer/select": {
- "version": "5.0.2",
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.3.tgz",
+ "integrity": "sha512-zYyqWgGQi3NhBcNq4Isc5rB3oEdQEh1Q/EcAnOW0FK4MpnXWkvSBYgA4cYrTM4A9UB573omouZbnL9JJ74Mq3A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@inquirer/ansi": "^2.0.2",
- "@inquirer/core": "^11.0.2",
- "@inquirer/figures": "^2.0.2",
- "@inquirer/type": "^4.0.2"
+ "@inquirer/ansi": "^2.0.5",
+ "@inquirer/core": "^11.1.8",
+ "@inquirer/figures": "^2.0.5",
+ "@inquirer/type": "^4.0.5"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
@@ -918,7 +894,9 @@
}
},
"node_modules/@inquirer/type": {
- "version": "4.0.2",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz",
+ "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -933,8 +911,20 @@
}
}
},
+ "node_modules/@jest/diff-sequences": {
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz",
+ "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
"node_modules/@jest/get-type": {
"version": "30.1.0",
+ "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz",
+ "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -943,6 +933,8 @@
},
"node_modules/@jest/schemas": {
"version": "30.0.5",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
+ "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -994,6 +986,8 @@
},
"node_modules/@napi-rs/cross-toolchain": {
"version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@napi-rs/cross-toolchain/-/cross-toolchain-1.0.3.tgz",
+ "integrity": "sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg==",
"dev": true,
"license": "MIT",
"workspaces": [
@@ -1053,6 +1047,8 @@
},
"node_modules/@napi-rs/lzma": {
"version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma/-/lzma-1.4.5.tgz",
+ "integrity": "sha512-zS5LuN1OBPAyZpda2ZZgYOEDC+xecUdAGnrvbYzjnLXkrq/OBC3B9qcRvlxbDR3k5H/gVfvef1/jyUqPknqjbg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1082,57 +1078,864 @@
"@napi-rs/lzma-win32-x64-msvc": "1.4.5"
}
},
- "node_modules/@napi-rs/tar": {
- "version": "1.1.0",
+ "node_modules/@napi-rs/lzma-android-arm-eabi": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-android-arm-eabi/-/lzma-android-arm-eabi-1.4.5.tgz",
+ "integrity": "sha512-Up4gpyw2SacmyKWWEib06GhiDdF+H+CCU0LAV8pnM4aJIDqKKd5LHSlBht83Jut6frkB0vwEPmAkv4NjQ5u//Q==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
"license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-android-arm64": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-android-arm64/-/lzma-android-arm64-1.4.5.tgz",
+ "integrity": "sha512-uwa8sLlWEzkAM0MWyoZJg0JTD3BkPknvejAFG2acUA1raXM8jLrqujWCdOStisXhqQjZ2nDMp3FV6cs//zjfuQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-darwin-arm64": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-darwin-arm64/-/lzma-darwin-arm64-1.4.5.tgz",
+ "integrity": "sha512-0Y0TQLQ2xAjVabrMDem1NhIssOZzF/y/dqetc6OT8mD3xMTDtF8u5BqZoX3MyPc9FzpsZw4ksol+w7DsxHrpMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-darwin-x64": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-darwin-x64/-/lzma-darwin-x64-1.4.5.tgz",
+ "integrity": "sha512-vR2IUyJY3En+V1wJkwmbGWcYiT8pHloTAWdW4pG24+51GIq+intst6Uf6D/r46citObGZrlX0QvMarOkQeHWpw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-freebsd-x64": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-freebsd-x64/-/lzma-freebsd-x64-1.4.5.tgz",
+ "integrity": "sha512-XpnYQC5SVovO35tF0xGkbHYjsS6kqyNCjuaLQ2dbEblFRr5cAZVvsJ/9h7zj/5FluJPJRDojVNxGyRhTp4z2lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-arm-gnueabihf": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm-gnueabihf/-/lzma-linux-arm-gnueabihf-1.4.5.tgz",
+ "integrity": "sha512-ic1ZZMoRfRMwtSwxkyw4zIlbDZGC6davC9r+2oX6x9QiF247BRqqT94qGeL5ZP4Vtz0Hyy7TEViWhx5j6Bpzvw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-arm64-gnu": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm64-gnu/-/lzma-linux-arm64-gnu-1.4.5.tgz",
+ "integrity": "sha512-asEp7FPd7C1Yi6DQb45a3KPHKOFBSfGuJWXcAd4/bL2Fjetb2n/KK2z14yfW8YC/Fv6x3rBM0VAZKmJuz4tysg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-arm64-musl": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm64-musl/-/lzma-linux-arm64-musl-1.4.5.tgz",
+ "integrity": "sha512-yWjcPDgJ2nIL3KNvi4536dlT/CcCWO0DUyEOlBs/SacG7BeD6IjGh6yYzd3/X1Y3JItCbZoDoLUH8iB1lTXo3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-ppc64-gnu": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-ppc64-gnu/-/lzma-linux-ppc64-gnu-1.4.5.tgz",
+ "integrity": "sha512-0XRhKuIU/9ZjT4WDIG/qnX7Xz7mSQHYZo9Gb3MP2gcvBgr6BA4zywQ9k3gmQaPn9ECE+CZg2V7DV7kT+x2pUMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-riscv64-gnu": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-riscv64-gnu/-/lzma-linux-riscv64-gnu-1.4.5.tgz",
+ "integrity": "sha512-QrqDIPEUUB23GCpyQj/QFyMlr8SGxxyExeZz9OWFnHfb70kXdTLWrHS/hEI1Ru+lSbQ/6xRqeoGyQ4Aqdg+/RA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-s390x-gnu": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-s390x-gnu/-/lzma-linux-s390x-gnu-1.4.5.tgz",
+ "integrity": "sha512-k8RVM5aMhW86E9H0QXdquwojew4H3SwPxbRVbl49/COJQWCUjGi79X6mYruMnMPEznZinUiT1jgKbFo2A00NdA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.4.5.tgz",
+ "integrity": "sha512-6rMtBgnIq2Wcl1rQdZsnM+rtCcVCbws1nF8S2NzaUsVaZv8bjrPiAa0lwg4Eqnn1d9lgwqT+cZgm5m+//K08Kw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-x64-musl": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-musl/-/lzma-linux-x64-musl-1.4.5.tgz",
+ "integrity": "sha512-eiadGBKi7Vd0bCArBUOO/qqRYPHt/VQVvGyYvDFt6C2ZSIjlD+HuOl+2oS1sjf4CFjK4eDIog6EdXnL0NE6iyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-wasm32-wasi": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-wasm32-wasi/-/lzma-wasm32-wasi-1.4.5.tgz",
+ "integrity": "sha512-+VyHHlr68dvey6fXc2hehw9gHVFIW3TtGF1XkcbAu65qVXsA9D/T+uuoRVqhE+JCyFHFrO0ixRbZDRK1XJt1sA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@napi-rs/lzma-win32-arm64-msvc": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-arm64-msvc/-/lzma-win32-arm64-msvc-1.4.5.tgz",
+ "integrity": "sha512-eewnqvIyyhHi3KaZtBOJXohLvwwN27gfS2G/YDWdfHlbz1jrmfeHAmzMsP5qv8vGB+T80TMHNkro4kYjeh6Deg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-win32-ia32-msvc": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-ia32-msvc/-/lzma-win32-ia32-msvc-1.4.5.tgz",
+ "integrity": "sha512-OeacFVRCJOKNU/a0ephUfYZ2Yt+NvaHze/4TgOwJ0J0P4P7X1mHzN+ig9Iyd74aQDXYqc7kaCXA2dpAOcH87Cg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/lzma-win32-x64-msvc": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-x64-msvc/-/lzma-win32-x64-msvc-1.4.5.tgz",
+ "integrity": "sha512-T4I1SamdSmtyZgDXGAGP+y5LEK5vxHUFwe8mz6D4R7Sa5/WCxTcCIgPJ9BD7RkpO17lzhlaM2vmVvMy96Lvk9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar/-/tar-1.1.0.tgz",
+ "integrity": "sha512-7cmzIu+Vbupriudo7UudoMRH2OA3cTw67vva8MxeoAe5S7vPFI7z0vp0pMXiA25S8IUJefImQ90FeJjl8fjEaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@napi-rs/tar-android-arm-eabi": "1.1.0",
+ "@napi-rs/tar-android-arm64": "1.1.0",
+ "@napi-rs/tar-darwin-arm64": "1.1.0",
+ "@napi-rs/tar-darwin-x64": "1.1.0",
+ "@napi-rs/tar-freebsd-x64": "1.1.0",
+ "@napi-rs/tar-linux-arm-gnueabihf": "1.1.0",
+ "@napi-rs/tar-linux-arm64-gnu": "1.1.0",
+ "@napi-rs/tar-linux-arm64-musl": "1.1.0",
+ "@napi-rs/tar-linux-ppc64-gnu": "1.1.0",
+ "@napi-rs/tar-linux-s390x-gnu": "1.1.0",
+ "@napi-rs/tar-linux-x64-gnu": "1.1.0",
+ "@napi-rs/tar-linux-x64-musl": "1.1.0",
+ "@napi-rs/tar-wasm32-wasi": "1.1.0",
+ "@napi-rs/tar-win32-arm64-msvc": "1.1.0",
+ "@napi-rs/tar-win32-ia32-msvc": "1.1.0",
+ "@napi-rs/tar-win32-x64-msvc": "1.1.0"
+ }
+ },
+ "node_modules/@napi-rs/tar-android-arm-eabi": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-android-arm-eabi/-/tar-android-arm-eabi-1.1.0.tgz",
+ "integrity": "sha512-h2Ryndraj/YiKgMV/r5by1cDusluYIRT0CaE0/PekQ4u+Wpy2iUVqvzVU98ZPnhXaNeYxEvVJHNGafpOfaD0TA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-android-arm64": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-android-arm64/-/tar-android-arm64-1.1.0.tgz",
+ "integrity": "sha512-DJFyQHr1ZxNZorm/gzc1qBNLF/FcKzcH0V0Vwan5P+o0aE2keQIGEjJ09FudkF9v6uOuJjHCVDdK6S6uHtShAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-darwin-arm64": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-darwin-arm64/-/tar-darwin-arm64-1.1.0.tgz",
+ "integrity": "sha512-Zz2sXRzjIX4e532zD6xm2SjXEym6MkvfCvL2RMpG2+UwNVDVscHNcz3d47Pf3sysP2e2af7fBB3TIoK2f6trPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-darwin-x64": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-darwin-x64/-/tar-darwin-x64-1.1.0.tgz",
+ "integrity": "sha512-EI+CptIMNweT0ms9S3mkP/q+J6FNZ1Q6pvpJOEcWglRfyfQpLqjlC0O+dptruTPE8VamKYuqdjxfqD8hifZDOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-freebsd-x64": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-freebsd-x64/-/tar-freebsd-x64-1.1.0.tgz",
+ "integrity": "sha512-J0PIqX+pl6lBIAckL/c87gpodLbjZB1OtIK+RDscKC9NLdpVv6VGOxzUV/fYev/hctcE8EfkLbgFOfpmVQPg2g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-linux-arm-gnueabihf": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-arm-gnueabihf/-/tar-linux-arm-gnueabihf-1.1.0.tgz",
+ "integrity": "sha512-SLgIQo3f3EjkZ82ZwvrEgFvMdDAhsxCYjyoSuWfHCz0U16qx3SuGCp8+FYOPYCECHN3ZlGjXnoAIt9ERd0dEUg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-linux-arm64-gnu": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-arm64-gnu/-/tar-linux-arm64-gnu-1.1.0.tgz",
+ "integrity": "sha512-d014cdle52EGaH6GpYTQOP9Py7glMO1zz/+ynJPjjzYFSxvdYx0byrjumZk2UQdIyGZiJO2MEFpCkEEKFSgPYA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-linux-arm64-musl": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-arm64-musl/-/tar-linux-arm64-musl-1.1.0.tgz",
+ "integrity": "sha512-L/y1/26q9L/uBqiW/JdOb/Dc94egFvNALUZV2WCGKQXc6UByPBMgdiEyW2dtoYxYYYYc+AKD+jr+wQPcvX2vrQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-linux-ppc64-gnu": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-ppc64-gnu/-/tar-linux-ppc64-gnu-1.1.0.tgz",
+ "integrity": "sha512-EPE1K/80RQvPbLRJDJs1QmCIcH+7WRi0F73+oTe1582y9RtfGRuzAkzeBuAGRXAQEjRQw/RjtNqr6UTJ+8UuWQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-linux-s390x-gnu": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-s390x-gnu/-/tar-linux-s390x-gnu-1.1.0.tgz",
+ "integrity": "sha512-B2jhWiB1ffw1nQBqLUP1h4+J1ovAxBOoe5N2IqDMOc63fsPZKNqF1PvO/dIem8z7LL4U4bsfmhy3gBfu547oNQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-linux-x64-gnu": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-x64-gnu/-/tar-linux-x64-gnu-1.1.0.tgz",
+ "integrity": "sha512-tbZDHnb9617lTnsDMGo/eAMZxnsQFnaRe+MszRqHguKfMwkisc9CCJnks/r1o84u5fECI+J/HOrKXgczq/3Oww==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-linux-x64-musl": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-x64-musl/-/tar-linux-x64-musl-1.1.0.tgz",
+ "integrity": "sha512-dV6cODlzbO8u6Anmv2N/ilQHq/AWz0xyltuXoLU3yUyXbZcnWYZuB2rL8OBGPmqNcD+x9NdScBNXh7vWN0naSQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-wasm32-wasi": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-wasm32-wasi/-/tar-wasm32-wasi-1.1.0.tgz",
+ "integrity": "sha512-jIa9nb2HzOrfH0F8QQ9g3WE4aMH5vSI5/1NYVNm9ysCmNjCCtMXCAhlI3WKCdm/DwHf0zLqdrrtDFXODcNaqMw==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@napi-rs/tar-win32-arm64-msvc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-win32-arm64-msvc/-/tar-win32-arm64-msvc-1.1.0.tgz",
+ "integrity": "sha512-vfpG71OB0ijtjemp3WTdmBKJm9R70KM8vsSExMsIQtV0lVzP07oM1CW6JbNRPXNLhRoue9ofYLiUDk8bE0Hckg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-win32-ia32-msvc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-win32-ia32-msvc/-/tar-win32-ia32-msvc-1.1.0.tgz",
+ "integrity": "sha512-hGPyPW60YSpOSgzfy68DLBHgi6HxkAM+L59ZZZPMQ0TOXjQg+p2EW87+TjZfJOkSpbYiEkULwa/f4a2hcVjsqQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/tar-win32-x64-msvc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/tar-win32-x64-msvc/-/tar-win32-x64-msvc-1.1.0.tgz",
+ "integrity": "sha512-L6Ed1DxXK9YSCMyvpR8MiNAyKNkQLjsHsHK9E0qnHa8NzLFqzDKhvs5LfnWxM2kJ+F7m/e5n9zPm24kHb3LsVw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
+ "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools/-/wasm-tools-1.0.1.tgz",
+ "integrity": "sha512-enkZYyuCdo+9jneCPE/0fjIta4wWnvVN9hBo2HuiMpRF0q3lzv1J6b/cl7i0mxZUKhBrV3aCKDBQnCOhwKbPmQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@napi-rs/wasm-tools-android-arm-eabi": "1.0.1",
+ "@napi-rs/wasm-tools-android-arm64": "1.0.1",
+ "@napi-rs/wasm-tools-darwin-arm64": "1.0.1",
+ "@napi-rs/wasm-tools-darwin-x64": "1.0.1",
+ "@napi-rs/wasm-tools-freebsd-x64": "1.0.1",
+ "@napi-rs/wasm-tools-linux-arm64-gnu": "1.0.1",
+ "@napi-rs/wasm-tools-linux-arm64-musl": "1.0.1",
+ "@napi-rs/wasm-tools-linux-x64-gnu": "1.0.1",
+ "@napi-rs/wasm-tools-linux-x64-musl": "1.0.1",
+ "@napi-rs/wasm-tools-wasm32-wasi": "1.0.1",
+ "@napi-rs/wasm-tools-win32-arm64-msvc": "1.0.1",
+ "@napi-rs/wasm-tools-win32-ia32-msvc": "1.0.1",
+ "@napi-rs/wasm-tools-win32-x64-msvc": "1.0.1"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools-android-arm-eabi": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-android-arm-eabi/-/wasm-tools-android-arm-eabi-1.0.1.tgz",
+ "integrity": "sha512-lr07E/l571Gft5v4aA1dI8koJEmF1F0UigBbsqg9OWNzg80H3lDPO+auv85y3T/NHE3GirDk7x/D3sLO57vayw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools-android-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-android-arm64/-/wasm-tools-android-arm64-1.0.1.tgz",
+ "integrity": "sha512-WDR7S+aRLV6LtBJAg5fmjKkTZIdrEnnQxgdsb7Cf8pYiMWBHLU+LC49OUVppQ2YSPY0+GeYm9yuZWW3kLjJ7Bg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools-darwin-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-darwin-arm64/-/wasm-tools-darwin-arm64-1.0.1.tgz",
+ "integrity": "sha512-qWTI+EEkiN0oIn/N2gQo7+TVYil+AJ20jjuzD2vATS6uIjVz+Updeqmszi7zq7rdFTLp6Ea3/z4kDKIfZwmR9g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools-darwin-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-darwin-x64/-/wasm-tools-darwin-x64-1.0.1.tgz",
+ "integrity": "sha512-bA6hubqtHROR5UI3tToAF/c6TDmaAgF0SWgo4rADHtQ4wdn0JeogvOk50gs2TYVhKPE2ZD2+qqt7oBKB+sxW3A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools-freebsd-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-freebsd-x64/-/wasm-tools-freebsd-x64-1.0.1.tgz",
+ "integrity": "sha512-90+KLBkD9hZEjPQW1MDfwSt5J1L46EUKacpCZWyRuL6iIEO5CgWU0V/JnEgFsDOGyyYtiTvHc5bUdUTWd4I9Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools-linux-arm64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-arm64-gnu/-/wasm-tools-linux-arm64-gnu-1.0.1.tgz",
+ "integrity": "sha512-rG0QlS65x9K/u3HrKafDf8cFKj5wV2JHGfl8abWgKew0GVPyp6vfsDweOwHbWAjcHtp2LHi6JHoW80/MTHm52Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools-linux-arm64-musl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-arm64-musl/-/wasm-tools-linux-arm64-musl-1.0.1.tgz",
+ "integrity": "sha512-jAasbIvjZXCgX0TCuEFQr+4D6Lla/3AAVx2LmDuMjgG4xoIXzjKWl7c4chuaD+TI+prWT0X6LJcdzFT+ROKGHQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools-linux-x64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-x64-gnu/-/wasm-tools-linux-x64-gnu-1.0.1.tgz",
+ "integrity": "sha512-Plgk5rPqqK2nocBGajkMVbGm010Z7dnUgq0wtnYRZbzWWxwWcXfZMPa8EYxrK4eE8SzpI7VlZP1tdVsdjgGwMw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools-linux-x64-musl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-x64-musl/-/wasm-tools-linux-x64-musl-1.0.1.tgz",
+ "integrity": "sha512-GW7AzGuWxtQkyHknHWYFdR0CHmW6is8rG2Rf4V6GNmMpmwtXt/ItWYWtBe4zqJWycMNazpfZKSw/BpT7/MVCXQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools-wasm32-wasi": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-wasm32-wasi/-/wasm-tools-wasm32-wasi-1.0.1.tgz",
+ "integrity": "sha512-/nQVSTrqSsn7YdAc2R7Ips/tnw5SPUcl3D7QrXCNGPqjbatIspnaexvaOYNyKMU6xPu+pc0BTnKVmqhlJJCPLA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools-win32-arm64-msvc": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-win32-arm64-msvc/-/wasm-tools-win32-arm64-msvc-1.0.1.tgz",
+ "integrity": "sha512-PFi7oJIBu5w7Qzh3dwFea3sHRO3pojMsaEnUIy22QvsW+UJfNQwJCryVrpoUt8m4QyZXI+saEq/0r4GwdoHYFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/wasm-tools-win32-ia32-msvc": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-win32-ia32-msvc/-/wasm-tools-win32-ia32-msvc-1.0.1.tgz",
+ "integrity": "sha512-gXkuYzxQsgkj05Zaq+KQTkHIN83dFAwMcTKa2aQcpYPRImFm2AQzEyLtpXmyCWzJ0F9ZYAOmbSyrNew8/us6bw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
"node": ">= 10"
- },
- "optionalDependencies": {
- "@napi-rs/tar-android-arm-eabi": "1.1.0",
- "@napi-rs/tar-android-arm64": "1.1.0",
- "@napi-rs/tar-darwin-arm64": "1.1.0",
- "@napi-rs/tar-darwin-x64": "1.1.0",
- "@napi-rs/tar-freebsd-x64": "1.1.0",
- "@napi-rs/tar-linux-arm-gnueabihf": "1.1.0",
- "@napi-rs/tar-linux-arm64-gnu": "1.1.0",
- "@napi-rs/tar-linux-arm64-musl": "1.1.0",
- "@napi-rs/tar-linux-ppc64-gnu": "1.1.0",
- "@napi-rs/tar-linux-s390x-gnu": "1.1.0",
- "@napi-rs/tar-linux-x64-gnu": "1.1.0",
- "@napi-rs/tar-linux-x64-musl": "1.1.0",
- "@napi-rs/tar-wasm32-wasi": "1.1.0",
- "@napi-rs/tar-win32-arm64-msvc": "1.1.0",
- "@napi-rs/tar-win32-ia32-msvc": "1.1.0",
- "@napi-rs/tar-win32-x64-msvc": "1.1.0"
}
},
- "node_modules/@napi-rs/wasm-tools": {
+ "node_modules/@napi-rs/wasm-tools-win32-x64-msvc": {
"version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-win32-x64-msvc/-/wasm-tools-win32-x64-msvc-1.0.1.tgz",
+ "integrity": "sha512-rEAf05nol3e3eei2sRButmgXP+6ATgm0/38MKhz9Isne82T4rPIMYsCIFj0kOisaGeVwoi2fnm7O9oWp5YVnYQ==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
"license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
"node": ">= 10"
- },
- "optionalDependencies": {
- "@napi-rs/wasm-tools-android-arm-eabi": "1.0.1",
- "@napi-rs/wasm-tools-android-arm64": "1.0.1",
- "@napi-rs/wasm-tools-darwin-arm64": "1.0.1",
- "@napi-rs/wasm-tools-darwin-x64": "1.0.1",
- "@napi-rs/wasm-tools-freebsd-x64": "1.0.1",
- "@napi-rs/wasm-tools-linux-arm64-gnu": "1.0.1",
- "@napi-rs/wasm-tools-linux-arm64-musl": "1.0.1",
- "@napi-rs/wasm-tools-linux-x64-gnu": "1.0.1",
- "@napi-rs/wasm-tools-linux-x64-musl": "1.0.1",
- "@napi-rs/wasm-tools-wasm32-wasi": "1.0.1",
- "@napi-rs/wasm-tools-win32-arm64-msvc": "1.0.1",
- "@napi-rs/wasm-tools-win32-ia32-msvc": "1.0.1",
- "@napi-rs/wasm-tools-win32-x64-msvc": "1.0.1"
}
},
"node_modules/@octokit/auth-token": {
"version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
+ "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1141,8 +1944,11 @@
},
"node_modules/@octokit/core": {
"version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
+ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.3",
@@ -1157,7 +1963,9 @@
}
},
"node_modules/@octokit/endpoint": {
- "version": "11.0.2",
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz",
+ "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1170,6 +1978,8 @@
},
"node_modules/@octokit/graphql": {
"version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
+ "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1183,11 +1993,15 @@
},
"node_modules/@octokit/openapi-types": {
"version": "27.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
+ "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
"dev": true,
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
+ "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1202,6 +2016,8 @@
},
"node_modules/@octokit/plugin-request-log": {
"version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz",
+ "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1213,6 +2029,8 @@
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
+ "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1226,14 +2044,17 @@
}
},
"node_modules/@octokit/request": {
- "version": "10.0.7",
+ "version": "10.0.8",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz",
+ "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@octokit/endpoint": "^11.0.2",
+ "@octokit/endpoint": "^11.0.3",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"fast-content-type-parse": "^3.0.0",
+ "json-with-bigint": "^3.5.3",
"universal-user-agent": "^7.0.2"
},
"engines": {
@@ -1242,6 +2063,8 @@
},
"node_modules/@octokit/request-error": {
"version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
+ "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1253,6 +2076,8 @@
},
"node_modules/@octokit/rest": {
"version": "22.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz",
+ "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1267,6 +2092,8 @@
},
"node_modules/@octokit/types": {
"version": "16.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
+ "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1275,16 +2102,22 @@
},
"node_modules/@radix-ui/number": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"dev": true,
"license": "MIT"
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"dev": true,
"license": "MIT"
},
"node_modules/@radix-ui/react-accessible-icon": {
"version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz",
+ "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1307,6 +2140,8 @@
},
"node_modules/@radix-ui/react-accordion": {
"version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
+ "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1337,6 +2172,8 @@
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
+ "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1364,6 +2201,8 @@
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1386,6 +2225,8 @@
},
"node_modules/@radix-ui/react-aspect-ratio": {
"version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz",
+ "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1408,6 +2249,8 @@
},
"node_modules/@radix-ui/react-avatar": {
"version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
+ "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1434,6 +2277,8 @@
},
"node_modules/@radix-ui/react-checkbox": {
"version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
+ "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1463,6 +2308,8 @@
},
"node_modules/@radix-ui/react-collapsible": {
"version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
+ "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1492,6 +2339,8 @@
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1517,6 +2366,8 @@
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -1531,6 +2382,8 @@
},
"node_modules/@radix-ui/react-context": {
"version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -1545,6 +2398,8 @@
},
"node_modules/@radix-ui/react-context-menu": {
"version": "2.2.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz",
+ "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1572,6 +2427,8 @@
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1607,6 +2464,8 @@
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -1621,6 +2480,8 @@
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1647,6 +2508,8 @@
},
"node_modules/@radix-ui/react-dropdown-menu": {
"version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1675,6 +2538,8 @@
},
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -1689,6 +2554,8 @@
},
"node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1713,6 +2580,8 @@
},
"node_modules/@radix-ui/react-form": {
"version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz",
+ "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1740,6 +2609,8 @@
},
"node_modules/@radix-ui/react-hover-card": {
"version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
+ "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1770,6 +2641,8 @@
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1787,6 +2660,8 @@
},
"node_modules/@radix-ui/react-label": {
"version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
+ "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1809,6 +2684,8 @@
},
"node_modules/@radix-ui/react-menu": {
"version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1848,6 +2725,8 @@
},
"node_modules/@radix-ui/react-menubar": {
"version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz",
+ "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1879,6 +2758,8 @@
},
"node_modules/@radix-ui/react-navigation-menu": {
"version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
+ "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1914,6 +2795,8 @@
},
"node_modules/@radix-ui/react-one-time-password-field": {
"version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz",
+ "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1947,6 +2830,8 @@
},
"node_modules/@radix-ui/react-password-toggle-field": {
"version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz",
+ "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1976,6 +2861,8 @@
},
"node_modules/@radix-ui/react-popover": {
"version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
+ "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2012,6 +2899,8 @@
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2043,6 +2932,8 @@
},
"node_modules/@radix-ui/react-portal": {
"version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2066,6 +2957,8 @@
},
"node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2089,6 +2982,8 @@
},
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2111,6 +3006,8 @@
},
"node_modules/@radix-ui/react-progress": {
"version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
+ "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2134,6 +3031,8 @@
},
"node_modules/@radix-ui/react-radio-group": {
"version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
+ "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2165,6 +3064,8 @@
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2195,6 +3096,8 @@
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
+ "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2225,6 +3128,8 @@
},
"node_modules/@radix-ui/react-select": {
"version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2267,6 +3172,8 @@
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
+ "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2289,6 +3196,8 @@
},
"node_modules/@radix-ui/react-slider": {
"version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
+ "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2321,6 +3230,8 @@
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2338,6 +3249,8 @@
},
"node_modules/@radix-ui/react-switch": {
"version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
+ "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2366,6 +3279,8 @@
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2395,6 +3310,8 @@
},
"node_modules/@radix-ui/react-toast": {
"version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz",
+ "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2428,6 +3345,8 @@
},
"node_modules/@radix-ui/react-toggle": {
"version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
+ "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2452,6 +3371,8 @@
},
"node_modules/@radix-ui/react-toggle-group": {
"version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz",
+ "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2480,6 +3401,8 @@
},
"node_modules/@radix-ui/react-toolbar": {
"version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz",
+ "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2508,6 +3431,8 @@
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2541,6 +3466,8 @@
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -2555,6 +3482,8 @@
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2573,6 +3502,8 @@
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2590,6 +3521,8 @@
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2607,6 +3540,8 @@
},
"node_modules/@radix-ui/react-use-is-hydrated": {
"version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2624,6 +3559,8 @@
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -2638,6 +3575,8 @@
},
"node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -2652,6 +3591,8 @@
},
"node_modules/@radix-ui/react-use-rect": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2669,6 +3610,8 @@
},
"node_modules/@radix-ui/react-use-size": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2686,6 +3629,8 @@
},
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2708,26 +3653,36 @@
},
"node_modules/@radix-ui/rect": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"dev": true,
"license": "MIT"
},
"node_modules/@repeaterjs/repeater": {
"version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz",
+ "integrity": "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==",
"dev": true,
"license": "MIT"
},
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
+ "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
"dev": true,
"license": "MIT"
},
"node_modules/@sinclair/typebox": {
- "version": "0.34.41",
+ "version": "0.34.49",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz",
+ "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==",
"dev": true,
"license": "MIT"
},
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
+ "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2738,7 +3693,9 @@
}
},
"node_modules/@standard-schema/spec": {
- "version": "1.0.0",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"dev": true,
"license": "MIT"
},
@@ -2748,7 +3705,6 @@
"integrity": "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"intent": "bin/intent.js"
},
@@ -2766,7 +3722,6 @@
"integrity": "sha512-4zroxL6VDj5O+w7l3dYZnUeL/h30KtNSV7UWzKAL7cl+8clMFdISPDlDlluS37As7oqvPVKo8B83VlIBvgmRog==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@tanstack/devtools-event-client": "^0.4.1",
"@tanstack/pacer-lite": "^0.1.1",
@@ -2783,7 +3738,6 @@
"integrity": "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
},
@@ -2822,7 +3776,6 @@
"integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@tanstack/store": "0.9.3",
"use-sync-external-store": "^1.6.0"
@@ -2842,12 +3795,22 @@
"integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@types/k6": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@types/k6/-/k6-1.6.0.tgz",
@@ -2861,17 +3824,22 @@
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/@types/uuid": {
"version": "9.0.8",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
+ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"dev": true,
"license": "MIT"
},
"node_modules/@whatwg-node/cookie-store": {
"version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@whatwg-node/cookie-store/-/cookie-store-0.2.3.tgz",
+ "integrity": "sha512-LPDv38Hv+RrVA8o7x4YOjQx4qhqOs3Lm8aPTsPrQCUF3MxXEUBEYcUqD5wRTaTxzQEd7ZXbDEnXBLDECFoOfjg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2884,6 +3852,8 @@
},
"node_modules/@whatwg-node/disposablestack": {
"version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz",
+ "integrity": "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2896,6 +3866,8 @@
},
"node_modules/@whatwg-node/events": {
"version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.2.tgz",
+ "integrity": "sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2907,6 +3879,8 @@
},
"node_modules/@whatwg-node/fetch": {
"version": "0.10.13",
+ "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.13.tgz",
+ "integrity": "sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2918,7 +3892,9 @@
}
},
"node_modules/@whatwg-node/node-fetch": {
- "version": "0.8.4",
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.8.5.tgz",
+ "integrity": "sha512-4xzCl/zphPqlp9tASLVeUhB5+WJHbuWGYpfoC2q1qh5dw0AqZBW7L27V5roxYWijPxj4sspRAAoOH3d2ztaHUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2933,6 +3909,8 @@
},
"node_modules/@whatwg-node/promise-helpers": {
"version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz",
+ "integrity": "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2943,7 +3921,9 @@
}
},
"node_modules/@whatwg-node/server": {
- "version": "0.10.17",
+ "version": "0.10.18",
+ "resolved": "https://registry.npmjs.org/@whatwg-node/server/-/server-0.10.18.tgz",
+ "integrity": "sha512-kMwLlxUbduttIgaPdSkmEarFpP+mSY8FEm+QWMBRJwxOHWkri+cxd8KZHO9EMrB9vgUuz+5WEaCawaL5wGVoXg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2959,6 +3939,8 @@
},
"node_modules/address": {
"version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/address/-/address-2.0.3.tgz",
+ "integrity": "sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2967,6 +3949,8 @@
},
"node_modules/ansi-regex": {
"version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2978,6 +3962,8 @@
},
"node_modules/ansi-styles": {
"version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2992,15 +3978,21 @@
},
"node_modules/arg": {
"version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3012,6 +4004,8 @@
},
"node_modules/array-buffer-byte-length": {
"version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3027,6 +4021,8 @@
},
"node_modules/async-retry": {
"version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
+ "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3035,11 +4031,15 @@
},
"node_modules/asynckit": {
"version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3053,17 +4053,21 @@
}
},
"node_modules/axios": {
- "version": "1.13.5",
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
+ "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
- "proxy-from-env": "^1.1.0"
+ "proxy-from-env": "^2.1.0"
}
},
"node_modules/before-after-hook": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
+ "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
"dev": true,
"license": "Apache-2.0"
},
@@ -3073,11 +4077,15 @@
},
"node_modules/bluebird": {
"version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true,
"license": "MIT"
},
"node_modules/call-bind": {
"version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3095,6 +4103,8 @@
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3107,6 +4117,8 @@
},
"node_modules/call-bound": {
"version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3122,6 +4134,8 @@
},
"node_modules/chalk": {
"version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3137,11 +4151,15 @@
},
"node_modules/chardet": {
"version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
+ "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
"dev": true,
"license": "MIT"
},
"node_modules/check-more-types": {
"version": "2.24.0",
+ "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
+ "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3150,6 +4168,8 @@
},
"node_modules/cli-width": {
"version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
"dev": true,
"license": "ISC",
"engines": {
@@ -3158,6 +4178,8 @@
},
"node_modules/clipanion": {
"version": "4.0.0-rc.4",
+ "resolved": "https://registry.npmjs.org/clipanion/-/clipanion-4.0.0-rc.4.tgz",
+ "integrity": "sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q==",
"dev": true,
"license": "MIT",
"workspaces": [
@@ -3172,6 +4194,8 @@
},
"node_modules/cliui": {
"version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
+ "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -3185,6 +4209,8 @@
},
"node_modules/color-convert": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3196,16 +4222,22 @@
},
"node_modules/color-name": {
"version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/colorette": {
"version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3217,6 +4249,8 @@
},
"node_modules/cross-inspect": {
"version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz",
+ "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3228,6 +4262,8 @@
},
"node_modules/cross-spawn": {
"version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3239,14 +4275,6 @@
"node": ">= 8"
}
},
- "node_modules/cross-spawn/node_modules/path-key": {
- "version": "3.1.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
@@ -3261,6 +4289,8 @@
},
"node_modules/debug": {
"version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3277,6 +4307,8 @@
},
"node_modules/deep-equal": {
"version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
+ "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3308,6 +4340,8 @@
},
"node_modules/define-data-property": {
"version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3324,6 +4358,8 @@
},
"node_modules/define-properties": {
"version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3340,6 +4376,8 @@
},
"node_modules/delayed-stream": {
"version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3348,11 +4386,15 @@
},
"node_modules/detect-node-es": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"dev": true,
"license": "MIT"
},
"node_modules/detect-port": {
"version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-2.1.0.tgz",
+ "integrity": "sha512-epZuWb/6Q62L+nDHJc/hQAqf8pylsqgk3BpZXVBx1CDnr3nkrVNn73Uu1rXcFzkNcc+hkP3whuOg7JZYaQB65Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3372,6 +4414,8 @@
},
"node_modules/dunder-proto": {
"version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3385,11 +4429,15 @@
},
"node_modules/duplexer": {
"version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
+ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
"dev": true,
"license": "MIT"
},
"node_modules/emnapi": {
- "version": "1.7.1",
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/emnapi/-/emnapi-1.9.2.tgz",
+ "integrity": "sha512-OdUoQe/8so7FvubnE/DNV9sNNSFwDYQiK4ZCAz4agMnD1s6faLuDn2gzxfJrmMoKfxZhhsckqGNwqPnS5K140A==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -3403,11 +4451,15 @@
},
"node_modules/emoji-regex": {
"version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/es-define-property": {
"version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3416,6 +4468,8 @@
},
"node_modules/es-errors": {
"version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3424,6 +4478,8 @@
},
"node_modules/es-get-iterator": {
"version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
+ "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3443,6 +4499,8 @@
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3454,6 +4512,8 @@
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3467,7 +4527,9 @@
}
},
"node_modules/es-toolkit": {
- "version": "1.43.0",
+ "version": "1.45.1",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz",
+ "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==",
"dev": true,
"license": "MIT",
"workspaces": [
@@ -3477,6 +4539,8 @@
},
"node_modules/escalade": {
"version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3485,6 +4549,8 @@
},
"node_modules/event-stream": {
"version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
+ "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3499,6 +4565,8 @@
},
"node_modules/execa": {
"version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
+ "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3522,27 +4590,22 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
- "node_modules/execa/node_modules/human-signals": {
- "version": "8.0.1",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/execa/node_modules/strip-final-newline": {
- "version": "4.0.0",
- "dev": true,
+ "node_modules/explain-json-schema": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/explain-json-schema/-/explain-json-schema-1.1.1.tgz",
+ "integrity": "sha512-nCKSlt2mtRzY41adcqTqjgJmQWZJeuA2HMli4fPyDoXd5zWIOTVt+hWIykSsucDTZZ8uEygkCwTYdoMt9dya/g==",
"license": "MIT",
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "minimist": "^1.2.5"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "bin": {
+ "explain-json-schema": "cli.js"
}
},
"node_modules/fast-content-type-parse": {
"version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
+ "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"dev": true,
"funding": [
{
@@ -3556,18 +4619,47 @@
],
"license": "MIT"
},
+ "node_modules/fast-string-truncated-width": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
+ "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-string-width": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz",
+ "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-string-truncated-width": "^3.0.2"
+ }
+ },
+ "node_modules/fast-wrap-ansi": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz",
+ "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-string-width": "^3.0.2"
+ }
+ },
"node_modules/fets": {
- "version": "0.8.5",
+ "version": "0.8.6",
+ "resolved": "https://registry.npmjs.org/fets/-/fets-0.8.6.tgz",
+ "integrity": "sha512-BurmiEk8hPIR6yBjrWnsaZ7lkVUp9tc10Icen0Q+SXqbE/hzdHhe0jNyeulMH3SJcsCovm+mNU3Vrfg8qNcSAw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@sinclair/typebox": "^0.34.0",
+ "@sinclair/typebox": "^0.34.48",
"@whatwg-node/cookie-store": "^0.2.0",
- "@whatwg-node/fetch": "^0.10.0",
- "@whatwg-node/server": "^0.10.0",
+ "@whatwg-node/fetch": "^0.10.12",
+ "@whatwg-node/server": "^0.10.18",
"hotscript": "^1.0.11",
"json-schema-to-ts": "^3.0.0",
- "qs": "^6.13.1",
+ "qs": "^6.14.2",
"ts-toolbelt": "^9.6.0",
"tslib": "^2.3.1"
},
@@ -3577,6 +4669,8 @@
},
"node_modules/figures": {
"version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
+ "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3591,6 +4685,8 @@
},
"node_modules/follow-redirects": {
"version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"dev": true,
"funding": [
{
@@ -3610,6 +4706,8 @@
},
"node_modules/for-each": {
"version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3624,6 +4722,8 @@
},
"node_modules/form-data": {
"version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3639,11 +4739,15 @@
},
"node_modules/from": {
"version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
+ "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==",
"dev": true,
"license": "MIT"
},
"node_modules/function-bind": {
"version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -3652,6 +4756,8 @@
},
"node_modules/functions-have-names": {
"version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
"dev": true,
"license": "MIT",
"funding": {
@@ -3660,6 +4766,8 @@
},
"node_modules/get-caller-file": {
"version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
@@ -3667,7 +4775,9 @@
}
},
"node_modules/get-east-asian-width": {
- "version": "1.4.0",
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
+ "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3679,6 +4789,8 @@
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3702,6 +4814,8 @@
},
"node_modules/get-nonce": {
"version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3709,7 +4823,9 @@
}
},
"node_modules/get-port": {
- "version": "7.1.0",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz",
+ "integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3721,6 +4837,8 @@
},
"node_modules/get-proto": {
"version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3733,6 +4851,8 @@
},
"node_modules/get-stream": {
"version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
+ "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3748,11 +4868,15 @@
},
"node_modules/get-them-args": {
"version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/get-them-args/-/get-them-args-1.3.2.tgz",
+ "integrity": "sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw==",
"dev": true,
"license": "MIT"
},
"node_modules/gopd": {
"version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3763,13 +4887,32 @@
}
},
"node_modules/graphql": {
- "version": "16.13.1",
+ "version": "16.13.2",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz",
+ "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
},
+ "node_modules/graphql-http": {
+ "version": "1.22.4",
+ "resolved": "https://registry.npmjs.org/graphql-http/-/graphql-http-1.22.4.tgz",
+ "integrity": "sha512-OC3ucK988teMf+Ak/O+ZJ0N2ukcgrEurypp8ePyJFWq83VzwRAmHxxr+XxrMpxO/FIwI4a7m/Fzv3tWGJv0wPA==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "implementations/**/*"
+ ],
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "graphql": ">=0.11 <=16"
+ }
+ },
"node_modules/graphql-ws": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.8.tgz",
@@ -3799,7 +4942,9 @@
}
},
"node_modules/graphql-yoga": {
- "version": "5.18.1",
+ "version": "5.21.0",
+ "resolved": "https://registry.npmjs.org/graphql-yoga/-/graphql-yoga-5.21.0.tgz",
+ "integrity": "sha512-PS37UoDihx8209RRl1ogttzWevNYDnGvP7beHkwHzUpUdfZTHsVRTVe1ysGXre1EjwUAePbpez302YSrq70Ngw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3823,8 +4968,31 @@
"graphql": "^15.2.0 || ^16.0.0"
}
},
+ "node_modules/handlebars": {
+ "version": "4.7.9",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
+ "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.2",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
"node_modules/has-bigints": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3836,6 +5004,8 @@
},
"node_modules/has-flag": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3844,6 +5014,8 @@
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3855,6 +5027,8 @@
},
"node_modules/has-symbols": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3866,6 +5040,8 @@
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3880,6 +5056,8 @@
},
"node_modules/hasown": {
"version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3891,19 +5069,25 @@
},
"node_modules/hotscript": {
"version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/hotscript/-/hotscript-1.0.13.tgz",
+ "integrity": "sha512-C++tTF1GqkGYecL+2S1wJTfoH6APGAsbb7PAWQ3iVIwgG/EFseAfEVOKFgAFq4yK3+6j1EjUD4UQ9dRJHX/sSQ==",
"dev": true,
"license": "ISC"
},
"node_modules/human-signals": {
- "version": "2.1.0",
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
+ "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
- "node": ">=10.17.0"
+ "node": ">=18.18.0"
}
},
"node_modules/iconv-lite": {
- "version": "0.7.1",
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3919,6 +5103,8 @@
},
"node_modules/internal-slot": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3932,6 +5118,8 @@
},
"node_modules/is-arguments": {
"version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+ "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3947,6 +5135,8 @@
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3963,6 +5153,8 @@
},
"node_modules/is-bigint": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3977,6 +5169,8 @@
},
"node_modules/is-boolean-object": {
"version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3992,6 +5186,8 @@
},
"node_modules/is-callable": {
"version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4003,6 +5199,8 @@
},
"node_modules/is-date-object": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4018,6 +5216,8 @@
},
"node_modules/is-map": {
"version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4029,6 +5229,8 @@
},
"node_modules/is-number-object": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4044,6 +5246,8 @@
},
"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==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4055,6 +5259,8 @@
},
"node_modules/is-regex": {
"version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4072,6 +5278,8 @@
},
"node_modules/is-set": {
"version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4083,6 +5291,8 @@
},
"node_modules/is-shared-array-buffer": {
"version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4097,6 +5307,8 @@
},
"node_modules/is-stream": {
"version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
+ "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4108,6 +5320,8 @@
},
"node_modules/is-string": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4123,6 +5337,8 @@
},
"node_modules/is-symbol": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4139,6 +5355,8 @@
},
"node_modules/is-unicode-supported": {
"version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+ "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4150,6 +5368,8 @@
},
"node_modules/is-weakmap": {
"version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4161,6 +5381,8 @@
},
"node_modules/is-weakset": {
"version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4176,16 +5398,38 @@
},
"node_modules/isarray": {
"version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true,
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
+ "node_modules/jest-diff": {
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz",
+ "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/diff-sequences": "30.3.0",
+ "@jest/get-type": "30.1.0",
+ "chalk": "^4.1.2",
+ "pretty-format": "30.3.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
"node_modules/joi": {
- "version": "18.0.2",
+ "version": "18.1.2",
+ "resolved": "https://registry.npmjs.org/joi/-/joi-18.1.2.tgz",
+ "integrity": "sha512-rF5MAmps5esSlhCA+N1b6IYHDw9j/btzGaqfgie522jS02Ju/HXBxamlXVlKEHAxoMKQL77HWI8jlqWsFuekZA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -4195,7 +5439,7 @@
"@hapi/pinpoint": "^2.0.1",
"@hapi/tlds": "^1.1.1",
"@hapi/topo": "^6.0.2",
- "@standard-schema/spec": "^1.0.0"
+ "@standard-schema/spec": "^1.1.0"
},
"engines": {
"node": ">= 20"
@@ -4203,6 +5447,8 @@
},
"node_modules/js-levenshtein": {
"version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
+ "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4211,6 +5457,8 @@
},
"node_modules/js-yaml": {
"version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -4221,6 +5469,8 @@
},
"node_modules/json-schema-to-ts": {
"version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
+ "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4231,8 +5481,32 @@
"node": ">=16"
}
},
+ "node_modules/json-with-bigint": {
+ "version": "3.5.8",
+ "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz",
+ "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsonschema2mk": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/jsonschema2mk/-/jsonschema2mk-2.1.1.tgz",
+ "integrity": "sha512-QNTpK8IU36rMxVMlXfzEI4s7ZLDaCzDHYX9gSjqW9AE2d7cvHFTE5JajCEHCcNVVxR/V3Z2+VIsvYTDn+X/3qg==",
+ "license": "MIT",
+ "dependencies": {
+ "explain-json-schema": "^1.1.1",
+ "handlebars": "^4.7.7",
+ "js-yaml": "^4.1.0",
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "jsonschema2mk": "cli.js"
+ }
+ },
"node_modules/kill-port-process": {
"version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/kill-port-process/-/kill-port-process-4.0.2.tgz",
+ "integrity": "sha512-fO8gc45EYJQUQWozPBmdTpsR0GDvldsmrhP2I4FPoNejwyBY4Liiwj9Is7P/5rj6k07ZQ5Ob0g0k2dqQcslW/w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -4248,6 +5522,8 @@
},
"node_modules/lazy-ass": {
"version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
+ "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4255,12 +5531,16 @@
}
},
"node_modules/lodash": {
- "version": "4.17.23",
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
@@ -4288,10 +5568,14 @@
},
"node_modules/map-stream": {
"version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
+ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==",
"dev": true
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4300,11 +5584,15 @@
},
"node_modules/merge-stream": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true,
"license": "MIT"
},
"node_modules/mime-db": {
"version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4313,6 +5601,8 @@
},
"node_modules/mime-types": {
"version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4324,6 +5614,8 @@
},
"node_modules/mimic-fn": {
"version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4332,6 +5624,8 @@
},
"node_modules/minimist": {
"version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -4339,19 +5633,31 @@
},
"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==",
"dev": true,
"license": "MIT"
},
"node_modules/mute-stream": {
"version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz",
+ "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "license": "MIT"
+ },
"node_modules/npm-run-path": {
"version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
+ "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4365,8 +5671,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4378,6 +5699,8 @@
},
"node_modules/object-is": {
"version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+ "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4393,6 +5716,8 @@
},
"node_modules/object-keys": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4401,6 +5726,8 @@
},
"node_modules/object.assign": {
"version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4420,6 +5747,8 @@
},
"node_modules/obug": {
"version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
@@ -4429,6 +5758,8 @@
},
"node_modules/onetime": {
"version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4443,6 +5774,8 @@
},
"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==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4453,18 +5786,19 @@
}
},
"node_modules/path-key": {
- "version": "4.0.0",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=8"
}
},
"node_modules/pause-stream": {
"version": "0.0.11",
+ "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
+ "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==",
"dev": true,
"license": [
"MIT",
@@ -4476,6 +5810,8 @@
},
"node_modules/pid-port": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pid-port/-/pid-port-2.0.1.tgz",
+ "integrity": "sha512-pnLo01AmMclw8l+/gfknsP2N351oe8VkVmCLFUvJZ11NRPPmghJrv0OcwsdgPQxsZkFYwm6hPWW0JKmXYCaXAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4490,14 +5826,46 @@
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
+ "node_modules/pretty-format": {
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
+ "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "30.0.5",
+ "ansi-styles": "^5.2.0",
+ "react-is": "^18.3.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/pretty-ms": {
"version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
+ "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4511,12 +5879,19 @@
}
},
"node_modules/proxy-from-env": {
- "version": "1.1.0",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
},
"node_modules/ps-tree": {
"version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz",
+ "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4531,6 +5906,8 @@
},
"node_modules/qs": {
"version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -4545,6 +5922,8 @@
},
"node_modules/radix-ui": {
"version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz",
+ "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4646,11 +6025,15 @@
},
"node_modules/react-is": {
"version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/react-remove-scroll": {
"version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4675,6 +6058,8 @@
},
"node_modules/react-remove-scroll-bar": {
"version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4696,6 +6081,8 @@
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4717,6 +6104,8 @@
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4736,6 +6125,8 @@
},
"node_modules/retry": {
"version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4748,6 +6139,8 @@
},
"node_modules/rxjs": {
"version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -4756,6 +6149,8 @@
},
"node_modules/safe-regex-test": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4772,6 +6167,8 @@
},
"node_modules/safer-buffer": {
"version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"license": "MIT"
},
@@ -4780,11 +6177,12 @@
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/semver": {
- "version": "7.7.3",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -4796,6 +6194,8 @@
},
"node_modules/set-function-length": {
"version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4812,6 +6212,8 @@
},
"node_modules/set-function-name": {
"version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4826,6 +6228,8 @@
},
"node_modules/shebang-command": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4837,6 +6241,8 @@
},
"node_modules/shebang-regex": {
"version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4845,6 +6251,8 @@
},
"node_modules/side-channel": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4863,6 +6271,8 @@
},
"node_modules/side-channel-list": {
"version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4878,6 +6288,8 @@
},
"node_modules/side-channel-map": {
"version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4895,6 +6307,8 @@
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4913,6 +6327,8 @@
},
"node_modules/signal-exit": {
"version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
@@ -4922,8 +6338,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/split": {
"version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
+ "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4935,6 +6362,8 @@
},
"node_modules/start-server-and-test": {
"version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.1.5.tgz",
+ "integrity": "sha512-A/SbXpgXE25ScSkpLLqvGvVZT0ykN6+AzS8tVqMBCTxbJy2Nwuen59opT+afalK5aS+AuQmZs0EsLwjnuDN+/g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4958,6 +6387,8 @@
},
"node_modules/start-server-and-test/node_modules/execa": {
"version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4980,6 +6411,8 @@
},
"node_modules/start-server-and-test/node_modules/get-stream": {
"version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4989,8 +6422,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/start-server-and-test/node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
"node_modules/start-server-and-test/node_modules/is-stream": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5002,6 +6447,8 @@
},
"node_modules/start-server-and-test/node_modules/npm-run-path": {
"version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5011,21 +6458,27 @@
"node": ">=8"
}
},
- "node_modules/start-server-and-test/node_modules/path-key": {
- "version": "3.1.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/start-server-and-test/node_modules/signal-exit": {
"version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true,
"license": "ISC"
},
+ "node_modules/start-server-and-test/node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5038,6 +6491,8 @@
},
"node_modules/stream-combiner": {
"version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
+ "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5046,6 +6501,8 @@
},
"node_modules/string-width": {
"version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5061,11 +6518,13 @@
}
},
"node_modules/strip-ansi": {
- "version": "7.1.2",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-regex": "^6.0.1"
+ "ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
@@ -5075,15 +6534,22 @@
}
},
"node_modules/strip-final-newline": {
- "version": "2.0.0",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
+ "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=6"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5095,16 +6561,22 @@
},
"node_modules/through": {
"version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"dev": true,
"license": "MIT"
},
"node_modules/ts-algebra": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
+ "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
"dev": true,
"license": "MIT"
},
"node_modules/ts-graphviz": {
"version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/ts-graphviz/-/ts-graphviz-1.8.2.tgz",
+ "integrity": "sha512-5YhbFoHmjxa7pgQLkB07MtGnGJ/yhvjmc9uhsnDBEICME6gkPf83SBwLDQqGDoCa3XzUMWLk1AU2Wn1u1naDtA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5117,16 +6589,23 @@
},
"node_modules/ts-toolbelt": {
"version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz",
+ "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==",
"dev": true,
"license": "Apache-2.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==",
"dev": true,
- "license": "0BSD"
+ "license": "0BSD",
+ "peer": true
},
"node_modules/typanion": {
"version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/typanion/-/typanion-3.14.0.tgz",
+ "integrity": "sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug==",
"dev": true,
"license": "MIT",
"workspaces": [
@@ -5135,6 +6614,8 @@
},
"node_modules/typescript": {
"version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -5145,6 +6626,19 @@
"node": ">=14.17"
}
},
+ "node_modules/uglify-js": {
+ "version": "3.19.3",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
@@ -5154,6 +6648,8 @@
},
"node_modules/unicorn-magic": {
"version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5165,16 +6661,22 @@
},
"node_modules/universal-user-agent": {
"version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
+ "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
"dev": true,
"license": "ISC"
},
"node_modules/urlpattern-polyfill": {
"version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz",
+ "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==",
"dev": true,
"license": "MIT"
},
"node_modules/use-callback-ref": {
"version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5195,6 +6697,8 @@
},
"node_modules/use-sidecar": {
"version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5216,6 +6720,8 @@
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -5223,7 +6729,9 @@
}
},
"node_modules/uuid": {
- "version": "9.0.1",
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
+ "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa",
@@ -5231,11 +6739,13 @@
],
"license": "MIT",
"bin": {
- "uuid": "dist/bin/uuid"
+ "uuid": "dist-node/bin/uuid"
}
},
"node_modules/wait-on": {
"version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz",
+ "integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5254,6 +6764,8 @@
},
"node_modules/which": {
"version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -5268,6 +6780,8 @@
},
"node_modules/which-boxed-primitive": {
"version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5286,6 +6800,8 @@
},
"node_modules/which-collection": {
"version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5303,6 +6819,8 @@
},
"node_modules/which-typed-array": {
"version": "1.1.20",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
+ "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5321,8 +6839,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+ "license": "MIT"
+ },
"node_modules/wrap-ansi": {
"version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5339,6 +6865,8 @@
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5350,6 +6878,8 @@
},
"node_modules/y18n": {
"version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
@@ -5358,6 +6888,8 @@
},
"node_modules/yargs": {
"version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
+ "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5374,6 +6906,8 @@
},
"node_modules/yargs-parser": {
"version": "22.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
+ "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"dev": true,
"license": "ISC",
"engines": {
@@ -5382,6 +6916,8 @@
},
"node_modules/yoctocolors": {
"version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
+ "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
"dev": true,
"license": "MIT",
"engines": {
From 5c78153cad6913c656c6b84c8bc017771d7b2bad Mon Sep 17 00:00:00 2001
From: "knope-bot[bot]" <152252888+knope-bot[bot]@users.noreply.github.com>
Date: Sun, 12 Apr 2026 19:35:06 +0300
Subject: [PATCH 17/76] chore(release): router crates and artifacts (#890)
Co-authored-by: knope-bot[bot] <152252888+knope-bot[bot]@users.noreply.github.com>
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.../replace_graphiql_with_hive_laboratory.md | 27 -------------------
Cargo.lock | 4 +--
bin/router/CHANGELOG.md | 27 +++++++++++++++++++
bin/router/Cargo.toml | 4 +--
lib/executor/Cargo.toml | 2 +-
lib/internal/Cargo.toml | 2 +-
lib/router-config/CHANGELOG.md | 27 +++++++++++++++++++
lib/router-config/Cargo.toml | 2 +-
8 files changed, 61 insertions(+), 34 deletions(-)
delete mode 100644 .changeset/replace_graphiql_with_hive_laboratory.md
diff --git a/.changeset/replace_graphiql_with_hive_laboratory.md b/.changeset/replace_graphiql_with_hive_laboratory.md
deleted file mode 100644
index 85ddaa171..000000000
--- a/.changeset/replace_graphiql_with_hive_laboratory.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-hive-router: patch
-hive-router-config: patch
----
-
-# Replace GraphiQL with Hive Laboratory
-
-The Laboratory is Hive's powerful GraphQL playground that provides a comprehensive environment for exploring, testing, and experimenting with your GraphQL APIs. Whether you're developing new queries, debugging issues, or sharing operations with your team, the Laboratory offers all the tools you need.
-
-Read more about Hive Laboratory in [the introduction blog post](https://the-guild.dev/graphql/hive/product-updates/2026-01-28-new-laboratory) or [the documentation](https://the-guild.dev/graphql/hive/docs/new-laboratory).
-
-### Breaking Changes:
-
-The top-level config option has been renamed.
-
-```diff
-- graphiql:
-+ laboratory:
- enabled: true
-```
-
-So was the environment variable override.
-
-```diff
-- GRAPHIQL_ENABLED=true
-+ LABORATORY_ENABLED=true
-```
diff --git a/Cargo.lock b/Cargo.lock
index 860693372..b3b7bdbb2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2350,7 +2350,7 @@ dependencies = [
[[package]]
name = "hive-router"
-version = "0.0.45"
+version = "0.0.46"
dependencies = [
"ahash",
"anyhow",
@@ -2406,7 +2406,7 @@ dependencies = [
[[package]]
name = "hive-router-config"
-version = "0.0.26"
+version = "0.0.27"
dependencies = [
"config",
"envconfig",
diff --git a/bin/router/CHANGELOG.md b/bin/router/CHANGELOG.md
index 966a8ae04..bb5f11422 100644
--- a/bin/router/CHANGELOG.md
+++ b/bin/router/CHANGELOG.md
@@ -116,6 +116,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Other
- *(deps)* update release-plz/action action to v0.5.113 ([#389](https://github.com/graphql-hive/router/pull/389))
+## 0.0.46 (2026-04-12)
+
+### Fixes
+
+#### Replace GraphiQL with Hive Laboratory
+
+The Laboratory is Hive's powerful GraphQL playground that provides a comprehensive environment for exploring, testing, and experimenting with your GraphQL APIs. Whether you're developing new queries, debugging issues, or sharing operations with your team, the Laboratory offers all the tools you need.
+
+Read more about Hive Laboratory in [the introduction blog post](https://the-guild.dev/graphql/hive/product-updates/2026-01-28-new-laboratory) or [the documentation](https://the-guild.dev/graphql/hive/docs/new-laboratory).
+
+#### Breaking Changes:
+
+The top-level config option has been renamed.
+
+```diff
+- graphiql:
++ laboratory:
+ enabled: true
+```
+
+So was the environment variable override.
+
+```diff
+- GRAPHIQL_ENABLED=true
++ LABORATORY_ENABLED=true
+```
+
## 0.0.45 (2026-03-31)
### Fixes
diff --git a/bin/router/Cargo.toml b/bin/router/Cargo.toml
index bfeb5521e..593a691a7 100644
--- a/bin/router/Cargo.toml
+++ b/bin/router/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router"
-version = "0.0.45"
+version = "0.0.46"
edition = "2021"
description = "GraphQL router/gateway for Federation"
license = "MIT"
@@ -23,7 +23,7 @@ testing = []
[dependencies]
hive-router-query-planner = { path = "../../lib/query-planner", version = "2.5.1" }
hive-router-plan-executor = { path = "../../lib/executor", version = "6.9.2" }
-hive-router-config = { path = "../../lib/router-config", version = "0.0.26" }
+hive-router-config = { path = "../../lib/router-config", version = "0.0.27" }
hive-router-internal = { path = "../../lib/internal", version = "0.0.15" }
hive-console-sdk = { path = "../../lib/hive-console-sdk", version = "0.3.8" }
graphql-tools = { path = "../../lib/graphql-tools", version = "0.5.3" }
diff --git a/lib/executor/Cargo.toml b/lib/executor/Cargo.toml
index 8485eaaaa..928cd7e52 100644
--- a/lib/executor/Cargo.toml
+++ b/lib/executor/Cargo.toml
@@ -16,7 +16,7 @@ doctest = false
[dependencies]
hive-router-query-planner = { path = "../query-planner", version = "2.5.1" }
-hive-router-config = { path = "../router-config", version = "0.0.26" }
+hive-router-config = { path = "../router-config", version = "0.0.27" }
hive-router-internal = { path = "../internal", version = "0.0.15" }
graphql-tools = { path = "../graphql-tools", version = "0.5.3" }
diff --git a/lib/internal/Cargo.toml b/lib/internal/Cargo.toml
index 0cf55a4c4..2a0c66f85 100644
--- a/lib/internal/Cargo.toml
+++ b/lib/internal/Cargo.toml
@@ -15,7 +15,7 @@ authors = ["The Guild"]
noop_otlp_exporter = []
[dependencies]
-hive-router-config = { path = "../router-config", version = "0.0.26" }
+hive-router-config = { path = "../router-config", version = "0.0.27" }
sonic-rs = { workspace = true }
vrl = { workspace = true }
diff --git a/lib/router-config/CHANGELOG.md b/lib/router-config/CHANGELOG.md
index 452171037..ca6a6c32b 100644
--- a/lib/router-config/CHANGELOG.md
+++ b/lib/router-config/CHANGELOG.md
@@ -66,6 +66,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- *(hive-router)* fix docker image issues ([#394](https://github.com/graphql-hive/router/pull/394))
+## 0.0.27 (2026-04-12)
+
+### Fixes
+
+#### Replace GraphiQL with Hive Laboratory
+
+The Laboratory is Hive's powerful GraphQL playground that provides a comprehensive environment for exploring, testing, and experimenting with your GraphQL APIs. Whether you're developing new queries, debugging issues, or sharing operations with your team, the Laboratory offers all the tools you need.
+
+Read more about Hive Laboratory in [the introduction blog post](https://the-guild.dev/graphql/hive/product-updates/2026-01-28-new-laboratory) or [the documentation](https://the-guild.dev/graphql/hive/docs/new-laboratory).
+
+#### Breaking Changes:
+
+The top-level config option has been renamed.
+
+```diff
+- graphiql:
++ laboratory:
+ enabled: true
+```
+
+So was the environment variable override.
+
+```diff
+- GRAPHIQL_ENABLED=true
++ LABORATORY_ENABLED=true
+```
+
## 0.0.26 (2026-03-26)
### Features
diff --git a/lib/router-config/Cargo.toml b/lib/router-config/Cargo.toml
index 44e4fa9ab..1e424306f 100644
--- a/lib/router-config/Cargo.toml
+++ b/lib/router-config/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router-config"
-version = "0.0.26"
+version = "0.0.27"
edition = "2021"
publish = true
license = "MIT"
From fd4955a8891553188c27edd84316ee869db3c019 Mon Sep 17 00:00:00 2001
From: Dotan Simha
Date: Sun, 12 Apr 2026 21:54:45 +0300
Subject: [PATCH 18/76] fix(release): fix workspace root discovery issues in
build script
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
Cargo.lock | 16 ++++++++++++++++
bin/router/Cargo.toml | 3 +++
bin/router/build.rs | 13 +++++++------
3 files changed, 26 insertions(+), 6 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index b3b7bdbb2..0f5960472 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2093,6 +2093,12 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "get_dir"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7be53a543a609b266f8ed4237af223fa96bdaa54d69fd6845c71a421a4a4748"
+
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -2401,6 +2407,7 @@ dependencies = [
"tracing-tree",
"ulid",
"vrl",
+ "workspace_root",
"xxhash-rust",
]
@@ -7810,6 +7817,15 @@ dependencies = [
"regex",
]
+[[package]]
+name = "workspace_root"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d804157714956faa87af2ccf3ea2917a7e588c84c6e0a473311c1fca161818"
+dependencies = [
+ "get_dir",
+]
+
[[package]]
name = "writeable"
version = "0.6.2"
diff --git a/bin/router/Cargo.toml b/bin/router/Cargo.toml
index 593a691a7..4d52814b1 100644
--- a/bin/router/Cargo.toml
+++ b/bin/router/Cargo.toml
@@ -20,6 +20,9 @@ path = "src/main.rs"
noop_otlp_exporter = ["hive-router-internal/noop_otlp_exporter"]
testing = []
+[build-dependencies]
+workspace_root = "0.2.0"
+
[dependencies]
hive-router-query-planner = { path = "../../lib/query-planner", version = "2.5.1" }
hive-router-plan-executor = { path = "../../lib/executor", version = "6.9.2" }
diff --git a/bin/router/build.rs b/bin/router/build.rs
index 95e3fa721..798c1fc7f 100644
--- a/bin/router/build.rs
+++ b/bin/router/build.rs
@@ -3,28 +3,29 @@ use std::{
path::{Path, PathBuf},
process::Command,
};
+use workspace_root::get_workspace_root;
fn main() {
- let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing manifest dir"));
+ let workspace_root_dir: PathBuf = get_workspace_root();
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR"));
let output_file = out_dir.join("laboratory.html");
- let product_logo = manifest_dir.join("static/product_logo.svg");
- let node_modules_dist = manifest_dir.join("../../node_modules/@graphql-hive/laboratory/dist");
+ let product_logo = workspace_root_dir.join("bin/router/static/product_logo.svg");
+ let node_modules_dist = workspace_root_dir.join("node_modules/@graphql-hive/laboratory/dist");
println!("cargo:rerun-if-changed={}", product_logo.display());
println!(
"cargo:rerun-if-changed={}",
- manifest_dir.join("../../package.json").display()
+ workspace_root_dir.join("package.json").display()
);
println!(
"cargo:rerun-if-changed={}",
- manifest_dir.join("../../package-lock.json").display()
+ workspace_root_dir.join("package-lock.json").display()
);
if !node_modules_dist.exists() {
let status = Command::new("npm")
.args(["install", "--include=dev"]) // NODE_ENV=production will skip dev deps - make sure they're in
- .current_dir(manifest_dir.join("../../"))
+ .current_dir(workspace_root_dir)
.status()
.expect("Failed to execute npm install");
From 70006ff32c4dfe9bcc99cf8e0a1bc8addf4444a7 Mon Sep 17 00:00:00 2001
From: Dotan Simha
Date: Mon, 13 Apr 2026 07:42:48 +0300
Subject: [PATCH 19/76] fix(router): move build script to the right place to
avoid publish errors
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.npmrc | 2 +
Cargo.lock | 16 -
bin/router/.npmrc | 2 +
bin/router/Cargo.toml | 3 -
bin/router/build.rs | 14 +-
bin/router/package-lock.json | 2109 ++++++++++++++++++++++++++++++++++
bin/router/package.json | 9 +
package-lock.json | 2098 +--------------------------------
package.json | 1 -
9 files changed, 2139 insertions(+), 2115 deletions(-)
create mode 100644 .npmrc
create mode 100644 bin/router/.npmrc
create mode 100644 bin/router/package-lock.json
create mode 100644 bin/router/package.json
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 000000000..09b35cdd0
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,2 @@
+audit=false
+fund=false
diff --git a/Cargo.lock b/Cargo.lock
index 0f5960472..b3b7bdbb2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2093,12 +2093,6 @@ dependencies = [
"zeroize",
]
-[[package]]
-name = "get_dir"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7be53a543a609b266f8ed4237af223fa96bdaa54d69fd6845c71a421a4a4748"
-
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -2407,7 +2401,6 @@ dependencies = [
"tracing-tree",
"ulid",
"vrl",
- "workspace_root",
"xxhash-rust",
]
@@ -7817,15 +7810,6 @@ dependencies = [
"regex",
]
-[[package]]
-name = "workspace_root"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70d804157714956faa87af2ccf3ea2917a7e588c84c6e0a473311c1fca161818"
-dependencies = [
- "get_dir",
-]
-
[[package]]
name = "writeable"
version = "0.6.2"
diff --git a/bin/router/.npmrc b/bin/router/.npmrc
new file mode 100644
index 000000000..09b35cdd0
--- /dev/null
+++ b/bin/router/.npmrc
@@ -0,0 +1,2 @@
+audit=false
+fund=false
diff --git a/bin/router/Cargo.toml b/bin/router/Cargo.toml
index 4d52814b1..593a691a7 100644
--- a/bin/router/Cargo.toml
+++ b/bin/router/Cargo.toml
@@ -20,9 +20,6 @@ path = "src/main.rs"
noop_otlp_exporter = ["hive-router-internal/noop_otlp_exporter"]
testing = []
-[build-dependencies]
-workspace_root = "0.2.0"
-
[dependencies]
hive-router-query-planner = { path = "../../lib/query-planner", version = "2.5.1" }
hive-router-plan-executor = { path = "../../lib/executor", version = "6.9.2" }
diff --git a/bin/router/build.rs b/bin/router/build.rs
index 798c1fc7f..0ad2a038d 100644
--- a/bin/router/build.rs
+++ b/bin/router/build.rs
@@ -3,29 +3,29 @@ use std::{
path::{Path, PathBuf},
process::Command,
};
-use workspace_root::get_workspace_root;
fn main() {
- let workspace_root_dir: PathBuf = get_workspace_root();
+ let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
+ println!("build script using workspace root: {:?}", manifest_dir);
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR"));
let output_file = out_dir.join("laboratory.html");
- let product_logo = workspace_root_dir.join("bin/router/static/product_logo.svg");
- let node_modules_dist = workspace_root_dir.join("node_modules/@graphql-hive/laboratory/dist");
+ let product_logo = manifest_dir.join("static/product_logo.svg");
+ let node_modules_dist = manifest_dir.join("node_modules/@graphql-hive/laboratory/dist");
println!("cargo:rerun-if-changed={}", product_logo.display());
println!(
"cargo:rerun-if-changed={}",
- workspace_root_dir.join("package.json").display()
+ manifest_dir.join("package.json").display()
);
println!(
"cargo:rerun-if-changed={}",
- workspace_root_dir.join("package-lock.json").display()
+ manifest_dir.join("package-lock.json").display()
);
if !node_modules_dist.exists() {
let status = Command::new("npm")
.args(["install", "--include=dev"]) // NODE_ENV=production will skip dev deps - make sure they're in
- .current_dir(workspace_root_dir)
+ .current_dir(manifest_dir)
.status()
.expect("Failed to execute npm install");
diff --git a/bin/router/package-lock.json b/bin/router/package-lock.json
new file mode 100644
index 000000000..1a8fdc5bc
--- /dev/null
+++ b/bin/router/package-lock.json
@@ -0,0 +1,2109 @@
+{
+ "name": "hive-router-js-deps",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "hive-router-js-deps",
+ "devDependencies": {
+ "@graphql-hive/laboratory": "0.1.2"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
+ "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@graphql-hive/laboratory": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@graphql-hive/laboratory/-/laboratory-0.1.2.tgz",
+ "integrity": "sha512-Memzux8zIWGzyY7FPHTmf6GUrnL+AixpWYOCJg5AnSfWCjakM7rc+yB4RHgcm04Gbcqy7C3Zb+CcPn7YOgNNJA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "radix-ui": "^1.4.3",
+ "uuid": "^13.0.0"
+ },
+ "peerDependencies": {
+ "@tanstack/react-form": "^1.23.8",
+ "date-fns": "^4.1.0",
+ "graphql-ws": "^6.0.6",
+ "lucide-react": "^0.548.0",
+ "lz-string": "^1.5.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0",
+ "tslib": "^2.8.1",
+ "zod": "^4.1.12"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-accessible-icon": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz",
+ "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
+ "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collapsible": "1.1.12",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-alert-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
+ "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dialog": "1.1.15",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-aspect-ratio": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz",
+ "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
+ "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
+ "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
+ "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context-menu": {
+ "version": "2.2.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz",
+ "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-form": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz",
+ "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-label": "2.1.7",
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
+ "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
+ "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menubar": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz",
+ "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-navigation-menu": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
+ "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-one-time-password-field": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz",
+ "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-password-toggle-field": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz",
+ "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-is-hydrated": "0.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
+ "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
+ "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-radio-group": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
+ "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
+ "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
+ "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slider": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
+ "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
+ "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz",
+ "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
+ "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle-group": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz",
+ "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-toggle": "1.1.10",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toolbar": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz",
+ "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-separator": "1.1.7",
+ "@radix-ui/react-toggle-group": "1.1.11"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tanstack/devtools-event-client": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.4.3.tgz",
+ "integrity": "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "intent": "bin/intent.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/form-core": {
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.29.0.tgz",
+ "integrity": "sha512-uyeKEdJBfbj0bkBSwvSYVRtWLOaXvfNX3CeVw1HqGOXVLxpBBGAqWdYLc+UoX/9xcoFwFXrjR9QqMPzvwm2yyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/devtools-event-client": "^0.4.1",
+ "@tanstack/pacer-lite": "^0.1.1",
+ "@tanstack/store": "^0.9.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/pacer-lite": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/pacer-lite/-/pacer-lite-0.1.1.tgz",
+ "integrity": "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-form": {
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.29.0.tgz",
+ "integrity": "sha512-jj425NNX0QKqbUzqSNiYI3HCPHSk2df47acXCJyXczWOTmG81ECZGkgofgqamFsSU9kMiH6Di5RLUnftrlhWSw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@tanstack/form-core": "1.29.0",
+ "@tanstack/react-store": "^0.9.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@tanstack/react-start": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tanstack/react-store": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz",
+ "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/store": "0.9.3",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@tanstack/store": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz",
+ "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/graphql": {
+ "version": "16.13.2",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz",
+ "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+ }
+ },
+ "node_modules/graphql-ws": {
+ "version": "6.0.8",
+ "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.8.tgz",
+ "integrity": "sha512-m3EOaNsUBXwAnkBWbzPfe0Nq8pXUfxsWnolC54sru3FzHvhTZL0Ouf/BoQsaGAXqM+YPerXOJ47BUnmgmoupCw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@fastify/websocket": "^10 || ^11",
+ "crossws": "~0.3",
+ "graphql": "^15.10.1 || ^16",
+ "ws": "^8"
+ },
+ "peerDependenciesMeta": {
+ "@fastify/websocket": {
+ "optional": true
+ },
+ "crossws": {
+ "optional": true
+ },
+ "ws": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.548.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.548.0.tgz",
+ "integrity": "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/radix-ui": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz",
+ "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-accessible-icon": "1.1.7",
+ "@radix-ui/react-accordion": "1.2.12",
+ "@radix-ui/react-alert-dialog": "1.1.15",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-aspect-ratio": "1.1.7",
+ "@radix-ui/react-avatar": "1.1.10",
+ "@radix-ui/react-checkbox": "1.3.3",
+ "@radix-ui/react-collapsible": "1.1.12",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-context-menu": "2.2.16",
+ "@radix-ui/react-dialog": "1.1.15",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-dropdown-menu": "2.1.16",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-form": "0.1.8",
+ "@radix-ui/react-hover-card": "1.1.15",
+ "@radix-ui/react-label": "2.1.7",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-menubar": "1.1.16",
+ "@radix-ui/react-navigation-menu": "1.2.14",
+ "@radix-ui/react-one-time-password-field": "0.1.8",
+ "@radix-ui/react-password-toggle-field": "0.1.3",
+ "@radix-ui/react-popover": "1.1.15",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-progress": "1.1.7",
+ "@radix-ui/react-radio-group": "1.3.8",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-scroll-area": "1.2.10",
+ "@radix-ui/react-select": "2.2.6",
+ "@radix-ui/react-separator": "1.1.7",
+ "@radix-ui/react-slider": "1.3.6",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-switch": "1.2.6",
+ "@radix-ui/react-tabs": "1.1.13",
+ "@radix-ui/react-toast": "1.2.15",
+ "@radix-ui/react-toggle": "1.1.10",
+ "@radix-ui/react-toggle-group": "1.1.11",
+ "@radix-ui/react-toolbar": "1.1.11",
+ "@radix-ui/react-tooltip": "1.2.8",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-escape-keydown": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
+ "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.5"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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==",
+ "dev": true,
+ "license": "0BSD",
+ "peer": true
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
+ "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/bin/router/package.json b/bin/router/package.json
new file mode 100644
index 000000000..8bce25511
--- /dev/null
+++ b/bin/router/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "hive-router-js-deps",
+ "type": "module",
+ "private": true,
+ "packageManager": "npm@11.11.1",
+ "devDependencies": {
+ "@graphql-hive/laboratory": "0.1.2"
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index a2b3cbd75..3ebab7614 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,6 @@
"bench"
],
"devDependencies": {
- "@graphql-hive/laboratory": "0.1.2",
"cross-spawn": "^7.0.6",
"qs": "^6.15.0"
}
@@ -234,48 +233,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@floating-ui/core": {
- "version": "1.7.5",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
- "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@floating-ui/utils": "^0.2.11"
- }
- },
- "node_modules/@floating-ui/dom": {
- "version": "1.7.6",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
- "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@floating-ui/core": "^1.7.5",
- "@floating-ui/utils": "^0.2.11"
- }
- },
- "node_modules/@floating-ui/react-dom": {
- "version": "2.1.8",
- "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
- "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@floating-ui/dom": "^1.7.6"
- },
- "peerDependencies": {
- "react": ">=16.8.0",
- "react-dom": ">=16.8.0"
- }
- },
- "node_modules/@floating-ui/utils": {
- "version": "0.2.11",
- "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
- "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@graphql-hive/federation-gateway-audit": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@graphql-hive/federation-gateway-audit/-/federation-gateway-audit-0.0.2.tgz",
@@ -302,28 +259,6 @@
"graphql": "*"
}
},
- "node_modules/@graphql-hive/laboratory": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/@graphql-hive/laboratory/-/laboratory-0.1.2.tgz",
- "integrity": "sha512-Memzux8zIWGzyY7FPHTmf6GUrnL+AixpWYOCJg5AnSfWCjakM7rc+yB4RHgcm04Gbcqy7C3Zb+CcPn7YOgNNJA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "radix-ui": "^1.4.3",
- "uuid": "^13.0.0"
- },
- "peerDependencies": {
- "@tanstack/react-form": "^1.23.8",
- "date-fns": "^4.1.0",
- "graphql-ws": "^6.0.6",
- "lucide-react": "^0.548.0",
- "lz-string": "^1.5.0",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0",
- "tslib": "^2.8.1",
- "zod": "^4.1.12"
- }
- },
"node_modules/@graphql-hive/router-query-planner": {
"resolved": "lib/node-addon",
"link": true
@@ -2086,1577 +2021,19 @@
"@octokit/plugin-request-log": "^6.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0"
},
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@octokit/types": {
- "version": "16.0.0",
- "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
- "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@octokit/openapi-types": "^27.0.0"
- }
- },
- "node_modules/@radix-ui/number": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
- "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@radix-ui/primitive": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
- "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@radix-ui/react-accessible-icon": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz",
- "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-visually-hidden": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-accordion": {
- "version": "1.2.12",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
- "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-collapsible": "1.1.12",
- "@radix-ui/react-collection": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-controllable-state": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-alert-dialog": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
- "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-dialog": "1.1.15",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-slot": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-arrow": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
- "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.1.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-aspect-ratio": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz",
- "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.1.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-avatar": {
- "version": "1.1.10",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
- "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-is-hydrated": "0.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-checkbox": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
- "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-use-previous": "1.1.1",
- "@radix-ui/react-use-size": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-collapsible": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
- "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-collection": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
- "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-slot": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
- "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-context": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
- "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-context-menu": {
- "version": "2.2.16",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz",
- "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-menu": "2.1.16",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
- "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.11",
- "@radix-ui/react-focus-guards": "1.1.3",
- "@radix-ui/react-focus-scope": "1.1.7",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-slot": "1.2.3",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-direction": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
- "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dismissable-layer": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
- "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-escape-keydown": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dropdown-menu": {
- "version": "2.1.16",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
- "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-menu": "2.1.16",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-controllable-state": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-focus-guards": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
- "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-focus-scope": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
- "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-form": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz",
- "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-label": "2.1.7",
- "@radix-ui/react-primitive": "2.1.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-hover-card": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
- "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.11",
- "@radix-ui/react-popper": "1.2.8",
- "@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-controllable-state": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-id": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
- "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-label": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
- "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.1.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-menu": {
- "version": "2.1.16",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
- "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-collection": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-dismissable-layer": "1.1.11",
- "@radix-ui/react-focus-guards": "1.1.3",
- "@radix-ui/react-focus-scope": "1.1.7",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-popper": "1.2.8",
- "@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-roving-focus": "1.1.11",
- "@radix-ui/react-slot": "1.2.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-menubar": {
- "version": "1.1.16",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz",
- "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-collection": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-menu": "2.1.16",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-roving-focus": "1.1.11",
- "@radix-ui/react-use-controllable-state": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-navigation-menu": {
- "version": "1.2.14",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
- "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-collection": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-dismissable-layer": "1.1.11",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-use-layout-effect": "1.1.1",
- "@radix-ui/react-use-previous": "1.1.1",
- "@radix-ui/react-visually-hidden": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-one-time-password-field": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz",
- "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/number": "1.1.1",
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-collection": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-roving-focus": "1.1.11",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-use-effect-event": "0.0.2",
- "@radix-ui/react-use-is-hydrated": "0.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-password-toggle-field": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz",
- "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-use-effect-event": "0.0.2",
- "@radix-ui/react-use-is-hydrated": "0.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-popover": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
- "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.11",
- "@radix-ui/react-focus-guards": "1.1.3",
- "@radix-ui/react-focus-scope": "1.1.7",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-popper": "1.2.8",
- "@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-slot": "1.2.3",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-popper": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
- "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@floating-ui/react-dom": "^2.0.0",
- "@radix-ui/react-arrow": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-layout-effect": "1.1.1",
- "@radix-ui/react-use-rect": "1.1.1",
- "@radix-ui/react-use-size": "1.1.1",
- "@radix-ui/rect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-portal": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
- "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-presence": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
- "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-primitive": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
- "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-slot": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-progress": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
- "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-radio-group": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
- "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-roving-focus": "1.1.11",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-use-previous": "1.1.1",
- "@radix-ui/react-use-size": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-roving-focus": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
- "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-collection": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-scroll-area": {
- "version": "1.2.10",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
- "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/number": "1.1.1",
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-select": {
- "version": "2.2.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
- "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/number": "1.1.1",
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-collection": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-dismissable-layer": "1.1.11",
- "@radix-ui/react-focus-guards": "1.1.3",
- "@radix-ui/react-focus-scope": "1.1.7",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-popper": "1.2.8",
- "@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-slot": "1.2.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-use-layout-effect": "1.1.1",
- "@radix-ui/react-use-previous": "1.1.1",
- "@radix-ui/react-visually-hidden": "1.2.3",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-separator": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
- "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.1.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-slider": {
- "version": "1.3.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
- "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/number": "1.1.1",
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-collection": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-use-layout-effect": "1.1.1",
- "@radix-ui/react-use-previous": "1.1.1",
- "@radix-ui/react-use-size": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-slot": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
- "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-switch": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
- "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-use-previous": "1.1.1",
- "@radix-ui/react-use-size": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-tabs": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
- "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-roving-focus": "1.1.11",
- "@radix-ui/react-use-controllable-state": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-toast": {
- "version": "1.2.15",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz",
- "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-collection": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.11",
- "@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-use-layout-effect": "1.1.1",
- "@radix-ui/react-visually-hidden": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-toggle": {
- "version": "1.1.10",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
- "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-use-controllable-state": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-toggle-group": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz",
- "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-roving-focus": "1.1.11",
- "@radix-ui/react-toggle": "1.1.10",
- "@radix-ui/react-use-controllable-state": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-toolbar": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz",
- "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-roving-focus": "1.1.11",
- "@radix-ui/react-separator": "1.1.7",
- "@radix-ui/react-toggle-group": "1.1.11"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-tooltip": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
- "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.11",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-popper": "1.2.8",
- "@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-slot": "1.2.3",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-visually-hidden": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-callback-ref": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
- "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-controllable-state": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
- "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-effect-event": "0.0.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-effect-event": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
- "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-escape-keydown": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
- "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-callback-ref": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-is-hydrated": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
- "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "use-sync-external-store": "^1.5.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-layout-effect": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
- "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-previous": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
- "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-rect": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
- "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/rect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-size": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
- "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-visually-hidden": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
- "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.1.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">= 20"
}
},
- "node_modules/@radix-ui/rect": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
- "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "node_modules/@octokit/types": {
+ "version": "16.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
+ "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^27.0.0"
+ }
},
"node_modules/@repeaterjs/repeater": {
"version": "3.0.6",
@@ -3699,107 +2076,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@tanstack/devtools-event-client": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.4.3.tgz",
- "integrity": "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "intent": "bin/intent.js"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@tanstack/form-core": {
- "version": "1.28.6",
- "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.28.6.tgz",
- "integrity": "sha512-4zroxL6VDj5O+w7l3dYZnUeL/h30KtNSV7UWzKAL7cl+8clMFdISPDlDlluS37As7oqvPVKo8B83VlIBvgmRog==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@tanstack/devtools-event-client": "^0.4.1",
- "@tanstack/pacer-lite": "^0.1.1",
- "@tanstack/store": "^0.9.1"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@tanstack/pacer-lite": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/@tanstack/pacer-lite/-/pacer-lite-0.1.1.tgz",
- "integrity": "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@tanstack/react-form": {
- "version": "1.28.6",
- "resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.28.6.tgz",
- "integrity": "sha512-dRxwKeNW3uuJvf0sXsIQ2compFMnIJNk9B436Lx0fqkqK+CBvA1tNmEdX+faoCpuQ5Wua3c8ahVibJ65cpkijA==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@tanstack/form-core": "1.28.6",
- "@tanstack/react-store": "^0.9.1"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- },
- "peerDependencies": {
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@tanstack/react-start": {
- "optional": true
- }
- }
- },
- "node_modules/@tanstack/react-store": {
- "version": "0.9.3",
- "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz",
- "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@tanstack/store": "0.9.3",
- "use-sync-external-store": "^1.6.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- },
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/@tanstack/store": {
- "version": "0.9.3",
- "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz",
- "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -3989,19 +2265,6 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
- "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==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/array-buffer-byte-length": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
@@ -4275,18 +2538,6 @@
"node": ">= 8"
}
},
- "node_modules/date-fns": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
- "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/kossnocorp"
- }
- },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -4384,13 +2635,6 @@
"node": ">=0.4.0"
}
},
- "node_modules/detect-node-es": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
- "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/detect-port": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-port/-/detect-port-2.1.0.tgz",
@@ -4812,16 +3056,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/get-nonce": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
- "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/get-port": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz",
@@ -4913,34 +3147,6 @@
"graphql": ">=0.11 <=16"
}
},
- "node_modules/graphql-ws": {
- "version": "6.0.8",
- "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.8.tgz",
- "integrity": "sha512-m3EOaNsUBXwAnkBWbzPfe0Nq8pXUfxsWnolC54sru3FzHvhTZL0Ouf/BoQsaGAXqM+YPerXOJ47BUnmgmoupCw==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=20"
- },
- "peerDependencies": {
- "@fastify/websocket": "^10 || ^11",
- "crossws": "~0.3",
- "graphql": "^15.10.1 || ^16",
- "ws": "^8"
- },
- "peerDependenciesMeta": {
- "@fastify/websocket": {
- "optional": true
- },
- "crossws": {
- "optional": true
- },
- "ws": {
- "optional": true
- }
- }
- },
"node_modules/graphql-yoga": {
"version": "5.21.0",
"resolved": "https://registry.npmjs.org/graphql-yoga/-/graphql-yoga-5.21.0.tgz",
@@ -5544,28 +3750,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/lucide-react": {
- "version": "0.548.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.548.0.tgz",
- "integrity": "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA==",
- "dev": true,
- "license": "ISC",
- "peer": true,
- "peerDependencies": {
- "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/lz-string": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
- "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "bin": {
- "lz-string": "bin/bin.js"
- }
- },
"node_modules/map-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
@@ -5920,109 +4104,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/radix-ui": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz",
- "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.3",
- "@radix-ui/react-accessible-icon": "1.1.7",
- "@radix-ui/react-accordion": "1.2.12",
- "@radix-ui/react-alert-dialog": "1.1.15",
- "@radix-ui/react-arrow": "1.1.7",
- "@radix-ui/react-aspect-ratio": "1.1.7",
- "@radix-ui/react-avatar": "1.1.10",
- "@radix-ui/react-checkbox": "1.3.3",
- "@radix-ui/react-collapsible": "1.1.12",
- "@radix-ui/react-collection": "1.1.7",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-context-menu": "2.2.16",
- "@radix-ui/react-dialog": "1.1.15",
- "@radix-ui/react-direction": "1.1.1",
- "@radix-ui/react-dismissable-layer": "1.1.11",
- "@radix-ui/react-dropdown-menu": "2.1.16",
- "@radix-ui/react-focus-guards": "1.1.3",
- "@radix-ui/react-focus-scope": "1.1.7",
- "@radix-ui/react-form": "0.1.8",
- "@radix-ui/react-hover-card": "1.1.15",
- "@radix-ui/react-label": "2.1.7",
- "@radix-ui/react-menu": "2.1.16",
- "@radix-ui/react-menubar": "1.1.16",
- "@radix-ui/react-navigation-menu": "1.2.14",
- "@radix-ui/react-one-time-password-field": "0.1.8",
- "@radix-ui/react-password-toggle-field": "0.1.3",
- "@radix-ui/react-popover": "1.1.15",
- "@radix-ui/react-popper": "1.2.8",
- "@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-presence": "1.1.5",
- "@radix-ui/react-primitive": "2.1.3",
- "@radix-ui/react-progress": "1.1.7",
- "@radix-ui/react-radio-group": "1.3.8",
- "@radix-ui/react-roving-focus": "1.1.11",
- "@radix-ui/react-scroll-area": "1.2.10",
- "@radix-ui/react-select": "2.2.6",
- "@radix-ui/react-separator": "1.1.7",
- "@radix-ui/react-slider": "1.3.6",
- "@radix-ui/react-slot": "1.2.3",
- "@radix-ui/react-switch": "1.2.6",
- "@radix-ui/react-tabs": "1.1.13",
- "@radix-ui/react-toast": "1.2.15",
- "@radix-ui/react-toggle": "1.1.10",
- "@radix-ui/react-toggle-group": "1.1.11",
- "@radix-ui/react-toolbar": "1.1.11",
- "@radix-ui/react-tooltip": "1.2.8",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "@radix-ui/react-use-effect-event": "0.0.2",
- "@radix-ui/react-use-escape-keydown": "1.1.1",
- "@radix-ui/react-use-is-hydrated": "0.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.1",
- "@radix-ui/react-use-size": "1.1.1",
- "@radix-ui/react-visually-hidden": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/react": {
- "version": "19.2.4",
- "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
- "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-dom": {
- "version": "19.2.4",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
- "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "scheduler": "^0.27.0"
- },
- "peerDependencies": {
- "react": "^19.2.4"
- }
- },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -6030,78 +4111,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/react-remove-scroll": {
- "version": "2.7.2",
- "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
- "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "react-remove-scroll-bar": "^2.3.7",
- "react-style-singleton": "^2.2.3",
- "tslib": "^2.1.0",
- "use-callback-ref": "^1.3.3",
- "use-sidecar": "^1.1.3"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/react-remove-scroll-bar": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
- "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "react-style-singleton": "^2.2.2",
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/react-style-singleton": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
- "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-nonce": "^1.0.0",
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -6172,13 +4181,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/scheduler": {
- "version": "0.27.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
- "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@@ -6673,75 +4675,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/use-callback-ref": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
- "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/use-sidecar": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
- "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "detect-node-es": "^1.1.0",
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/use-sync-external-store": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
- "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/uuid": {
- "version": "13.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
- "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
- "dev": true,
- "funding": [
- "https://github.com/sponsors/broofa",
- "https://github.com/sponsors/ctavan"
- ],
- "license": "MIT",
- "bin": {
- "uuid": "dist-node/bin/uuid"
- }
- },
"node_modules/wait-on": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz",
@@ -6926,17 +4859,6 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
- },
- "node_modules/zod": {
- "version": "4.3.6",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
- "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "funding": {
- "url": "https://github.com/sponsors/colinhacks"
- }
}
}
}
diff --git a/package.json b/package.json
index 6ea2a8485..e4fbe32af 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,6 @@
],
"packageManager": "npm@11.11.1",
"devDependencies": {
- "@graphql-hive/laboratory": "0.1.2",
"cross-spawn": "^7.0.6",
"qs": "^6.15.0"
},
From bbbb50e4bc51b65bfa6b4b76b8721d69f4347a4b Mon Sep 17 00:00:00 2001
From: Kamil Kisiela
Date: Sun, 12 Apr 2026 18:49:28 +0200
Subject: [PATCH 20/76] fix(query-planner): planning for conditional inline
fragments and field conditions (#894) Co-authored-by: kamilkisiela
<8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/planner_conditional.md | 12 ++
.../products-example.supergraph.graphql | 117 +++++++++++++
.../src/ast/normalization/mod.rs | 107 ++++++++++++
.../pipeline/flatten_fragments.rs | 19 ++
lib/query-planner/src/ast/selection_set.rs | 118 ++++++++++++-
lib/query-planner/src/graph/mod.rs | 10 ++
lib/query-planner/src/graph/tests.rs | 10 +-
lib/query-planner/src/tests/include_skip.rs | 162 ++++++++++++++++++
8 files changed, 548 insertions(+), 7 deletions(-)
create mode 100644 .changeset/planner_conditional.md
create mode 100644 lib/query-planner/fixture/products-example.supergraph.graphql
diff --git a/.changeset/planner_conditional.md b/.changeset/planner_conditional.md
new file mode 100644
index 000000000..1138d9d93
--- /dev/null
+++ b/.changeset/planner_conditional.md
@@ -0,0 +1,12 @@
+---
+hive-router-query-planner: patch
+node-addon: patch
+hive-router-plan-executor: patch
+hive-router: patch
+---
+
+# Fix planning for conditional inline fragments and field conditions
+
+Fixed a query-planner bug where directive-only inline fragments (using `@include`/`@skip` without an explicit type condition) could fail during normalization/planning for deeply nested operations.
+
+This update improves planner handling for conditional selections and adds regression tests to prevent these failures in the future.
diff --git a/lib/query-planner/fixture/products-example.supergraph.graphql b/lib/query-planner/fixture/products-example.supergraph.graphql
new file mode 100644
index 000000000..631e291e2
--- /dev/null
+++ b/lib/query-planner/fixture/products-example.supergraph.graphql
@@ -0,0 +1,117 @@
+schema
+ @link(url: "https://specs.apollo.dev/link/v1.0")
+ @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
+ query: Query
+}
+
+directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
+
+directive @join__field(
+ graph: join__Graph
+ requires: join__FieldSet
+ provides: join__FieldSet
+ type: String
+ external: Boolean
+ override: String
+ usedOverridden: Boolean
+) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
+
+directive @join__graph(name: String!, url: String!) on ENUM_VALUE
+
+directive @join__implements(
+ graph: join__Graph!
+ interface: String!
+) repeatable on OBJECT | INTERFACE
+
+directive @join__type(
+ graph: join__Graph!
+ key: join__FieldSet
+ extension: Boolean! = false
+ resolvable: Boolean! = true
+ isInterfaceObject: Boolean! = false
+) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
+
+directive @join__unionMember(
+ graph: join__Graph!
+ member: String!
+) repeatable on UNION
+
+directive @link(
+ url: String
+ as: String
+ for: link__Purpose
+ import: [link__Import]
+) repeatable on SCHEMA
+
+scalar join__FieldSet
+
+enum join__Graph {
+ ACCOUNTS @join__graph(name: "accounts", url: "http://0.0.0.0:4200/accounts")
+ INVENTORY
+ @join__graph(name: "inventory", url: "http://0.0.0.0:4200/inventory")
+ PRODUCTS @join__graph(name: "products", url: "http://0.0.0.0:4200/products")
+ REVIEWS @join__graph(name: "reviews", url: "http://0.0.0.0:4200/reviews")
+}
+
+scalar link__Import
+
+enum link__Purpose {
+ """
+ `SECURITY` features provide metadata necessary to securely resolve fields.
+ """
+ SECURITY
+
+ """
+ `EXECUTION` features provide metadata necessary for operation execution.
+ """
+ EXECUTION
+}
+
+type Product
+ @join__type(graph: INVENTORY, key: "upc")
+ @join__type(graph: PRODUCTS, key: "upc")
+ @join__type(graph: REVIEWS, key: "upc") {
+ upc: String!
+ weight: Int
+ @join__field(graph: INVENTORY, external: true)
+ @join__field(graph: PRODUCTS)
+ price: Int
+ @join__field(graph: INVENTORY, external: true)
+ @join__field(graph: PRODUCTS)
+ inStock: Boolean @join__field(graph: INVENTORY)
+ shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight")
+ name: String @join__field(graph: PRODUCTS)
+ reviews: [Review] @join__field(graph: REVIEWS)
+ notes: String @join__field(graph: PRODUCTS)
+ internal: String @join__field(graph: PRODUCTS)
+}
+
+type Query
+ @join__type(graph: ACCOUNTS)
+ @join__type(graph: INVENTORY)
+ @join__type(graph: PRODUCTS)
+ @join__type(graph: REVIEWS) {
+ me: User @join__field(graph: ACCOUNTS)
+ user(id: ID!): User @join__field(graph: ACCOUNTS)
+ users: [User] @join__field(graph: ACCOUNTS)
+ topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS)
+}
+
+type Review @join__type(graph: REVIEWS, key: "id") {
+ id: ID!
+ body: String
+ product: Product
+ author: User @join__field(graph: REVIEWS, provides: "username")
+}
+
+type User
+ @join__type(graph: ACCOUNTS, key: "id")
+ @join__type(graph: REVIEWS, key: "id") {
+ id: ID!
+ name: String @join__field(graph: ACCOUNTS)
+ username: String
+ @join__field(graph: ACCOUNTS)
+ @join__field(graph: REVIEWS, external: true)
+ birthday: Int @join__field(graph: ACCOUNTS)
+ reviews: [Review] @join__field(graph: REVIEWS)
+}
diff --git a/lib/query-planner/src/ast/normalization/mod.rs b/lib/query-planner/src/ast/normalization/mod.rs
index a568312a5..b730841c3 100644
--- a/lib/query-planner/src/ast/normalization/mod.rs
+++ b/lib/query-planner/src/ast/normalization/mod.rs
@@ -1145,6 +1145,113 @@ mod tests {
",
);
}
+
+ #[test]
+ fn directive_only_inline_fragment_on_abstract_parent_include() {
+ let schema_str =
+ std::fs::read_to_string("./fixture/tests/requires-with-fragments.supergraph.graphql")
+ .expect("Unable to read supergraph");
+ let schema = parse_schema(&schema_str);
+ let supergraph = SupergraphState::new(&schema);
+
+ insta::assert_snapshot!(
+ pretty_query(
+ normalize_operation(
+ &supergraph,
+ &parse_query(
+ r#"
+ query($guest: Boolean!) {
+ userFromA {
+ profile {
+ ... @include(if: $guest) {
+ displayName
+ ... on Account {
+ accountType
+ }
+ }
+ }
+ }
+ }
+ "#,
+ )
+ .expect("to parse"),
+ None,
+ )
+ .unwrap()
+ .to_string()
+ ),
+ @r"
+ query($guest: Boolean!) {
+ userFromA {
+ profile {
+ ... on AdminAccount @include(if: $guest) {
+ displayName
+ accountType
+ }
+ ... on GuestAccount @include(if: $guest) {
+ displayName
+ accountType
+ }
+ }
+ }
+ }
+ "
+ );
+ }
+
+ #[test]
+ fn directive_only_inline_fragment_on_abstract_parent_skip() {
+ let schema_str =
+ std::fs::read_to_string("./fixture/tests/requires-with-fragments.supergraph.graphql")
+ .expect("Unable to read supergraph");
+ let schema = parse_schema(&schema_str);
+ let supergraph = SupergraphState::new(&schema);
+
+ insta::assert_snapshot!(
+ pretty_query(
+ normalize_operation(
+ &supergraph,
+ &parse_query(
+ r#"
+ query($guest: Boolean!) {
+ userFromA {
+ profile {
+ ... @skip(if: $guest) {
+ displayName
+ ... on Account {
+ accountType
+ }
+ }
+ }
+ }
+ }
+ "#,
+ )
+ .expect("to parse"),
+ None,
+ )
+ .unwrap()
+ .to_string()
+ ),
+ @r"
+ query($guest: Boolean!) {
+ userFromA {
+ profile {
+ ... on AdminAccount @skip(if: $guest) {
+ displayName
+ accountType
+ }
+ ... on GuestAccount @skip(if: $guest) {
+ displayName
+ accountType
+ }
+ }
+ }
+ }
+ "
+ );
+ }
+
#[test]
fn nested_fragment_spreads_1() {
let schema_str =
diff --git a/lib/query-planner/src/ast/normalization/pipeline/flatten_fragments.rs b/lib/query-planner/src/ast/normalization/pipeline/flatten_fragments.rs
index ecc80d6a8..c0c372fa5 100644
--- a/lib/query-planner/src/ast/normalization/pipeline/flatten_fragments.rs
+++ b/lib/query-planner/src/ast/normalization/pipeline/flatten_fragments.rs
@@ -212,6 +212,7 @@ fn process_inline_fragment(
parent_type_def: &SupergraphDefinition,
mut fragment: InlineFragment<'static, String>,
) -> Result>, NormalizationError> {
+ let had_no_type_condition = fragment.type_condition.is_none();
let type_condition_matches_parent = fragment
.type_condition
.as_ref()
@@ -235,6 +236,24 @@ fn process_inline_fragment(
parent_type_def,
&mut fragment.selection_set,
)?;
+
+ if had_no_type_condition {
+ fragment.type_condition =
+ Some(TypeCondition::On(parent_type_def.name().to_string()));
+
+ if matches!(
+ parent_type_def,
+ SupergraphDefinition::Interface(_) | SupergraphDefinition::Union(_)
+ ) {
+ return expand_abstract_fragment(
+ state,
+ possible_types,
+ parent_type_def,
+ fragment,
+ );
+ }
+ }
+
Ok(vec![Selection::InlineFragment(fragment)])
}
} else {
diff --git a/lib/query-planner/src/ast/selection_set.rs b/lib/query-planner/src/ast/selection_set.rs
index b79f61ee5..050ae71c7 100644
--- a/lib/query-planner/src/ast/selection_set.rs
+++ b/lib/query-planner/src/ast/selection_set.rs
@@ -444,7 +444,12 @@ pub fn merge_selection_set(target: &mut SelectionSet, source: &SelectionSet, as_
for target_item in target.items.iter_mut() {
match (source_item, target_item) {
(SelectionItem::Field(source_field), SelectionItem::Field(target_field)) => {
- if source_field == target_field {
+ if source_field == target_field
+ && field_condition_equal(
+ &Option::::from(source_field),
+ target_field,
+ )
+ {
found = true;
merge_selection_set(
&mut target_field.selections,
@@ -458,7 +463,12 @@ pub fn merge_selection_set(target: &mut SelectionSet, source: &SelectionSet, as_
SelectionItem::InlineFragment(source_fragment),
SelectionItem::InlineFragment(target_fragment),
) => {
- if source_fragment.type_condition == target_fragment.type_condition {
+ if source_fragment.type_condition == target_fragment.type_condition
+ && fragment_condition_equal(
+ &Option::::from(source_fragment),
+ target_fragment,
+ )
+ {
found = true;
merge_selection_set(
&mut target_fragment.selections,
@@ -750,4 +760,108 @@ mod tests {
@r#"{field1(id: 1, list: [1, 2], name: "test", obj: {key: "value"})}"#
)
}
+
+ #[test]
+ fn merge_selection_set_keeps_fields_with_different_conditions_separate() {
+ let mut target = SelectionSet {
+ items: vec![SelectionItem::Field(FieldSelection {
+ name: "reviews".to_string(),
+ selections: SelectionSet::default(),
+ alias: None,
+ arguments: None,
+ skip_if: None,
+ include_if: Some("first".to_string()),
+ })],
+ };
+ let source = SelectionSet {
+ items: vec![SelectionItem::Field(FieldSelection {
+ name: "reviews".to_string(),
+ selections: SelectionSet::default(),
+ alias: None,
+ arguments: None,
+ skip_if: None,
+ include_if: Some("second".to_string()),
+ })],
+ };
+
+ merge_selection_set(&mut target, &source, false);
+
+ assert_eq!(target.items.len(), 2);
+ }
+
+ #[test]
+ fn merge_selection_set_merges_fields_with_same_condition() {
+ let mut target = SelectionSet {
+ items: vec![SelectionItem::Field(FieldSelection {
+ name: "reviews".to_string(),
+ selections: SelectionSet {
+ items: vec![SelectionItem::Field(FieldSelection {
+ name: "id".to_string(),
+ selections: SelectionSet::default(),
+ alias: None,
+ arguments: None,
+ skip_if: None,
+ include_if: None,
+ })],
+ },
+ alias: None,
+ arguments: None,
+ skip_if: None,
+ include_if: Some("cond".to_string()),
+ })],
+ };
+ let source = SelectionSet {
+ items: vec![SelectionItem::Field(FieldSelection {
+ name: "reviews".to_string(),
+ selections: SelectionSet {
+ items: vec![SelectionItem::Field(FieldSelection {
+ name: "body".to_string(),
+ selections: SelectionSet::default(),
+ alias: None,
+ arguments: None,
+ skip_if: None,
+ include_if: None,
+ })],
+ },
+ alias: None,
+ arguments: None,
+ skip_if: None,
+ include_if: Some("cond".to_string()),
+ })],
+ };
+
+ merge_selection_set(&mut target, &source, false);
+
+ assert_eq!(target.items.len(), 1);
+
+ let SelectionItem::Field(field) = &target.items[0] else {
+ panic!("expected field selection");
+ };
+
+ assert_eq!(field.selections.items.len(), 2);
+ }
+
+ #[test]
+ fn merge_selection_set_keeps_inline_fragments_with_different_conditions_separate() {
+ let mut target = SelectionSet {
+ items: vec![SelectionItem::InlineFragment(InlineFragmentSelection {
+ type_condition: "User".to_string(),
+ selections: SelectionSet::default(),
+ skip_if: None,
+ include_if: Some("first".to_string()),
+ })],
+ };
+ let source = SelectionSet {
+ items: vec![SelectionItem::InlineFragment(InlineFragmentSelection {
+ type_condition: "User".to_string(),
+ selections: SelectionSet::default(),
+ skip_if: None,
+ include_if: Some("second".to_string()),
+ })],
+ };
+
+ merge_selection_set(&mut target, &source, false);
+
+ assert_eq!(target.items.len(), 2);
+ }
}
diff --git a/lib/query-planner/src/graph/mod.rs b/lib/query-planner/src/graph/mod.rs
index 6c6b110b1..b8aa7c115 100644
--- a/lib/query-planner/src/graph/mod.rs
+++ b/lib/query-planner/src/graph/mod.rs
@@ -983,6 +983,8 @@ impl Graph {
SubgraphTypeSpecialization::Provides(view_id),
));
+ self.upsert_edge(tail, tail, Edge::Selfie(return_type_name.to_string()));
+
trace!(
"Creating viewed (#{}) field edge for '{}.{}' (type: {})",
view_id,
@@ -1042,6 +1044,8 @@ impl Graph {
SubgraphTypeSpecialization::Provides(view_id),
));
+ self.upsert_edge(tail, tail, Edge::Selfie(type_name_from_cond.to_string()));
+
// because it's abstract -> object move, add an abstract move edge
self.upsert_edge(
head,
@@ -1120,6 +1124,12 @@ impl Graph {
SubgraphTypeSpecialization::Provides(view_id),
));
+ self.upsert_edge(
+ tail,
+ tail,
+ Edge::Selfie(return_type_name.to_string()),
+ );
+
trace!(
"Creating viewed (#{}) link for provided field '{}.{}/{:?}' (type: {})",
view_id, def_name, field_name, join_type.graph_id, return_type_name
diff --git a/lib/query-planner/src/graph/tests.rs b/lib/query-planner/src/graph/tests.rs
index bfea8de64..048a4cc9b 100644
--- a/lib/query-planner/src/graph/tests.rs
+++ b/lib/query-planner/src/graph/tests.rs
@@ -369,8 +369,8 @@ mod graph_tests {
// Verify that each provided path points only to the relevant, provided fields
let (viewed_incoming, viewed_outgoing) = find_node(&graph, &node1.display_name());
viewed_outgoing.assert_field_edge("id", "String/foo");
- assert_eq!(viewed_incoming.edges.len(), 1);
- assert_eq!(viewed_outgoing.edges.len(), 2);
+ assert_eq!(viewed_incoming.edges.len(), 2); // +1 for Selfie
+ assert_eq!(viewed_outgoing.edges.len(), 3); // +1 for Selfie
let (_, to) = outgoing
.edges_field("user")
@@ -383,8 +383,8 @@ mod graph_tests {
// Verify that each provided path points only to the relevant, provided fields
let (viewed_incoming, viewed_outgoing) = find_node(&graph, &node2.display_name());
viewed_outgoing.assert_field_edge("name", "String/foo");
- assert_eq!(viewed_incoming.edges.len(), 1);
- assert_eq!(viewed_outgoing.edges.len(), 3);
+ assert_eq!(viewed_incoming.edges.len(), 2); // +1 for Selfie
+ assert_eq!(viewed_outgoing.edges.len(), 4); // +1 for Selfie
let (nested_provides_id, nested_provides_node) = viewed_outgoing
.edge_field("profile")
@@ -397,7 +397,7 @@ mod graph_tests {
.starts_with("Profile/foo/"));
let mut nested_edges = graph.edges_from(nested_provides_id);
- assert_eq!(nested_edges.clone().count(), 1);
+ assert_eq!(nested_edges.clone().count(), 2); // +1 for Selfie
assert_eq!(
nested_edges.next().unwrap().weight().display_name(),
String::from("age")
diff --git a/lib/query-planner/src/tests/include_skip.rs b/lib/query-planner/src/tests/include_skip.rs
index 6e2db65cb..fcca985c7 100644
--- a/lib/query-planner/src/tests/include_skip.rs
+++ b/lib/query-planner/src/tests/include_skip.rs
@@ -435,3 +435,165 @@ fn include_union_fragment_at_root_fetch_test() -> Result<(), Box> {
Ok(())
}
+
+#[test]
+fn plans_query_with_nested_directive_only_inline_fragments() -> Result<(), Box> {
+ init_logger();
+ let document = parse_operation(
+ r#"
+ fragment User on User {
+ id
+ username
+ name
+ }
+
+ fragment Review on Review {
+ id
+ body
+ }
+
+ fragment Product on Product {
+ inStock
+ name
+ price
+ shippingEstimate
+ upc
+ weight
+ }
+
+ query TestQuery($user: Boolean = true, $product: Boolean = true, $reviews: Boolean = true) {
+ users {
+ ...User
+ ... @include(if: $reviews) {
+ reviews {
+ ...Review
+ product {
+ ...Product
+ reviews {
+ ...Review
+ ... @include(if: $user) {
+ author {
+ ...User
+ reviews {
+ ...Review
+ ... @include(if: $product) {
+ product {
+ ...Product
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ... @include(if: $product) {
+ topProducts {
+ ...Product
+ reviews {
+ ...Review
+ author {
+ ...User
+ ... @include(if: $reviews) {
+ reviews {
+ ...Review
+ product {
+ ...Product
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ "#,
+ );
+
+ let query_plan = build_query_plan("fixture/products-example.supergraph.graphql", document);
+ assert!(
+ query_plan.is_ok(),
+ "expected query planning to succeed, got: {:?}",
+ query_plan.err()
+ );
+
+ Ok(())
+}
+
+#[test]
+fn plans_query_with_field_level_include_skip_conditions() -> Result<(), Box> {
+ init_logger();
+ let document = parse_operation(
+ r#"
+ fragment User on User {
+ id
+ username
+ name
+ }
+
+ fragment Review on Review {
+ id
+ body
+ }
+
+ fragment Product on Product {
+ inStock
+ name
+ price
+ shippingEstimate
+ upc
+ weight
+ }
+
+ query TestQuery($user: Boolean = true, $product: Boolean = true, $reviews: Boolean = true) {
+ users {
+ ...User
+ reviews @include(if: $reviews) {
+ ...Review
+ product {
+ ...Product
+ reviews {
+ ...Review
+ author @include(if: $user) {
+ ...User
+ reviews {
+ ...Review
+ product @include(if: $product) {
+ ...Product
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ topProducts @include(if: $product) {
+ ...Product
+ reviews {
+ ...Review
+ author {
+ ...User
+ reviews @include(if: $reviews) {
+ ...Review
+ product {
+ ...Product
+ }
+ }
+ }
+ }
+ }
+ }
+ "#,
+ );
+
+ let query_plan = build_query_plan("fixture/products-example.supergraph.graphql", document);
+ assert!(
+ query_plan.is_ok(),
+ "expected query planning to succeed, got: {:?}",
+ query_plan.err()
+ );
+
+ Ok(())
+}
From c789b62e97a8446fa052d777a845c121057fceef Mon Sep 17 00:00:00 2001
From: Arda TANRIKULU
Date: Sun, 12 Apr 2026 13:46:42 -0400
Subject: [PATCH 21/76] fix(executor): correct timeout error message (#901)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/fix_timeout_error.md | 8 +
.../timeout_per_subgraph_dynamic.router.yaml | 4 +-
e2e/src/timeout_per_subgraph.rs | 137 +++++++++++++++++-
lib/executor/src/executors/error.rs | 4 +-
lib/executor/src/executors/http.rs | 9 +-
5 files changed, 149 insertions(+), 13 deletions(-)
create mode 100644 .changeset/fix_timeout_error.md
diff --git a/.changeset/fix_timeout_error.md b/.changeset/fix_timeout_error.md
new file mode 100644
index 000000000..00e52d25c
--- /dev/null
+++ b/.changeset/fix_timeout_error.md
@@ -0,0 +1,8 @@
+---
+hive-router-plan-executor: patch
+hive-router: patch
+---
+
+# Fix timeout error message to include the timeout duration instead of the endpoint URL
+
+Previously by mistake, the error message for subgraph request timeouts included the endpoint URL instead of the timeout duration like `Request to subgraph timed out after http://ACCOUNT_ENDPOINT:PORT/accounts milliseconds`. This change simplifies the error message like `Request to subgraph timed out`.
\ No newline at end of file
diff --git a/e2e/configs/timeout_per_subgraph_dynamic.router.yaml b/e2e/configs/timeout_per_subgraph_dynamic.router.yaml
index fb897b843..2d246114e 100644
--- a/e2e/configs/timeout_per_subgraph_dynamic.router.yaml
+++ b/e2e/configs/timeout_per_subgraph_dynamic.router.yaml
@@ -4,7 +4,7 @@ supergraph:
path: ../supergraph.graphql
traffic_shaping:
all:
- request_timeout: 2s
+ request_timeout: 200ms
# Disable deduplication to better hunt for deadlocks in tests
dedupe_enabled: false
subgraphs:
@@ -12,7 +12,7 @@ traffic_shaping:
request_timeout:
expression: |
if (.request.headers."x-timeout" == "short") {
- "10s"
+ "1s"
} else {
.default
}
diff --git a/e2e/src/timeout_per_subgraph.rs b/e2e/src/timeout_per_subgraph.rs
index 207830bc3..05c7b78f9 100644
--- a/e2e/src/timeout_per_subgraph.rs
+++ b/e2e/src/timeout_per_subgraph.rs
@@ -1,9 +1,144 @@
#[cfg(test)]
-mod override_subgraph_urls_e2e_tests {
+mod timeout_per_subgraph_e2e_tests {
+ use std::{thread::sleep, time::Duration};
+
+ use ntex::http::StatusCode;
use sonic_rs::json;
use crate::testkit::{some_header_map, ClientResponseExt, TestRouter, TestSubgraphs};
+ const SUBGRAPH_DELAY: Duration = Duration::from_millis(300);
+
+ #[ntex::test]
+ async fn should_apply_static_subgraph_timeout_override() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_on_request(|request| {
+ if request.path == "/accounts" {
+ sleep(SUBGRAPH_DELAY);
+ }
+ None
+ })
+ .build()
+ .start()
+ .await;
+
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .file_config("configs/timeout_per_subgraph_static.router.yaml")
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request("{ users { id } }", None, None)
+ .await;
+
+ assert_eq!(res.status(), StatusCode::OK, "Expected 200 OK");
+ let expected_json = json!({
+ "data": {
+ "users": [
+ { "id": "1" },
+ { "id": "2" },
+ { "id": "3" },
+ { "id": "4" },
+ { "id": "5" },
+ { "id": "6" }
+ ]
+ }
+ });
+
+ let json_body = res.json_body().await;
+ assert_eq!(json_body, expected_json);
+ }
+
+ #[ntex::test]
+ async fn should_apply_dynamic_subgraph_timeout_override_and_fallback_to_default() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_on_request(|request| {
+ if request.path == "/accounts" {
+ sleep(SUBGRAPH_DELAY);
+ }
+ None
+ })
+ .build()
+ .start()
+ .await;
+
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .file_config("configs/timeout_per_subgraph_dynamic.router.yaml")
+ .build()
+ .start()
+ .await;
+
+ let short_timeout_res = router
+ .send_graphql_request(
+ "{ users { id } }",
+ None,
+ some_header_map! {
+ http::header::HeaderName::from_static("x-timeout") => "short"
+ },
+ )
+ .await;
+
+ assert_eq!(
+ short_timeout_res.status(),
+ StatusCode::OK,
+ "Expected 200 OK"
+ );
+
+ let expected_json = json!({
+ "data": {
+ "users": [
+ { "id": "1" },
+ { "id": "2" },
+ { "id": "3" },
+ { "id": "4" },
+ { "id": "5" },
+ { "id": "6" }
+ ]
+ }
+ });
+
+ let json_body = short_timeout_res.json_body().await;
+ assert_eq!(json_body, expected_json);
+
+ let default_timeout_res = router
+ .send_graphql_request(
+ "{ users { id } }",
+ None,
+ some_header_map! {
+ http::header::HeaderName::from_static("x-timeout") => "long"
+ },
+ )
+ .await;
+
+ assert_eq!(
+ default_timeout_res.status(),
+ StatusCode::OK,
+ "Expected 200 OK"
+ );
+ insta::assert_snapshot!(
+ default_timeout_res.json_body_string_pretty().await,
+ @r#"
+ {
+ "data": {
+ "users": null
+ },
+ "errors": [
+ {
+ "message": "Request to subgraph timed out",
+ "extensions": {
+ "code": "SUBGRAPH_REQUEST_TIMEOUT",
+ "serviceName": "accounts"
+ }
+ }
+ ]
+ }
+ "#
+ );
+ }
+
#[ntex::test]
async fn should_not_deadlock_when_overriding_subgraph_timeout_statically() {
let subgraphs = TestSubgraphs::builder().build().start().await;
diff --git a/lib/executor/src/executors/error.rs b/lib/executor/src/executors/error.rs
index 6a4541b13..e85137d2d 100644
--- a/lib/executor/src/executors/error.rs
+++ b/lib/executor/src/executors/error.rs
@@ -38,9 +38,9 @@ pub enum SubgraphExecutorError {
#[error("Failed to resolve VRL expression for timeout for subgraph. Runtime error: {0}")]
#[strum(serialize = "SUBGRAPH_TIMEOUT_EXPRESSION_RESOLUTION_FAILURE")]
TimeoutExpressionResolution(String),
- #[error("Request to subgraph timed out after {0} milliseconds")]
+ #[error("Request to subgraph timed out")]
#[strum(serialize = "SUBGRAPH_REQUEST_TIMEOUT")]
- RequestTimeout(String, u128),
+ RequestTimeout(#[from] tokio::time::error::Elapsed),
#[error("Failed to read response body from subgraph \"{0}\": {1}")]
#[strum(serialize = "SUBGRAPH_RESPONSE_BODY_READ_FAILURE")]
ResponseBodyReadFailure(String, String),
diff --git a/lib/executor/src/executors/http.rs b/lib/executor/src/executors/http.rs
index 4fb11fda0..6f92dbad0 100644
--- a/lib/executor/src/executors/http.rs
+++ b/lib/executor/src/executors/http.rs
@@ -223,14 +223,7 @@ async fn send_request<'a>(
let res_fut = http_client.request(req);
let res = if let Some(timeout_duration) = timeout {
- tokio::time::timeout(timeout_duration, res_fut)
- .await
- .map_err(|_| {
- SubgraphExecutorError::RequestTimeout(
- endpoint.to_string(),
- timeout_duration.as_millis(),
- )
- })?
+ tokio::time::timeout(timeout_duration, res_fut).await?
} else {
res_fut.await
}?;
From 9ef2f800e805b9ea4fd853dead4119baa8e68059 Mon Sep 17 00:00:00 2001
From: Dotan Simha
Date: Mon, 13 Apr 2026 08:29:56 +0300
Subject: [PATCH 22/76] chore(bump): added missing changeset to bump release
flow
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/version_bump_to_fix_release_issues.md | 9 +++++++++
1 file changed, 9 insertions(+)
create mode 100644 .changeset/version_bump_to_fix_release_issues.md
diff --git a/.changeset/version_bump_to_fix_release_issues.md b/.changeset/version_bump_to_fix_release_issues.md
new file mode 100644
index 000000000..8b86ef1bc
--- /dev/null
+++ b/.changeset/version_bump_to_fix_release_issues.md
@@ -0,0 +1,9 @@
+---
+hive-router-query-planner: patch
+hive-router-internal: patch
+hive-router: patch
+hive-router-plan-executor: patch
+hive-router-config: patch
+---
+
+# Version bump to fix release issues
From 1623897d56125d9075612977890a56e266721af1 Mon Sep 17 00:00:00 2001
From: Dotan Simha
Date: Mon, 13 Apr 2026 09:35:48 +0300
Subject: [PATCH 23/76] chore: added missing changeset
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.../replace_graphiql_with_hive_laboratory.md | 27 +++++++++++++++++++
1 file changed, 27 insertions(+)
create mode 100644 .changeset/replace_graphiql_with_hive_laboratory.md
diff --git a/.changeset/replace_graphiql_with_hive_laboratory.md b/.changeset/replace_graphiql_with_hive_laboratory.md
new file mode 100644
index 000000000..85ddaa171
--- /dev/null
+++ b/.changeset/replace_graphiql_with_hive_laboratory.md
@@ -0,0 +1,27 @@
+---
+hive-router: patch
+hive-router-config: patch
+---
+
+# Replace GraphiQL with Hive Laboratory
+
+The Laboratory is Hive's powerful GraphQL playground that provides a comprehensive environment for exploring, testing, and experimenting with your GraphQL APIs. Whether you're developing new queries, debugging issues, or sharing operations with your team, the Laboratory offers all the tools you need.
+
+Read more about Hive Laboratory in [the introduction blog post](https://the-guild.dev/graphql/hive/product-updates/2026-01-28-new-laboratory) or [the documentation](https://the-guild.dev/graphql/hive/docs/new-laboratory).
+
+### Breaking Changes:
+
+The top-level config option has been renamed.
+
+```diff
+- graphiql:
++ laboratory:
+ enabled: true
+```
+
+So was the environment variable override.
+
+```diff
+- GRAPHIQL_ENABLED=true
++ LABORATORY_ENABLED=true
+```
From 1d126613914a7b8ae39a119d647dd912f115a6fd Mon Sep 17 00:00:00 2001
From: "knope-bot[bot]" <152252888+knope-bot[bot]@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:42:50 +0300
Subject: [PATCH 24/76] chore(release): router crates and artifacts (#904)
Co-authored-by: knope-bot[bot] <152252888+knope-bot[bot]@users.noreply.github.com>
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/fix_timeout_error.md | 8 ----
.changeset/planner_conditional.md | 12 ------
.../replace_graphiql_with_hive_laboratory.md | 27 -------------
.../version_bump_to_fix_release_issues.md | 9 -----
Cargo.lock | 12 +++---
bin/router/CHANGELOG.md | 40 +++++++++++++++++++
bin/router/Cargo.toml | 10 ++---
lib/executor/CHANGELOG.md | 17 ++++++++
lib/executor/Cargo.toml | 8 ++--
lib/internal/CHANGELOG.md | 6 +++
lib/internal/Cargo.toml | 4 +-
lib/node-addon/CHANGELOG.md | 10 +++++
lib/node-addon/Cargo.toml | 2 +-
lib/node-addon/package.json | 2 +-
lib/query-planner/CHANGELOG.md | 13 ++++++
lib/query-planner/Cargo.toml | 2 +-
lib/router-config/CHANGELOG.md | 29 ++++++++++++++
lib/router-config/Cargo.toml | 2 +-
18 files changed, 136 insertions(+), 77 deletions(-)
delete mode 100644 .changeset/fix_timeout_error.md
delete mode 100644 .changeset/planner_conditional.md
delete mode 100644 .changeset/replace_graphiql_with_hive_laboratory.md
delete mode 100644 .changeset/version_bump_to_fix_release_issues.md
diff --git a/.changeset/fix_timeout_error.md b/.changeset/fix_timeout_error.md
deleted file mode 100644
index 00e52d25c..000000000
--- a/.changeset/fix_timeout_error.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-hive-router-plan-executor: patch
-hive-router: patch
----
-
-# Fix timeout error message to include the timeout duration instead of the endpoint URL
-
-Previously by mistake, the error message for subgraph request timeouts included the endpoint URL instead of the timeout duration like `Request to subgraph timed out after http://ACCOUNT_ENDPOINT:PORT/accounts milliseconds`. This change simplifies the error message like `Request to subgraph timed out`.
\ No newline at end of file
diff --git a/.changeset/planner_conditional.md b/.changeset/planner_conditional.md
deleted file mode 100644
index 1138d9d93..000000000
--- a/.changeset/planner_conditional.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-hive-router-query-planner: patch
-node-addon: patch
-hive-router-plan-executor: patch
-hive-router: patch
----
-
-# Fix planning for conditional inline fragments and field conditions
-
-Fixed a query-planner bug where directive-only inline fragments (using `@include`/`@skip` without an explicit type condition) could fail during normalization/planning for deeply nested operations.
-
-This update improves planner handling for conditional selections and adds regression tests to prevent these failures in the future.
diff --git a/.changeset/replace_graphiql_with_hive_laboratory.md b/.changeset/replace_graphiql_with_hive_laboratory.md
deleted file mode 100644
index 85ddaa171..000000000
--- a/.changeset/replace_graphiql_with_hive_laboratory.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-hive-router: patch
-hive-router-config: patch
----
-
-# Replace GraphiQL with Hive Laboratory
-
-The Laboratory is Hive's powerful GraphQL playground that provides a comprehensive environment for exploring, testing, and experimenting with your GraphQL APIs. Whether you're developing new queries, debugging issues, or sharing operations with your team, the Laboratory offers all the tools you need.
-
-Read more about Hive Laboratory in [the introduction blog post](https://the-guild.dev/graphql/hive/product-updates/2026-01-28-new-laboratory) or [the documentation](https://the-guild.dev/graphql/hive/docs/new-laboratory).
-
-### Breaking Changes:
-
-The top-level config option has been renamed.
-
-```diff
-- graphiql:
-+ laboratory:
- enabled: true
-```
-
-So was the environment variable override.
-
-```diff
-- GRAPHIQL_ENABLED=true
-+ LABORATORY_ENABLED=true
-```
diff --git a/.changeset/version_bump_to_fix_release_issues.md b/.changeset/version_bump_to_fix_release_issues.md
deleted file mode 100644
index 8b86ef1bc..000000000
--- a/.changeset/version_bump_to_fix_release_issues.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-hive-router-query-planner: patch
-hive-router-internal: patch
-hive-router: patch
-hive-router-plan-executor: patch
-hive-router-config: patch
----
-
-# Version bump to fix release issues
diff --git a/Cargo.lock b/Cargo.lock
index b3b7bdbb2..09b24214c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2350,7 +2350,7 @@ dependencies = [
[[package]]
name = "hive-router"
-version = "0.0.46"
+version = "0.0.47"
dependencies = [
"ahash",
"anyhow",
@@ -2406,7 +2406,7 @@ dependencies = [
[[package]]
name = "hive-router-config"
-version = "0.0.27"
+version = "0.0.28"
dependencies = [
"config",
"envconfig",
@@ -2428,7 +2428,7 @@ dependencies = [
[[package]]
name = "hive-router-internal"
-version = "0.0.15"
+version = "0.0.16"
dependencies = [
"ahash",
"async-trait",
@@ -2468,7 +2468,7 @@ dependencies = [
[[package]]
name = "hive-router-plan-executor"
-version = "6.9.2"
+version = "6.9.3"
dependencies = [
"ahash",
"async-trait",
@@ -2508,7 +2508,7 @@ dependencies = [
[[package]]
name = "hive-router-query-planner"
-version = "2.5.1"
+version = "2.5.2"
dependencies = [
"bitflags 2.11.0",
"criterion",
@@ -3527,7 +3527,7 @@ dependencies = [
[[package]]
name = "node-addon"
-version = "0.0.17"
+version = "0.0.18"
dependencies = [
"graphql-tools",
"hive-router-query-planner",
diff --git a/bin/router/CHANGELOG.md b/bin/router/CHANGELOG.md
index bb5f11422..365fd1424 100644
--- a/bin/router/CHANGELOG.md
+++ b/bin/router/CHANGELOG.md
@@ -116,6 +116,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Other
- *(deps)* update release-plz/action action to v0.5.113 ([#389](https://github.com/graphql-hive/router/pull/389))
+## 0.0.47 (2026-04-13)
+
+### Fixes
+
+- correct timeout error message (#901)
+- Version bump to fix release issues
+
+#### Fix timeout error message to include the timeout duration instead of the endpoint URL
+
+Previously by mistake, the error message for subgraph request timeouts included the endpoint URL instead of the timeout duration like `Request to subgraph timed out after http://ACCOUNT_ENDPOINT:PORT/accounts milliseconds`. This change simplifies the error message like `Request to subgraph timed out`.
+
+#### Fix planning for conditional inline fragments and field conditions
+
+Fixed a query-planner bug where directive-only inline fragments (using `@include`/`@skip` without an explicit type condition) could fail during normalization/planning for deeply nested operations.
+
+This update improves planner handling for conditional selections and adds regression tests to prevent these failures in the future.
+
+#### Replace GraphiQL with Hive Laboratory
+
+The Laboratory is Hive's powerful GraphQL playground that provides a comprehensive environment for exploring, testing, and experimenting with your GraphQL APIs. Whether you're developing new queries, debugging issues, or sharing operations with your team, the Laboratory offers all the tools you need.
+
+Read more about Hive Laboratory in [the introduction blog post](https://the-guild.dev/graphql/hive/product-updates/2026-01-28-new-laboratory) or [the documentation](https://the-guild.dev/graphql/hive/docs/new-laboratory).
+
+#### Breaking Changes:
+
+The top-level config option has been renamed.
+
+```diff
+- graphiql:
++ laboratory:
+ enabled: true
+```
+
+So was the environment variable override.
+
+```diff
+- GRAPHIQL_ENABLED=true
++ LABORATORY_ENABLED=true
+```
+
## 0.0.46 (2026-04-12)
### Fixes
diff --git a/bin/router/Cargo.toml b/bin/router/Cargo.toml
index 593a691a7..b53e63e80 100644
--- a/bin/router/Cargo.toml
+++ b/bin/router/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router"
-version = "0.0.46"
+version = "0.0.47"
edition = "2021"
description = "GraphQL router/gateway for Federation"
license = "MIT"
@@ -21,10 +21,10 @@ noop_otlp_exporter = ["hive-router-internal/noop_otlp_exporter"]
testing = []
[dependencies]
-hive-router-query-planner = { path = "../../lib/query-planner", version = "2.5.1" }
-hive-router-plan-executor = { path = "../../lib/executor", version = "6.9.2" }
-hive-router-config = { path = "../../lib/router-config", version = "0.0.27" }
-hive-router-internal = { path = "../../lib/internal", version = "0.0.15" }
+hive-router-query-planner = { path = "../../lib/query-planner", version = "2.5.2" }
+hive-router-plan-executor = { path = "../../lib/executor", version = "6.9.3" }
+hive-router-config = { path = "../../lib/router-config", version = "0.0.28" }
+hive-router-internal = { path = "../../lib/internal", version = "0.0.16" }
hive-console-sdk = { path = "../../lib/hive-console-sdk", version = "0.3.8" }
graphql-tools = { path = "../../lib/graphql-tools", version = "0.5.3" }
diff --git a/lib/executor/CHANGELOG.md b/lib/executor/CHANGELOG.md
index 124ca02eb..399858458 100644
--- a/lib/executor/CHANGELOG.md
+++ b/lib/executor/CHANGELOG.md
@@ -94,6 +94,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Other
- *(deps)* update release-plz/action action to v0.5.113 ([#389](https://github.com/graphql-hive/router/pull/389))
+## 6.9.3 (2026-04-13)
+
+### Fixes
+
+- correct timeout error message (#901)
+- Version bump to fix release issues
+
+#### Fix timeout error message to include the timeout duration instead of the endpoint URL
+
+Previously by mistake, the error message for subgraph request timeouts included the endpoint URL instead of the timeout duration like `Request to subgraph timed out after http://ACCOUNT_ENDPOINT:PORT/accounts milliseconds`. This change simplifies the error message like `Request to subgraph timed out`.
+
+#### Fix planning for conditional inline fragments and field conditions
+
+Fixed a query-planner bug where directive-only inline fragments (using `@include`/`@skip` without an explicit type condition) could fail during normalization/planning for deeply nested operations.
+
+This update improves planner handling for conditional selections and adds regression tests to prevent these failures in the future.
+
## 6.9.2 (2026-03-31)
### Fixes
diff --git a/lib/executor/Cargo.toml b/lib/executor/Cargo.toml
index 928cd7e52..c74589fec 100644
--- a/lib/executor/Cargo.toml
+++ b/lib/executor/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router-plan-executor"
-version = "6.9.2"
+version = "6.9.3"
edition = "2021"
description = "GraphQL query planner executor for Federation specification"
license = "MIT"
@@ -15,9 +15,9 @@ authors = ["The Guild"]
doctest = false
[dependencies]
-hive-router-query-planner = { path = "../query-planner", version = "2.5.1" }
-hive-router-config = { path = "../router-config", version = "0.0.27" }
-hive-router-internal = { path = "../internal", version = "0.0.15" }
+hive-router-query-planner = { path = "../query-planner", version = "2.5.2" }
+hive-router-config = { path = "../router-config", version = "0.0.28" }
+hive-router-internal = { path = "../internal", version = "0.0.16" }
graphql-tools = { path = "../graphql-tools", version = "0.5.3" }
async-trait = { workspace = true }
diff --git a/lib/internal/CHANGELOG.md b/lib/internal/CHANGELOG.md
index c78023173..0a6ae79fc 100644
--- a/lib/internal/CHANGELOG.md
+++ b/lib/internal/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.0.16 (2026-04-13)
+
+### Fixes
+
+- Version bump to fix release issues
+
## 0.0.15 (2026-03-26)
### Features
diff --git a/lib/internal/Cargo.toml b/lib/internal/Cargo.toml
index 2a0c66f85..3f33f3b9c 100644
--- a/lib/internal/Cargo.toml
+++ b/lib/internal/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router-internal"
-version = "0.0.15"
+version = "0.0.16"
edition = "2021"
description = "GraphQL Hive Router internal crate"
license = "MIT"
@@ -15,7 +15,7 @@ authors = ["The Guild"]
noop_otlp_exporter = []
[dependencies]
-hive-router-config = { path = "../router-config", version = "0.0.27" }
+hive-router-config = { path = "../router-config", version = "0.0.28" }
sonic-rs = { workspace = true }
vrl = { workspace = true }
diff --git a/lib/node-addon/CHANGELOG.md b/lib/node-addon/CHANGELOG.md
index 1f55ba689..db0a4bd81 100644
--- a/lib/node-addon/CHANGELOG.md
+++ b/lib/node-addon/CHANGELOG.md
@@ -1,4 +1,14 @@
# @graphql-hive/router-query-planner changelog
+## 0.0.18 (2026-04-13)
+
+### Fixes
+
+#### Fix planning for conditional inline fragments and field conditions
+
+Fixed a query-planner bug where directive-only inline fragments (using `@include`/`@skip` without an explicit type condition) could fail during normalization/planning for deeply nested operations.
+
+This update improves planner handling for conditional selections and adds regression tests to prevent these failures in the future.
+
## 0.0.17 (2026-04-01)
### Fixes
diff --git a/lib/node-addon/Cargo.toml b/lib/node-addon/Cargo.toml
index 9cae9bb25..3957552e5 100644
--- a/lib/node-addon/Cargo.toml
+++ b/lib/node-addon/Cargo.toml
@@ -1,6 +1,6 @@
[package]
edition = "2021"
-version = "0.0.17"
+version = "0.0.18"
name = "node-addon"
publish = false
diff --git a/lib/node-addon/package.json b/lib/node-addon/package.json
index 2af38147c..c88baf827 100644
--- a/lib/node-addon/package.json
+++ b/lib/node-addon/package.json
@@ -1,6 +1,6 @@
{
"name": "@graphql-hive/router-query-planner",
- "version": "0.0.17",
+ "version": "0.0.18",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/lib/query-planner/CHANGELOG.md b/lib/query-planner/CHANGELOG.md
index 630570ca7..22e4b3155 100644
--- a/lib/query-planner/CHANGELOG.md
+++ b/lib/query-planner/CHANGELOG.md
@@ -30,6 +30,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Other
- *(deps)* update release-plz/action action to v0.5.113 ([#389](https://github.com/graphql-hive/router/pull/389))
+## 2.5.2 (2026-04-13)
+
+### Fixes
+
+- planning for conditional inline fragments and field conditions (#894)
+- Version bump to fix release issues
+
+#### Fix planning for conditional inline fragments and field conditions
+
+Fixed a query-planner bug where directive-only inline fragments (using `@include`/`@skip` without an explicit type condition) could fail during normalization/planning for deeply nested operations.
+
+This update improves planner handling for conditional selections and adds regression tests to prevent these failures in the future.
+
## 2.5.1 (2026-03-31)
### Fixes
diff --git a/lib/query-planner/Cargo.toml b/lib/query-planner/Cargo.toml
index 0675deadd..24e1d4ae2 100644
--- a/lib/query-planner/Cargo.toml
+++ b/lib/query-planner/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router-query-planner"
-version = "2.5.1"
+version = "2.5.2"
edition = "2021"
description = "GraphQL query planner for Federation specification"
license = "MIT"
diff --git a/lib/router-config/CHANGELOG.md b/lib/router-config/CHANGELOG.md
index ca6a6c32b..59c0b2471 100644
--- a/lib/router-config/CHANGELOG.md
+++ b/lib/router-config/CHANGELOG.md
@@ -66,6 +66,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- *(hive-router)* fix docker image issues ([#394](https://github.com/graphql-hive/router/pull/394))
+## 0.0.28 (2026-04-13)
+
+### Fixes
+
+- Version bump to fix release issues
+
+#### Replace GraphiQL with Hive Laboratory
+
+The Laboratory is Hive's powerful GraphQL playground that provides a comprehensive environment for exploring, testing, and experimenting with your GraphQL APIs. Whether you're developing new queries, debugging issues, or sharing operations with your team, the Laboratory offers all the tools you need.
+
+Read more about Hive Laboratory in [the introduction blog post](https://the-guild.dev/graphql/hive/product-updates/2026-01-28-new-laboratory) or [the documentation](https://the-guild.dev/graphql/hive/docs/new-laboratory).
+
+#### Breaking Changes:
+
+The top-level config option has been renamed.
+
+```diff
+- graphiql:
++ laboratory:
+ enabled: true
+```
+
+So was the environment variable override.
+
+```diff
+- GRAPHIQL_ENABLED=true
++ LABORATORY_ENABLED=true
+```
+
## 0.0.27 (2026-04-12)
### Fixes
diff --git a/lib/router-config/Cargo.toml b/lib/router-config/Cargo.toml
index 1e424306f..5139a0bf1 100644
--- a/lib/router-config/Cargo.toml
+++ b/lib/router-config/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "hive-router-config"
-version = "0.0.27"
+version = "0.0.28"
edition = "2021"
publish = true
license = "MIT"
From 6e026f9e85e5bdebc6bcfafce511f8e2c4f75f7a Mon Sep 17 00:00:00 2001
From: Dotan Simha
Date: Mon, 13 Apr 2026 10:29:40 +0300
Subject: [PATCH 25/76] fix: release issues with build.rs
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
bin/router/Cargo.toml | 1 +
bin/router/build.rs | 54 +++++++++++++++++--------------------------
2 files changed, 22 insertions(+), 33 deletions(-)
diff --git a/bin/router/Cargo.toml b/bin/router/Cargo.toml
index b53e63e80..7231cc26b 100644
--- a/bin/router/Cargo.toml
+++ b/bin/router/Cargo.toml
@@ -9,6 +9,7 @@ authors = ["The Guild"]
repository = "https://github.com/graphql-hive/router"
homepage = "https://github.com/graphql-hive/router"
readme = "README.crate.md"
+exclude = ["node_modules"]
[lib]
diff --git a/bin/router/build.rs b/bin/router/build.rs
index 0ad2a038d..7e953f896 100644
--- a/bin/router/build.rs
+++ b/bin/router/build.rs
@@ -6,17 +6,31 @@ use std::{
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
- println!("build script using workspace root: {:?}", manifest_dir);
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR"));
let output_file = out_dir.join("laboratory.html");
let product_logo = manifest_dir.join("static/product_logo.svg");
- let node_modules_dist = manifest_dir.join("node_modules/@graphql-hive/laboratory/dist");
+ let node_modules_dist = out_dir.join("node_modules/@graphql-hive/laboratory/dist");
+
+ fs::copy(
+ manifest_dir.join("package.json"),
+ out_dir.join("package.json"),
+ )
+ .expect("Failed to copy package.json");
+ fs::copy(
+ manifest_dir.join("package-lock.json"),
+ out_dir.join("package-lock.json"),
+ )
+ .expect("Failed to copy package-lock.json");
println!("cargo:rerun-if-changed={}", product_logo.display());
println!(
"cargo:rerun-if-changed={}",
manifest_dir.join("package.json").display()
);
+ println!(
+ "cargo:rerun-if-changed={}",
+ manifest_dir.join("node_modules").display()
+ );
println!(
"cargo:rerun-if-changed={}",
manifest_dir.join("package-lock.json").display()
@@ -24,8 +38,11 @@ fn main() {
if !node_modules_dist.exists() {
let status = Command::new("npm")
- .args(["install", "--include=dev"]) // NODE_ENV=production will skip dev deps - make sure they're in
- .current_dir(manifest_dir)
+ .args([
+ "install",
+ "--include=dev", // NODE_ENV=production will skip dev deps - make sure they're in
+ ])
+ .current_dir(out_dir)
.status()
.expect("Failed to execute npm install");
@@ -34,35 +51,6 @@ fn main() {
}
}
- println!(
- "cargo:rerun-if-changed={}",
- node_modules_dist.join("hive-laboratory.umd.js").display()
- );
- println!(
- "cargo:rerun-if-changed={}",
- node_modules_dist
- .join("monacoeditorwork/editor.worker.bundle.js")
- .display()
- );
- println!(
- "cargo:rerun-if-changed={}",
- node_modules_dist
- .join("monacoeditorwork/graphql.worker.bundle.js")
- .display()
- );
- println!(
- "cargo:rerun-if-changed={}",
- node_modules_dist
- .join("monacoeditorwork/json.worker.bundle.js")
- .display()
- );
- println!(
- "cargo:rerun-if-changed={}",
- node_modules_dist
- .join("monacoeditorwork/ts.worker.bundle.js")
- .display()
- );
-
let html = build_inline_laboratory_html(&node_modules_dist, &product_logo);
fs::write(output_file, html).expect("failed to write generated laboratory.html");
From f427bf15a6b4d3f2b5b5983454e494a94c94be57 Mon Sep 17 00:00:00 2001
From: Dotan Simha
Date: Mon, 13 Apr 2026 10:43:26 +0300
Subject: [PATCH 26/76] chore(release): remove `scopes` from knope in order to
use changeset file only
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
knope.toml | 8 --------
1 file changed, 8 deletions(-)
diff --git a/knope.toml b/knope.toml
index 20684a8f9..c387baff2 100644
--- a/knope.toml
+++ b/knope.toml
@@ -8,42 +8,34 @@ repo = "router"
[packages.hive-router-query-planner]
versioned_files = ["lib/query-planner/Cargo.toml", { path = "lib/executor/Cargo.toml", dependency = "hive-router-query-planner" }, { path = "bin/router/Cargo.toml", dependency = "hive-router-query-planner" }, "Cargo.lock"]
-scopes = ["hive-router-query-planner", "qp", "query-planner"]
changelog = "lib/query-planner/CHANGELOG.md"
[packages.hive-router-plan-executor]
versioned_files = ["lib/executor/Cargo.toml", { path = "bin/router/Cargo.toml", dependency = "hive-router-plan-executor" }, "Cargo.lock"]
-scopes = ["hive-router-plan-executor", "executor"]
changelog = "lib/executor/CHANGELOG.md"
[packages.hive-router-config]
versioned_files = ["lib/router-config/Cargo.toml", { path = "lib/executor/Cargo.toml", dependency = "hive-router-config" }, { path = "bin/router/Cargo.toml", dependency = "hive-router-config" }, { path = "lib/internal/Cargo.toml", dependency = "hive-router-config" }, "Cargo.lock"]
-scopes = ["config"]
changelog = "lib/router-config/CHANGELOG.md"
[packages.hive-router-internal]
versioned_files = ["lib/internal/Cargo.toml", { path = "lib/executor/Cargo.toml", dependency = "hive-router-internal" }, { path = "bin/router/Cargo.toml", dependency = "hive-router-internal" }, "Cargo.lock"]
-scopes = ["internal"]
changelog = "lib/internal/CHANGELOG.md"
[packages.graphql-tools]
versioned_files = ["lib/graphql-tools/Cargo.toml", { path = "lib/executor/Cargo.toml", dependency = "graphql-tools" }, { path = "lib/query-planner/Cargo.toml", dependency = "graphql-tools" }, { path = "lib/hive-console-sdk/Cargo.toml", dependency = "graphql-tools" }, { path = "bin/router/Cargo.toml", dependency = "graphql-tools" }, "Cargo.lock"]
-scopes = ["graphql-tools"]
changelog = "lib/graphql-tools/CHANGELOG.md"
[packages.hive-console-sdk]
versioned_files = ["lib/hive-console-sdk/Cargo.toml", { path = "bin/router/Cargo.toml", dependency = "hive-console-sdk" }, "Cargo.lock"]
-scopes = ["hive-console-sdk"]
changelog = "lib/hive-console-sdk/CHANGELOG.md"
[packages.hive-router]
versioned_files = ["bin/router/Cargo.toml", "Cargo.lock"]
-scopes = ["hive-router", "router", "config", "qp", "executor", "internal", "graphql-tools"]
changelog = "bin/router/CHANGELOG.md"
[packages.node-addon]
versioned_files = ["lib/node-addon/package.json", "lib/node-addon/Cargo.toml", "Cargo.lock"]
-scopes = ["node-addon"]
changelog = "lib/node-addon/CHANGELOG.md"
# "release" pipeline that prepares the release and pushes a release PR
From ab28d3a6d80201b35f8bff10332fe9224ecd2d26 Mon Sep 17 00:00:00 2001
From: Dotan Simha
Date: Mon, 13 Apr 2026 10:46:37 +0300
Subject: [PATCH 27/76] fix: configure knope to ignore conventional_commits
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
knope.toml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/knope.toml b/knope.toml
index c387baff2..f06d964bf 100644
--- a/knope.toml
+++ b/knope.toml
@@ -1,3 +1,6 @@
+[changes]
+ignore_conventional_commits = true
+
[bot.releases]
enabled = true
pull_request.title = "chore(release): router crates and artifacts"
@@ -43,7 +46,6 @@ changelog = "lib/node-addon/CHANGELOG.md"
name = "release"
[[workflows.steps]]
type = "PrepareRelease"
- ignore_conventional_commits = true
[[workflows.steps]]
type = "Command"
command = 'git commit -m "chore: prepare releases"'
From bd2717704d0bd11ff9082389ac4685748a13dc4d Mon Sep 17 00:00:00 2001
From: Dotan Simha
Date: Mon, 13 Apr 2026 15:59:40 +0300
Subject: [PATCH 28/76] chore(release): relax knope bot check config
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
knope.toml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/knope.toml b/knope.toml
index f06d964bf..ae4c981f7 100644
--- a/knope.toml
+++ b/knope.toml
@@ -1,6 +1,9 @@
[changes]
ignore_conventional_commits = true
+[bot.checks]
+skip_labels = ["chore", "internal"]
+
[bot.releases]
enabled = true
pull_request.title = "chore(release): router crates and artifacts"
From 9e668d677064ffa37ea20620168d9aab52c9b13e Mon Sep 17 00:00:00 2001
From: Denis Badurina
Date: Mon, 13 Apr 2026 16:43:39 +0200
Subject: [PATCH 29/76] chore: More deflakeify of E2Es (#907) Co-authored-by:
kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.../timeout_per_subgraph_dynamic.router.yaml | 2 +-
..._per_subgraph_dynamic_deadlock.router.yaml | 18 ++++
e2e/src/http.rs | 54 ++----------
e2e/src/supergraph.rs | 86 ++++++++++++-------
e2e/src/testkit/mod.rs | 9 +-
e2e/src/timeout_per_subgraph.rs | 18 +---
6 files changed, 89 insertions(+), 98 deletions(-)
create mode 100644 e2e/configs/timeout_per_subgraph_dynamic_deadlock.router.yaml
diff --git a/e2e/configs/timeout_per_subgraph_dynamic.router.yaml b/e2e/configs/timeout_per_subgraph_dynamic.router.yaml
index 2d246114e..5d627398c 100644
--- a/e2e/configs/timeout_per_subgraph_dynamic.router.yaml
+++ b/e2e/configs/timeout_per_subgraph_dynamic.router.yaml
@@ -12,7 +12,7 @@ traffic_shaping:
request_timeout:
expression: |
if (.request.headers."x-timeout" == "short") {
- "1s"
+ "5s"
} else {
.default
}
diff --git a/e2e/configs/timeout_per_subgraph_dynamic_deadlock.router.yaml b/e2e/configs/timeout_per_subgraph_dynamic_deadlock.router.yaml
new file mode 100644
index 000000000..caae80d13
--- /dev/null
+++ b/e2e/configs/timeout_per_subgraph_dynamic_deadlock.router.yaml
@@ -0,0 +1,18 @@
+# yaml-language-server: $schema=../../router-config.schema.json
+supergraph:
+ source: file
+ path: ../supergraph.graphql
+traffic_shaping:
+ all:
+ request_timeout: 5s
+ # Disable deduplication to better hunt for deadlocks in tests
+ dedupe_enabled: false
+ subgraphs:
+ accounts:
+ request_timeout:
+ expression: |
+ if (.request.headers."x-timeout" == "short") {
+ "5s"
+ } else {
+ .default
+ }
diff --git a/e2e/src/http.rs b/e2e/src/http.rs
index cce589075..488591c6f 100644
--- a/e2e/src/http.rs
+++ b/e2e/src/http.rs
@@ -1,9 +1,6 @@
#[cfg(test)]
mod http_tests {
- use std::{
- thread::sleep,
- time::{Duration, Instant},
- };
+ use std::time::{Duration, Instant};
use futures::{stream::FuturesUnordered, StreamExt};
use hive_router::pipeline::execution::EXPOSE_QUERY_PLAN_HEADER;
@@ -218,12 +215,7 @@ mod http_tests {
#[ntex::test]
async fn should_not_dedupe_inflight_router_requests_by_default() {
let subgraphs = TestSubgraphs::builder()
- .with_on_request(|request| {
- if request.path == "/products" {
- sleep(Duration::from_millis(50));
- }
- None
- })
+ .with_delay(Duration::from_millis(100))
.build()
.start()
.await;
@@ -283,12 +275,7 @@ mod http_tests {
#[ntex::test]
async fn should_dedupe_inflight_router_requests_when_enabled() {
let subgraphs = TestSubgraphs::builder()
- .with_on_request(|request| {
- if request.path == "/products" {
- sleep(Duration::from_millis(50));
- }
- None
- })
+ .with_delay(Duration::from_millis(100))
.build()
.start()
.await;
@@ -351,12 +338,7 @@ mod http_tests {
#[ntex::test]
async fn should_not_dedupe_inflight_router_mutation_requests() {
let subgraphs = TestSubgraphs::builder()
- .with_on_request(|request| {
- if request.path == "/products" {
- sleep(Duration::from_millis(50));
- }
- None
- })
+ .with_delay(Duration::from_millis(100))
.build()
.start()
.await;
@@ -415,12 +397,7 @@ mod http_tests {
#[ntex::test]
async fn should_not_share_inflight_dedupe_entry_across_schema_reload() {
let subgraphs = TestSubgraphs::builder()
- .with_on_request(|request| {
- if request.path == "/products" {
- sleep(Duration::from_millis(1500));
- }
- None
- })
+ .with_delay(Duration::from_millis(100))
.build()
.start()
.await;
@@ -529,12 +506,7 @@ mod http_tests {
#[ntex::test]
async fn should_use_all_headers_in_router_dedupe_key_by_default() {
let subgraphs = TestSubgraphs::builder()
- .with_on_request(|request| {
- if request.path == "/products" {
- sleep(Duration::from_millis(50));
- }
- None
- })
+ .with_delay(Duration::from_millis(100))
.build()
.start()
.await;
@@ -607,12 +579,7 @@ mod http_tests {
#[ntex::test]
async fn should_ignore_headers_in_router_dedupe_key_when_headers_is_empty() {
let subgraphs = TestSubgraphs::builder()
- .with_on_request(|request| {
- if request.path == "/products" {
- sleep(Duration::from_millis(50));
- }
- None
- })
+ .with_delay(Duration::from_millis(100))
.build()
.start()
.await;
@@ -686,12 +653,7 @@ mod http_tests {
#[ntex::test]
async fn should_use_case_insensitive_header_allowlist_in_router_dedupe_key() {
let subgraphs = TestSubgraphs::builder()
- .with_on_request(|request| {
- if request.path == "/products" {
- sleep(Duration::from_millis(50));
- }
- None
- })
+ .with_delay(Duration::from_millis(100))
.build()
.start()
.await;
diff --git a/e2e/src/supergraph.rs b/e2e/src/supergraph.rs
index 16435f5c1..44598274a 100644
--- a/e2e/src/supergraph.rs
+++ b/e2e/src/supergraph.rs
@@ -2,7 +2,6 @@
mod supergraph_e2e_tests {
use std::time::Duration;
- use hive_router::invoke_shutdown_hooks;
use sonic_rs::JsonValueTrait;
use crate::testkit::{wait_until_mock_matched, ClientResponseExt, TestRouter, TestSubgraphs};
@@ -26,38 +25,41 @@ mod supergraph_e2e_tests {
source: hive
endpoint: http://{host}/supergraph
key: dummy_key
- poll_interval: 500ms
+ poll_interval: 300ms
"#,
))
.build()
.start()
.await;
- assert_eq!(router.schema_state().plan_cache.entry_count(), 0);
- assert_eq!(router.schema_state().normalize_cache.entry_count(), 0);
-
let res = router
.send_graphql_request("{ __schema { types { name } } }", None, None)
.await;
-
assert!(res.status().is_success(), "Expected 200 OK");
- // Flush the caches
- router
- .schema_state()
- .normalize_cache
- .run_pending_tasks()
- .await;
- router.schema_state().plan_cache.run_pending_tasks().await;
- invoke_shutdown_hooks(router.shared_state()).await;
-
- ntex::time::sleep(Duration::from_millis(100)).await;
-
- // Now it should have the record
- assert_eq!(router.schema_state().plan_cache.entry_count(), 1);
- assert_eq!(router.schema_state().normalize_cache.entry_count(), 1);
+ // wait for caches to populate
+ let deadline = std::time::Instant::now() + Duration::from_secs(5);
+ loop {
+ router
+ .schema_state()
+ .normalize_cache
+ .run_pending_tasks()
+ .await;
+ router.schema_state().plan_cache.run_pending_tasks().await;
+ if router.schema_state().plan_cache.entry_count() >= 1
+ && router.schema_state().normalize_cache.entry_count() >= 1
+ {
+ break;
+ }
+ assert!(
+ std::time::Instant::now() < deadline,
+ "timed out waiting for caches to populate: plan={}, normalize={}",
+ router.schema_state().plan_cache.entry_count(),
+ router.schema_state().normalize_cache.entry_count()
+ );
+ ntex::time::sleep(Duration::from_millis(100)).await;
+ }
- // Remove the first mock and register the new supergraph so the poller picks it up
mock1.remove();
let mock2 = server
.mock("GET", "/supergraph")
@@ -67,22 +69,35 @@ mod supergraph_e2e_tests {
.with_body("type Query { dummyNew: NewType } type NewType { id: ID! }")
.create();
- // Wait for the poller to pick up the new supergraph
wait_until_mock_matched(&mock2)
.await
.expect("Expected mock2 to be matched");
- router
- .schema_state()
- .normalize_cache
- .run_pending_tasks()
- .await;
- router.schema_state().plan_cache.run_pending_tasks().await;
- invoke_shutdown_hooks(router.shared_state()).await;
-
- // Now cache should be empty again, if supergraph has changes
- assert_eq!(router.schema_state().plan_cache.entry_count(), 0);
- assert_eq!(router.schema_state().normalize_cache.entry_count(), 0);
+ // wait for the router to finish rebuilding with the new supergraph
+ router.wait_for_ready(None).await;
+
+ // wait for cache invalidation to be reflected (moka invalidate_all is lazy)
+ let deadline = std::time::Instant::now() + Duration::from_secs(5);
+ loop {
+ router
+ .schema_state()
+ .normalize_cache
+ .run_pending_tasks()
+ .await;
+ router.schema_state().plan_cache.run_pending_tasks().await;
+ if router.schema_state().plan_cache.entry_count() == 0
+ && router.schema_state().normalize_cache.entry_count() == 0
+ {
+ break;
+ }
+ assert!(
+ std::time::Instant::now() < deadline,
+ "timed out waiting for caches to clear: plan={}, normalize={}",
+ router.schema_state().plan_cache.entry_count(),
+ router.schema_state().normalize_cache.entry_count()
+ );
+ ntex::time::sleep(Duration::from_millis(100)).await;
+ }
}
/// In this test we are testing that the supergraph is not changed for in-flight requests.
@@ -239,6 +254,11 @@ mod supergraph_e2e_tests {
.await
.expect("Expected mock2 to be matched");
+ // wait for the router to finish applying the new supergraph state before asserting;
+ // the mock being matched only means the poller fetched the sdl, not that the router
+ // has finished rebuilding its query planner
+ router.wait_for_ready(None).await;
+
let res_new_supergraph = router
.send_graphql_request("{ users { id name reviews { id body } } }", None, None)
.await;
diff --git a/e2e/src/testkit/mod.rs b/e2e/src/testkit/mod.rs
index d74727052..2e83fbd8f 100644
--- a/e2e/src/testkit/mod.rs
+++ b/e2e/src/testkit/mod.rs
@@ -382,10 +382,6 @@ impl TestSubgraphs {
let middleware_state = Arc::new(TestSubgraphsMiddlewareState {
request_log: DashMap::new(),
});
- app = app.layer(axum::middleware::from_fn_with_state(
- middleware_state.clone(),
- record_requests,
- ));
if let Some(on_request) = self.on_request.clone() {
app = app.layer(axum::middleware::from_fn_with_state(
on_request,
@@ -400,6 +396,11 @@ impl TestSubgraphs {
},
));
}
+ // record_requests must be outermost so it logs the request before any blocking on_request handler runs
+ app = app.layer(axum::middleware::from_fn_with_state(
+ middleware_state.clone(),
+ record_requests,
+ ));
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
tokio::spawn(async move {
diff --git a/e2e/src/timeout_per_subgraph.rs b/e2e/src/timeout_per_subgraph.rs
index 05c7b78f9..27c6bd4ba 100644
--- a/e2e/src/timeout_per_subgraph.rs
+++ b/e2e/src/timeout_per_subgraph.rs
@@ -1,6 +1,6 @@
#[cfg(test)]
mod timeout_per_subgraph_e2e_tests {
- use std::{thread::sleep, time::Duration};
+ use std::time::Duration;
use ntex::http::StatusCode;
use sonic_rs::json;
@@ -12,12 +12,7 @@ mod timeout_per_subgraph_e2e_tests {
#[ntex::test]
async fn should_apply_static_subgraph_timeout_override() {
let subgraphs = TestSubgraphs::builder()
- .with_on_request(|request| {
- if request.path == "/accounts" {
- sleep(SUBGRAPH_DELAY);
- }
- None
- })
+ .with_delay(SUBGRAPH_DELAY)
.build()
.start()
.await;
@@ -54,12 +49,7 @@ mod timeout_per_subgraph_e2e_tests {
#[ntex::test]
async fn should_apply_dynamic_subgraph_timeout_override_and_fallback_to_default() {
let subgraphs = TestSubgraphs::builder()
- .with_on_request(|request| {
- if request.path == "/accounts" {
- sleep(SUBGRAPH_DELAY);
- }
- None
- })
+ .with_delay(SUBGRAPH_DELAY)
.build()
.start()
.await;
@@ -185,7 +175,7 @@ mod timeout_per_subgraph_e2e_tests {
let subgraphs = TestSubgraphs::builder().build().start().await;
let router = TestRouter::builder()
.with_subgraphs(&subgraphs)
- .file_config("configs/timeout_per_subgraph_dynamic.router.yaml")
+ .file_config("configs/timeout_per_subgraph_dynamic_deadlock.router.yaml")
.build()
.start()
.await;
From 3e51da45e6a0907b1d113210517e4554132d3a8c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 14 Apr 2026 04:41:48 +0300
Subject: [PATCH 30/76] chore(deps): bump rand from 0.10.0 to 0.10.1 in the
cargo group across 1 directory (#908)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps the cargo group with 1 update in the / directory:
[rand](https://github.com/rust-random/rand).
Updates `rand` from 0.10.0 to 0.10.1
Changelog
Sourced from rand's
changelog .
[0.10.1] — 2026-02-11
This release includes a fix for a soundness bug; see #1763 .
Changes
Document panic behavior of make_rng and add
#[track_caller] (#1761 )
Deprecate feature log (#1763 )
#1761 :
rust-random/rand#1761
#1763 :
rust-random/rand#1763
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/graphql-hive/router/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
Cargo.lock | 8 ++++----
Cargo.toml | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 09b24214c..45d321bc0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2382,7 +2382,7 @@ dependencies = [
"moka",
"ntex",
"prometheus",
- "rand 0.10.0",
+ "rand 0.10.1",
"regex-automata",
"reqwest",
"reqwest-middleware",
@@ -5018,9 +5018,9 @@ dependencies = [
[[package]]
name = "rand"
-version = "0.10.0"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
+checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20 0.10.0",
"getrandom 0.4.1",
@@ -6299,7 +6299,7 @@ dependencies = [
"async-graphql-axum",
"axum",
"lazy_static",
- "rand 0.10.0",
+ "rand 0.10.1",
"tokio",
]
diff --git a/Cargo.toml b/Cargo.toml
index 7c6a3addd..c05b29c29 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -64,7 +64,7 @@ thiserror = "2.0.14"
xxhash-rust = { version = "0.8.15", features = ["xxh3"] }
tokio = { version = "1.47.1", features = ["full"] }
tokio-util = { version = "0.7.16" }
-rand = "0.10.0"
+rand = "0.10.1"
jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] }
ntex = { version = "3.7.2", features = ["tokio"] }
tonic = { version = "0.14.2", features = ["tls-aws-lc"] }
From 83b15430dcdb77bc5b230f963707918e333c472e Mon Sep 17 00:00:00 2001
From: Denis Badurina
Date: Tue, 14 Apr 2026 18:22:18 +0200
Subject: [PATCH 31/76] Subscriptions (#620)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Arda TANRIKULU
Co-authored-by: theguild-bot
Co-authored-by: kamilkisiela <8167190+kamilkisiela@users.noreply.github.com>
---
.changeset/federated_graphql_subscriptions.md | 17 +
.changeset/query_plan_subscriptions_node.md | 8 +
Cargo.lock | 31 +
Cargo.toml | 3 +-
bench/subgraphs/Cargo.toml | 12 +
bench/subgraphs/graphql_with_subscriptions.rs | 456 ++++
bench/subgraphs/lib.rs | 28 +-
bench/subgraphs/reviews.rs | 79 +-
bench/supergraph.graphql | 8 +
bin/router/Cargo.toml | 5 +
bin/router/src/error.rs | 2 +
bin/router/src/lib.rs | 133 +-
.../src/pipeline/active_subscriptions.rs | 98 +
bin/router/src/pipeline/error.rs | 80 +-
bin/router/src/pipeline/execution.rs | 57 +-
bin/router/src/pipeline/http_callback.rs | 251 +++
.../src/pipeline/long_lived_client_limit.rs | 191 ++
bin/router/src/pipeline/mod.rs | 293 +--
.../src/pipeline/multipart_subscribe.rs | 133 ++
bin/router/src/pipeline/sse.rs | 48 +
bin/router/src/pipeline/websocket_server.rs | 754 +++++++
bin/router/src/schema_state.rs | 116 +-
bin/router/src/shared_state.rs | 156 +-
bin/router/src/telemetry.rs | 4 +-
docs/README.md | 182 +-
e2e/configs/header_propagation.router.yaml | 4 +-
e2e/src/file_supergraph.rs | 2 +-
e2e/src/hive_cdn_supergraph.rs | 7 +-
e2e/src/http.rs | 6 +-
e2e/src/http_callback.rs | 338 +++
e2e/src/lib.rs | 6 +
e2e/src/subscriptions.rs | 1829 +++++++++++++++++
e2e/src/supergraph.rs | 44 +-
e2e/src/testkit/mod.rs | 162 +-
e2e/src/testkit/otel.rs | 6 +-
e2e/src/websocket.rs | 478 +++++
e2e/supergraph.graphql | 10 +
lib/executor/Cargo.toml | 4 +
.../src/execution/client_request_details.rs | 6 +-
lib/executor/src/execution/plan.rs | 319 ++-
lib/executor/src/executors/common.rs | 13 +-
lib/executor/src/executors/error.rs | 41 +-
.../src/executors/graphql_transport_ws.rs | 433 ++++
lib/executor/src/executors/http.rs | 283 ++-
lib/executor/src/executors/http_callback.rs | 283 +++
lib/executor/src/executors/map.rs | 322 ++-
lib/executor/src/executors/mod.rs | 7 +
.../src/executors/multipart_subscribe.rs | 627 ++++++
lib/executor/src/executors/sse.rs | 381 ++++
lib/executor/src/executors/websocket.rs | 196 ++
.../src/executors/websocket_client.rs | 480 +++++
.../src/executors/websocket_common.rs | 200 ++
lib/executor/src/headers/mod.rs | 24 +-
lib/executor/src/introspection/resolve.rs | 37 +-
.../src/plugins/hooks/on_supergraph_load.rs | 4 +-
.../src/response/subgraph_response.rs | 29 +-
lib/internal/src/inflight.rs | 44 +-
lib/query-planner/src/planner/plan_nodes.rs | 34 +-
.../src/planner/query_plan/optimize.rs | 5 +-
lib/router-config/Cargo.toml | 3 +-
lib/router-config/src/env_overrides.rs | 16 +
lib/router-config/src/http_server.rs | 24 +-
lib/router-config/src/lib.rs | 42 +
.../src/primitives/absolute_path.rs | 107 +
lib/router-config/src/primitives/mod.rs | 1 +
lib/router-config/src/subscriptions.rs | 206 ++
lib/router-config/src/traffic_shaping.rs | 49 +-
lib/router-config/src/websocket.rs | 118 ++
68 files changed, 9826 insertions(+), 549 deletions(-)
create mode 100644 .changeset/federated_graphql_subscriptions.md
create mode 100644 .changeset/query_plan_subscriptions_node.md
create mode 100644 bench/subgraphs/graphql_with_subscriptions.rs
create mode 100644 bin/router/src/pipeline/active_subscriptions.rs
create mode 100644 bin/router/src/pipeline/http_callback.rs
create mode 100644 bin/router/src/pipeline/long_lived_client_limit.rs
create mode 100644 bin/router/src/pipeline/multipart_subscribe.rs
create mode 100644 bin/router/src/pipeline/sse.rs
create mode 100644 bin/router/src/pipeline/websocket_server.rs
create mode 100644 e2e/src/http_callback.rs
create mode 100644 e2e/src/subscriptions.rs
create mode 100644 e2e/src/websocket.rs
create mode 100644 lib/executor/src/executors/graphql_transport_ws.rs
create mode 100644 lib/executor/src/executors/http_callback.rs
create mode 100644 lib/executor/src/executors/multipart_subscribe.rs
create mode 100644 lib/executor/src/executors/sse.rs
create mode 100644 lib/executor/src/executors/websocket.rs
create mode 100644 lib/executor/src/executors/websocket_client.rs
create mode 100644 lib/executor/src/executors/websocket_common.rs
create mode 100644 lib/router-config/src/primitives/absolute_path.rs
create mode 100644 lib/router-config/src/subscriptions.rs
create mode 100644 lib/router-config/src/websocket.rs
diff --git a/.changeset/federated_graphql_subscriptions.md b/.changeset/federated_graphql_subscriptions.md
new file mode 100644
index 000000000..f298883d9
--- /dev/null
+++ b/.changeset/federated_graphql_subscriptions.md
@@ -0,0 +1,17 @@
+---
+hive-router: minor
+hive-router-config: minor
+hive-router-plan-executor: minor
+---
+
+# Federated GraphQL Subscriptions
+
+Hive Router now supports federated GraphQL subscriptions with full protocol coverage across [SSE](https://the-guild.dev/graphql/hive/docs/router/subscriptions/sse), [WebSockets](https://the-guild.dev/graphql/hive/docs/router/subscriptions/websockets), [Multipart HTTP](https://the-guild.dev/graphql/hive/docs/router/subscriptions/multipart-http), [Incremental Delivery](https://the-guild.dev/graphql/hive/docs/router/subscriptions/incremental-delivery), and [HTTP Callback](https://the-guild.dev/graphql/hive/docs/router/subscriptions/http-callback) - for both client-to-router and router-to-subgraph communication. Subscription events spanning multiple subgraphs are resolved automatically: when a subscription field lives in one subgraph but the response includes entity fields owned by others, the router fetches those on every event with no extra configuration.
+
+- [Read the product update](https://the-guild.dev/graphql/hive/product-updates/2026-04-14-hive-router-subscriptions)
+- [Subscriptions overview](https://the-guild.dev/graphql/hive/docs/router/subscriptions)
+- [Server-Sent Events](https://the-guild.dev/graphql/hive/docs/router/subscriptions/sse)
+- [Incremental Delivery over HTTP](https://the-guild.dev/graphql/hive/docs/router/subscriptions/incremental-delivery)
+- [Multipart HTTP](https://the-guild.dev/graphql/hive/docs/router/subscriptions/multipart-http)
+- [WebSockets](https://the-guild.dev/graphql/hive/docs/router/subscriptions/websockets)
+- [HTTP Callback](https://the-guild.dev/graphql/hive/docs/router/subscriptions/http-callback)
diff --git a/.changeset/query_plan_subscriptions_node.md b/.changeset/query_plan_subscriptions_node.md
new file mode 100644
index 000000000..0d05d59b2
--- /dev/null
+++ b/.changeset/query_plan_subscriptions_node.md
@@ -0,0 +1,8 @@
+---
+hive-router-query-planner: minor
+node-addon: minor
+---
+
+# Query Plan Subscriptions Node
+
+The query planner now emits a `Subscription` node when planning a subscription operation. The `Subscription` node contains a `primary` fetch that is sent to the subgraph owning the subscription field.
diff --git a/Cargo.lock b/Cargo.lock
index 45d321bc0..7dc7ddf40 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -995,6 +995,12 @@ dependencies = [
"tiny-keccak",
]
+[[package]]
+name = "const-str"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f"
+
[[package]]
name = "context-data-plugin-example"
version = "0.0.1"
@@ -2355,12 +2361,17 @@ dependencies = [
"ahash",
"anyhow",
"arc-swap",
+ "async-stream",
"async-trait",
+ "bytes",
"combine",
+ "const-str",
"cookie",
"criterion",
"dashmap",
"futures",
+ "futures-timer",
+ "futures-util",
"graphql-tools",
"headers-accept",
"hive-console-sdk",
@@ -2424,6 +2435,7 @@ dependencies = [
"thiserror 2.0.18",
"tonic",
"tracing",
+ "url",
]
[[package]]
@@ -2471,12 +2483,14 @@ name = "hive-router-plan-executor"
version = "6.9.3"
dependencies = [
"ahash",
+ "async-stream",
"async-trait",
"bumpalo",
"bytes",
"criterion",
"dashmap",
"futures",
+ "futures-util",
"graphql-tools",
"hive-router-config",
"hive-router-internal",
@@ -2502,6 +2516,7 @@ dependencies = [
"thiserror 2.0.18",
"tokio",
"tracing",
+ "ulid",
"xxhash-rust",
"zmij",
]
@@ -3620,6 +3635,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"regex",
+ "rustls",
"serde",
"serde_json",
"serde_urlencoded",
@@ -3627,6 +3643,7 @@ dependencies = [
"thiserror 2.0.18",
"uuid",
"variadics_please",
+ "webpki-roots",
]
[[package]]
@@ -3874,6 +3891,7 @@ dependencies = [
"ntex-net",
"ntex-service",
"ntex-util",
+ "rustls",
]
[[package]]
@@ -5678,6 +5696,7 @@ dependencies = [
"schemars_derive 1.2.1",
"serde",
"serde_json",
+ "url",
]
[[package]]
@@ -6297,10 +6316,21 @@ version = "0.0.1"
dependencies = [
"async-graphql",
"async-graphql-axum",
+ "async-stream",
"axum",
+ "bytes",
+ "futures",
+ "futures-timer",
+ "futures-util",
+ "hive-router",
+ "http",
"lazy_static",
"rand 0.10.1",
+ "reqwest",
+ "serde",
+ "serde_json",
"tokio",
+ "tower-service",
]
[[package]]
@@ -7064,6 +7094,7 @@ dependencies = [
"idna",
"percent-encoding",
"serde",
+ "serde_derive",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index c05b29c29..21a626928 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -64,9 +64,10 @@ thiserror = "2.0.14"
xxhash-rust = { version = "0.8.15", features = ["xxh3"] }
tokio = { version = "1.47.1", features = ["full"] }
tokio-util = { version = "0.7.16" }
+tokio-stream = { version = "0.1.17" }
rand = "0.10.1"
jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] }
-ntex = { version = "3.7.2", features = ["tokio"] }
+ntex = { version = "3.7.2", features = ["tokio", "rustls"] }
tonic = { version = "0.14.2", features = ["tls-aws-lc"] }
reqwest = { version = "0.12.23", default-features = false, features = ["http2", "rustls-tls"] }
reqwest-retry = "0.8.0"
diff --git a/bench/subgraphs/Cargo.toml b/bench/subgraphs/Cargo.toml
index bfa0859da..f19b79db9 100644
--- a/bench/subgraphs/Cargo.toml
+++ b/bench/subgraphs/Cargo.toml
@@ -17,6 +17,18 @@ lazy_static = { workspace = true }
rand = { workspace = true }
tokio = { workspace = true }
axum = { workspace = true }
+futures = { workspace = true }
+http = { workspace = true }
+serde_json = { workspace = true }
+serde = { workspace = true }
+reqwest = { workspace = true }
+# TODO: can this be a workspace = true?
+hive-router = { path = "../../bin/router" }
+bytes = "1.10.1"
async-graphql = "7.0.17"
async-graphql-axum = "7.0.17"
+futures-util = "0.3.31"
+tower-service = "0.3.3"
+async-stream = "0.3.6"
+futures-timer = "3.0.3"
diff --git a/bench/subgraphs/graphql_with_subscriptions.rs b/bench/subgraphs/graphql_with_subscriptions.rs
new file mode 100644
index 000000000..540e20043
--- /dev/null
+++ b/bench/subgraphs/graphql_with_subscriptions.rs
@@ -0,0 +1,456 @@
+use std::time::Duration;
+use std::{
+ convert::Infallible,
+ io,
+ task::{Context, Poll},
+};
+
+use async_graphql::{Executor, Response as GraphQLResponse};
+use async_graphql_axum::GraphQLBatchRequest;
+use async_graphql_axum::{rejection::GraphQLRejection, GraphQLRequest};
+use axum::{
+ body::{Body, HttpBody},
+ extract::FromRequest,
+ http::{Request as HttpRequest, Response as HttpResponse},
+ response::IntoResponse,
+ BoxError,
+};
+use bytes::Bytes;
+use futures_util::{future::BoxFuture, stream::BoxStream, Stream, StreamExt};
+use hive_router::pipeline::multipart_subscribe::APOLLO_MULTIPART_HTTP_CONTENT_TYPE;
+use hive_router::pipeline::sse::SSE_HEADER;
+use hive_router::pipeline::{multipart_subscribe, sse};
+use hive_router::tracing::error;
+use serde::{Deserialize, Serialize};
+use tower_service::Service;
+
+use crate::HTTPStreamingSubscriptionProtocol;
+
+#[derive(Clone)]
+pub struct GraphQL {
+ executor: E,
+ subscriptions_protocol: HTTPStreamingSubscriptionProtocol,
+}
+
+impl GraphQL
+where
+ E: Clone,
+{
+ pub fn new(executor: E, subscriptions_protocol: HTTPStreamingSubscriptionProtocol) -> Self {
+ Self {
+ executor,
+ subscriptions_protocol,
+ }
+ }
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct CallbackSubscriptionExt {
+ callback_url: String,
+ subscription_id: String,
+ verifier: String,
+ #[serde(default)]
+ heartbeat_interval_ms: u64,
+}
+
+#[derive(Serialize)]
+struct CallbackMessage<'a> {
+ kind: &'a str,
+ action: &'a str,
+ id: &'a str,
+ verifier: &'a str,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ payload: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ errors: Option>,
+}
+
+// yeah I know there's a graphql error struct, but I want to avoid overengineering this
+#[derive(Serialize)]
+struct GraphQLError {
+ message: String,
+}
+
+impl Service> for GraphQL
+where
+ B: HttpBody + Send + 'static,
+ B::Data: Into,
+ B::Error: Into,
+ E: Executor,
+{
+ type Response = HttpResponse;
+ type Error = Infallible;
+ type Future = BoxFuture<'static, Result>;
+
+ fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> {
+ Poll::Ready(Ok(()))
+ }
+
+ fn call(&mut self, req: HttpRequest) -> Self::Future {
+ let does_accept_callback = req
+ .headers()
+ .get("accept")
+ .and_then(|value| value.to_str().ok())
+ .is_some_and(|accept|
+ // we dont handle spaces, we dont handle q weights, we dont care atm
+ accept.contains("application/json;callbackSpec=1.0"));
+
+ let does_accept_multipart_mixed = req
+ .headers()
+ .get("accept")
+ .and_then(|value| value.to_str().ok())
+ .is_some_and(|accept|
+ // we dont use is_accept_multipart_mixed here because it requires the boundary
+ // to also be set in the accept header, which is not necessary according to
+ // the spec. hive router obeys the spec, and will set exactly the necessary accept
+ // as per https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol
+ accept.contains(r#"multipart/mixed;subscriptionSpec="1.0""#));
+
+ let does_accept_event_stream = req
+ .headers()
+ .get("accept")
+ .and_then(|value| value.to_str().ok())
+ .is_some_and(|accept| accept.contains(SSE_HEADER));
+
+ // for testing purposes. abruptly terminate the stream after N messages
+ let break_after_count = req
+ .headers()
+ .get("x-break-after")
+ .and_then(|v| v.to_str().ok())
+ .and_then(|v| v.parse::().ok());
+
+ let disable_heartbeats = req
+ .headers()
+ .get("x-disable-http-callback-heartbeats")
+ .and_then(|v| v.to_str().ok())
+ .is_some_and(|v| v == "true");
+
+ let sub_prot = self.subscriptions_protocol.clone();
+
+ let executor = self.executor.clone();
+ let req = req.map(Body::new);
+
+ Box::pin(async move {
+ if does_accept_callback {
+ let req = match GraphQLRequest::::from_request(req, &()).await {
+ Ok(req) => req,
+ Err(err) => return Ok(err.into_response()),
+ };
+
+ let gql_request = req.into_inner();
+
+ let sub_ext = match gql_request.extensions.get("subscription") {
+ Some(val) => {
+ let json = serde_json::to_value(val).unwrap();
+ match serde_json::from_value::(json) {
+ Ok(ext) => ext,
+ Err(_) => {
+ return Ok(HttpResponse::builder()
+ .status(400)
+ .body(Body::from(
+ r#"{"errors":[{"message":"Invalid subscription extension"}]}"#,
+ ))
+ .unwrap());
+ }
+ }
+ }
+ None => {
+ return Ok(HttpResponse::builder()
+ .status(400)
+ .body(Body::from(
+ r#"{"errors":[{"message":"Missing subscription extension"}]}"#,
+ ))
+ .unwrap());
+ }
+ };
+
+ // no hyper, just keeping it simple
+ let client = reqwest::Client::new();
+
+ let check_msg = CallbackMessage {
+ kind: "subscription",
+ action: "check",
+ id: &sub_ext.subscription_id,
+ verifier: &sub_ext.verifier,
+ payload: None,
+ errors: None,
+ };
+ let check_resp = client
+ .post(&sub_ext.callback_url)
+ .header("subscription-protocol", "callback/1.0")
+ .json(&check_msg)
+ .send()
+ .await;
+
+ match check_resp {
+ Ok(resp) if resp.status() == 204 => {}
+ Ok(resp) => {
+ error!("callback check responded with: {}", resp.status());
+ return Ok(HttpResponse::builder()
+ .status(400)
+ .body(Body::from(
+ r#"{"errors":[{"message":"Callback check failed for whatever reason"}]}"#,
+ ))
+ .unwrap());
+ }
+ Err(err) => {
+ error!("failed to send callback check request: {}", err);
+ return Ok(HttpResponse::builder()
+ .status(400)
+ .body(Body::from(
+ r#"{"errors":[{"message":"Failed to send callback check request"}]}"#,
+ ))
+ .unwrap());
+ }
+ }
+
+ let stream = executor.execute_stream(gql_request, None);
+
+ tokio::spawn(emit_subscription_events(
+ client,
+ sub_ext.callback_url,
+ sub_ext.subscription_id,
+ sub_ext.verifier,
+ sub_ext.heartbeat_interval_ms,
+ disable_heartbeats,
+ stream,
+ ));
+
+ return Ok(HttpResponse::builder()
+ .status(200)
+ .header("content-type", "application/json")
+ .body(Body::from(r#"{"data":null}"#))
+ .unwrap());
+ }
+
+ if !does_accept_multipart_mixed && !does_accept_event_stream {
+ let req =
+ match GraphQLBatchRequest::::from_request(req, &()).await {
+ Ok(req) => req,
+ Err(err) => return Ok(err.into_response()),
+ };
+ return Ok(async_graphql_axum::GraphQLResponse(
+ executor.execute_batch(req.0).await,
+ )
+ .into_response());
+ }
+
+ let req = match GraphQLRequest::::from_request(req, &()).await {
+ Ok(req) => req,
+ Err(err) => return Ok(err.into_response()),
+ };
+ let stream = executor.execute_stream(req.0, None);
+
+ let use_sse = match sub_prot {
+ HTTPStreamingSubscriptionProtocol::PreferMultipartFallbackSse => {
+ does_accept_event_stream
+ }
+ HTTPStreamingSubscriptionProtocol::SseOnly => true,
+ HTTPStreamingSubscriptionProtocol::MultipartOnly => false,
+ };
+
+ if use_sse {
+ let byte_stream =
+ create_sse_stream(stream, Duration::from_secs(10)).map(Ok::<_, io::Error>);
+ let body = if let Some(count) = break_after_count {
+ Body::from_stream(abrupt_terminate_after(byte_stream, count))
+ } else {
+ Body::from_stream(byte_stream)
+ };
+ Ok(HttpResponse::builder()
+ .header(http::header::CONTENT_TYPE, SSE_HEADER)
+ .header(http::header::CACHE_CONTROL, "no-cache")
+ .header(http::header::CONNECTION, "keep-alive")
+ .body(body)
+ .unwrap())
+ } else {
+ let byte_stream =
+ create_multipart_subscribe_stream(stream, Duration::from_secs(30))
+ .map(Ok::<_, io::Error>);
+ let body = if let Some(count) = break_after_count {
+ Body::from_stream(abrupt_terminate_after(byte_stream, count))
+ } else {
+ Body::from_stream(byte_stream)
+ };
+ Ok(HttpResponse::builder()
+ .header(
+ http::header::CONTENT_TYPE,
+ APOLLO_MULTIPART_HTTP_CONTENT_TYPE,
+ )
+ .header(http::header::CACHE_CONTROL, "no-cache")
+ .header(http::header::CONNECTION, "keep-alive")
+ .body(body)
+ .unwrap())
+ }
+ })
+ }
+}
+
+async fn emit_subscription_events(
+ client: reqwest::Client,
+ callback_url: String,
+ subscription_id: String,
+ verifier: String,
+ heartbeat_interval_ms: u64,
+ disable_heartbeats: bool,
+ stream: impl Stream- + Send + Unpin + 'static,
+) {
+ let mut stream = std::pin::pin!(stream);
+
+ let heartbeat_enabled = heartbeat_interval_ms > 0;
+ let mut heartbeat_interval = if heartbeat_enabled {
+ Some(tokio::time::interval(Duration::from_millis(
+ heartbeat_interval_ms,
+ )))
+ } else {
+ None
+ };
+
+ // skip the first immediate tick
+ if let Some(ref mut interval) = heartbeat_interval {
+ interval.tick().await;
+ }
+
+ loop {
+ let next_event = if let Some(ref mut interval) = heartbeat_interval {
+ tokio::select! {
+ item = stream.next() => item.map(|r| CallbackEvent::Next(Box::new(r))),
+ _ = interval.tick() => Some(CallbackEvent::Heartbeat),
+ }
+ } else {
+ stream
+ .next()
+ .await
+ .map(|r| CallbackEvent::Next(Box::new(r)))
+ };
+
+ match next_event {
+ Some(CallbackEvent::Next(response)) => {
+ let payload =
+ serde_json::to_value(&response).expect("Failed to serialize GraphQLResponse");
+ let msg = CallbackMessage {
+ kind: "subscription",
+ action: "next",
+ id: &subscription_id,
+ verifier: &verifier,
+ payload: Some(payload),
+ errors: None,
+ };
+ match send_callback(&client, &callback_url, &msg).await {
+ Ok(status) if status.is_success() => {}
+ // 404, or any other non-success status or error terminates the
+ // subscription. this subgraph is for testing, we dont dwell
+ // too much about learning the exact reason
+ _ => return,
+ }
+ }
+ Some(CallbackEvent::Heartbeat) => {
+ if disable_heartbeats {
+ continue;
+ }
+ let msg = CallbackMessage {
+ kind: "subscription",
+ action: "check",
+ id: &subscription_id,
+ verifier: &verifier,
+ payload: None,
+ errors: None,
+ };
+ match send_callback(&client, &callback_url, &msg).await {
+ Ok(status) if status.is_success() => {}
+ // 404, or any other non-success status or error terminates the
+ // subscription. this subgraph is for testing, we dont dwell
+ // too much about learning the exact reason
+ _ => return,
+ }
+ }
+ None => {
+ let msg = CallbackMessage {
+ kind: "subscription",
+ action: "complete",
+ id: &subscription_id,
+ verifier: &verifier,
+ payload: None,
+ errors: None,
+ };
+ let _ = send_callback(&client, &callback_url, &msg).await;
+ return;
+ }
+ }
+ }
+}
+
+enum CallbackEvent {
+ Next(Box
),
+ Heartbeat,
+}
+
+async fn send_callback(
+ client: &reqwest::Client,
+ url: &str,
+ msg: &CallbackMessage<'_>,
+) -> Result {
+ let resp = client
+ .post(url)
+ .header("subscription-protocol", "callback/1.0")
+ .json(msg)
+ .send()
+ .await?;
+ Ok(resp.status())
+}
+
+fn abrupt_terminate_after(
+ stream: S,
+ count: usize,
+) -> impl Stream- >
+where
+ S: Stream
- > + Send + 'static,
+{
+ async_stream::stream! {
+ let mut stream = std::pin::pin!(stream);
+ let mut emitted = 0;
+
+ while let Some(item) = stream.next().await {
+ yield item;
+ emitted += 1;
+ if emitted > count {
+ // error abruptly killing the connection
+ yield Err(io::Error::new(io::ErrorKind::ConnectionReset, "connection abruptly terminated"));
+ break;
+ }
+ }
+ }
+}
+
+pub fn create_sse_stream(
+ input: impl Stream
- + Send + Unpin + 'static,
+ heartbeat_interval: Duration,
+) -> BoxStream<'static, Bytes> {
+ // GraphQLResponse stream to Vec
stream
+ let byte_stream =
+ input.map(|resp| serde_json::to_vec(&resp).expect("Failed to serialize GraphQLResponse"));
+ sse::create_stream(byte_stream, heartbeat_interval)
+ .map(|result| {
+ // Convert Result to bytes::Bytes
+ // Unwrap is safe here as we control the serialization above
+ Bytes::copy_from_slice(&result.expect("SSE stream error"))
+ })
+ .boxed()
+}
+
+pub fn create_multipart_subscribe_stream(
+ input: impl Stream- + Send + Unpin + 'static,
+ heartbeat_interval: Duration,
+) -> BoxStream<'static, Bytes> {
+ // GraphQLResponse stream to Vec
stream
+ let byte_stream =
+ input.map(|resp| serde_json::to_vec(&resp).expect("Failed to serialize GraphQLResponse"));
+ multipart_subscribe::create_apollo_multipart_http_stream(byte_stream, heartbeat_interval)
+ .map(|result| {
+ // Convert Result to bytes::Bytes
+ // Unwrap is safe here as we control the serialization above
+ Bytes::copy_from_slice(&result.expect("Multipart stream error"))
+ })
+ .boxed()
+}
diff --git a/bench/subgraphs/lib.rs b/bench/subgraphs/lib.rs
index eac0c4aa9..f5d600f11 100644
--- a/bench/subgraphs/lib.rs
+++ b/bench/subgraphs/lib.rs
@@ -1,9 +1,10 @@
pub mod accounts;
+pub mod graphql_with_subscriptions;
pub mod inventory;
pub mod products;
pub mod reviews;
-use async_graphql_axum::GraphQL;
+use async_graphql_axum::{GraphQL, GraphQLSubscription};
use axum::{
extract::Request,
http::StatusCode,
@@ -57,7 +58,7 @@ pub fn start_subgraphs_server(port: Option) -> (JoinHandle<()>, Sender<()>)
.map(|v| v.to_string())
.unwrap_or(std::env::var("PORT").unwrap_or("4200".to_owned()));
- let mut app = subgraphs_app();
+ let mut app = subgraphs_app(HTTPStreamingSubscriptionProtocol::default());
app = app.route("/health", get(health_check_handler));
println!("Starting server on http://{}:{}", host, port);
@@ -80,7 +81,19 @@ pub fn start_subgraphs_server(port: Option) -> (JoinHandle<()>, Sender<()>)
(server_handle, shutdown_tx)
}
-pub fn subgraphs_app() -> Router<()> {
+/// The protocol to use for GraphQL subscriptions over HTTP streaming.
+/// It is purely the streaming HTTP protocol, other subscription protocols
+/// are handled automatically through HTTP negotiation (like websocket upgrades
+/// or http callbacks).
+#[derive(Clone, Default)]
+pub enum HTTPStreamingSubscriptionProtocol {
+ #[default]
+ PreferMultipartFallbackSse,
+ MultipartOnly,
+ SseOnly,
+}
+
+pub fn subgraphs_app(subscriptions_protocol: HTTPStreamingSubscriptionProtocol) -> Router<()> {
Router::new()
.route(
"/accounts",
@@ -94,9 +107,16 @@ pub fn subgraphs_app() -> Router<()> {
"/products",
post_service(GraphQL::new(products::get_subgraph())),
)
+ .route_service(
+ "/reviews/ws",
+ GraphQLSubscription::new(reviews::get_subgraph()),
+ )
.route(
"/reviews",
- post_service(GraphQL::new(reviews::get_subgraph())),
+ post_service(graphql_with_subscriptions::GraphQL::new(
+ reviews::get_subgraph(),
+ subscriptions_protocol,
+ )),
)
.route_layer(middleware::from_fn(add_subgraph_header))
.route_layer(middleware::from_fn(delay_middleware))
diff --git a/bench/subgraphs/reviews.rs b/bench/subgraphs/reviews.rs
index 0dc9cebff..86d839fc0 100644
--- a/bench/subgraphs/reviews.rs
+++ b/bench/subgraphs/reviews.rs
@@ -1,6 +1,7 @@
-use async_graphql::{
- ComplexObject, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject, ID,
-};
+use std::time::Duration;
+
+use async_graphql::{ComplexObject, EmptyMutation, Object, Schema, SimpleObject, Subscription, ID};
+use futures::stream::{self, Stream};
use lazy_static::lazy_static;
lazy_static! {
@@ -167,8 +168,76 @@ impl Query {
}
}
-pub fn get_subgraph() -> Schema {
- Schema::build(Query, EmptyMutation, EmptySubscription)
+pub struct Subscription;
+
+#[Subscription]
+impl Subscription {
+ async fn review_added(
+ &self,
+ #[graphql(default = 1)] step: usize,
+ #[graphql(default = 1_000)] interval_in_ms: u64,
+ ) -> impl Stream- {
+ stream::unfold(
+ (
+ 0,
+ if interval_in_ms > 0 {
+ Some(tokio::time::interval(Duration::from_millis(interval_in_ms)))
+ } else {
+ None
+ },
+ ),
+ move |(i, mut interval)| async move {
+ match REVIEWS.get(i) {
+ Some(review) => {
+ if let Some(int) = &mut interval {
+ int.tick().await;
+ }
+ Some((review.clone(), (i + step, interval)))
+ }
+ None => None,
+ }
+ },
+ )
+ }
+
+ async fn review_added_for_product(
+ &self,
+ product_upc: String,
+ #[graphql(default = 1_000)] interval_in_ms: u64,
+ ) -> impl Stream
- {
+ let reviews_for_product: Vec
= REVIEWS
+ .iter()
+ .filter(move |r| r.product.as_ref().unwrap().upc == product_upc)
+ .cloned()
+ .collect();
+
+ stream::unfold(
+ (
+ reviews_for_product,
+ 0,
+ if interval_in_ms > 0 {
+ Some(tokio::time::interval(Duration::from_millis(interval_in_ms)))
+ } else {
+ None
+ },
+ ),
+ move |(reviews_for_product, i, mut interval)| async move {
+ match reviews_for_product.get(i) {
+ Some(review) => {
+ if let Some(int) = &mut interval {
+ int.tick().await;
+ }
+ Some((review.clone(), (reviews_for_product, i + 1, interval)))
+ }
+ None => None,
+ }
+ },
+ )
+ }
+}
+
+pub fn get_subgraph() -> Schema {
+ Schema::build(Query, EmptyMutation, Subscription)
.enable_federation()
.finish()
}
diff --git a/bench/supergraph.graphql b/bench/supergraph.graphql
index 631e291e2..52b6f2ca3 100644
--- a/bench/supergraph.graphql
+++ b/bench/supergraph.graphql
@@ -2,6 +2,7 @@ schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
query: Query
+ subscription: Subscription
}
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
@@ -97,6 +98,13 @@ type Query
topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS)
}
+type Subscription @join__type(graph: REVIEWS) {
+ reviewAdded(step: Int = 1, intervalInMs: Int = 1000): Review
+ @join__field(graph: REVIEWS)
+ reviewAddedForProduct(productUpc: String!, intervalInMs: Int = 1000): Review
+ @join__field(graph: REVIEWS)
+}
+
type Review @join__type(graph: REVIEWS, key: "id") {
id: ID!
body: String
diff --git a/bin/router/Cargo.toml b/bin/router/Cargo.toml
index 7231cc26b..2b488abdb 100644
--- a/bin/router/Cargo.toml
+++ b/bin/router/Cargo.toml
@@ -73,7 +73,12 @@ insta = { workspace = true }
mimalloc = { version = "0.1.48", features = ["v3"] }
mediatype = "0.21.0"
headers-accept = "0.3.0"
+async-stream = "0.3.6"
+futures-util = "0.3.31"
+futures-timer = "3.0.3"
+const-str = "1.0.0"
md5 = "0.8.0"
+bytes = { workspace = true }
[dev-dependencies]
criterion = { workspace = true }
diff --git a/bin/router/src/error.rs b/bin/router/src/error.rs
index 4ea59439b..d083ccdce 100644
--- a/bin/router/src/error.rs
+++ b/bin/router/src/error.rs
@@ -14,6 +14,8 @@ pub enum RouterInitError {
SupergraphManagerError(#[from] SupergraphManagerError),
#[error("Failed to bind HTTP server to address: {0}. Error: {1}")]
HttpServerBindError(String, std::io::Error),
+ #[error("Failed to bind HTTP callback server to address: {0}. Error: {1}")]
+ HttpCallbackServerBindError(String, std::io::Error),
#[error("Failed to start HTTP server: {0}")]
HttpServerStartError(std::io::Error),
#[error(transparent)]
diff --git a/bin/router/src/lib.rs b/bin/router/src/lib.rs
index b00a8d0a8..06bf96266 100644
--- a/bin/router/src/lib.rs
+++ b/bin/router/src/lib.rs
@@ -22,9 +22,12 @@ use crate::{
},
jwt::JwtAuthRuntime,
pipeline::{
+ active_subscriptions::ActiveSubscriptions,
error::handle_pipeline_error,
graphql_request_handler,
header::ResponseMode,
+ http_callback::handler,
+ long_lived_client_limit::LongLivedClientLimitService,
request_extensions::{
read_graphql_operation_metric_identity, read_graphql_response_metric_status,
read_request_body_size, write_graphql_response_metric_status,
@@ -35,6 +38,7 @@ use crate::{
max_aliases_rule::MaxAliasesRule, max_depth_rule::MaxDepthRule,
max_directives_rule::MaxDirectivesRule,
},
+ websocket_server::ws_index,
},
plugins::plugins_service::PluginService,
telemetry::{HeaderExtractor, PrometheusAttached},
@@ -49,8 +53,9 @@ pub use dashmap::DashMap;
pub use graphql_tools;
use graphql_tools::validation::rules::default_rules_validation_plan;
pub use hive_router_config::humantime_serde;
-use hive_router_config::{load_config, HiveRouterConfig};
+use hive_router_config::{load_config, subscriptions::CallbackConfig, HiveRouterConfig};
pub use hive_router_internal::background_tasks;
+use hive_router_internal::background_tasks::{BackgroundTask, CancellationToken};
use hive_router_internal::telemetry::metrics::catalog::values::GraphQLResponseStatus;
use hive_router_internal::telemetry::{
otel::tracing_opentelemetry::OpenTelemetrySpanExt,
@@ -73,6 +78,31 @@ use tracing::{info, warn, Instrument};
static LABORATORY_HTML: &str = include_str!(concat!(env!("OUT_DIR"), "/laboratory.html"));
+struct CallbackServer(std::sync::Mutex>);
+
+impl From for CallbackServer {
+ fn from(server: ntex::server::Server) -> Self {
+ Self(std::sync::Mutex::new(Some(server)))
+ }
+}
+
+#[async_trait]
+impl BackgroundTask for CallbackServer {
+ fn id(&self) -> &str {
+ "callback_server"
+ }
+
+ async fn run(&self, token: CancellationToken) {
+ token.cancelled().await;
+ // only poisoned if a thread panicked while holding the lock; since the only
+ // operation inside is .take(), that can't happen
+ let server = self.0.lock().unwrap().take();
+ if let Some(server) = server {
+ server.stop(true).await;
+ }
+ }
+}
+
async fn graphql_endpoint_handler(
request: HttpRequest,
body_stream: web::types::Payload,
@@ -170,8 +200,10 @@ pub async fn router_entrypoint(plugin_registry: PluginRegistry) -> Result<(), Ro
.as_ref()
.and_then(|prom| prom.to_attached());
info!("hive-router@{} starting...", ROUTER_VERSION);
- let http_config = router_config.http.clone();
- let addr = router_config.http.address();
+ let addr = router_config.address();
+ let graphql_path = router_config.graphql_path().to_string();
+ let websocket_path = router_config.websocket_path().map(|p| p.to_string());
+ let callback_conf = router_config.callback_conf().cloned();
let mut bg_tasks_manager = background_tasks::BackgroundTasksManager::new();
let (shared_state, schema_state) = configure_app_from_config(
router_config,
@@ -182,20 +214,62 @@ pub async fn router_entrypoint(plugin_registry: PluginRegistry) -> Result<(), Ro
.await?;
let shared_state_clone = shared_state.clone();
- let graphql_path = http_config.graphql_endpoint();
+ let callback_subscriptions_for_handler = schema_state.callback_subscriptions.clone();
+
+ // when `listen` is set, the callback route lives on a dedicated server bound to that address
+ // otherwise, the callback route is mounted on the main server on the `callback_path`
+ let callback_path = match callback_conf {
+ Some(CallbackConfig {
+ listen: Some(listen),
+ ref path,
+ ..
+ }) => {
+ let cb_path = path.to_string();
+ let cb_addr = listen.to_string();
+ let cb_subs = callback_subscriptions_for_handler.clone();
+ let cb_server = web::HttpServer::new(async move || {
+ let cb_subs = cb_subs.clone();
+ let cb_path = cb_path.clone();
+ web::App::new()
+ .state(cb_subs)
+ .configure(move |m| add_callback_handler(m, &cb_path))
+ })
+ .bind(&cb_addr)
+ .map_err(|err| RouterInitError::HttpCallbackServerBindError(cb_addr, err))?
+ .run();
+
+ bg_tasks_manager.register_task(CallbackServer::from(cb_server));
+
+ None
+ }
+ Some(ref cb) => Some(cb.path.to_string()),
+ None => None,
+ };
- let paths = RouterPaths::new(graphql_path.to_string());
+ // after callback config check because there we decide if callback_path should be set
+ let paths = RouterPaths::new(graphql_path.clone(), websocket_path, callback_path);
paths.detect_conflicts(&prometheus)?;
- let graphql_path = graphql_path.to_string();
+ let long_lived_client_limit_service =
+ LongLivedClientLimitService::new(&shared_state.router_config);
+
let maybe_error = web::HttpServer::new(async move || {
let landing_page_path = graphql_path.clone();
let prometheus = prometheus.clone();
+ let long_lived_client_limit_service = long_lived_client_limit_service.clone();
web::App::new()
+ .middleware(long_lived_client_limit_service)
.middleware(PluginService)
.state(shared_state.clone())
.state(schema_state.clone())
.configure(|m| configure_ntex_app(m, &paths, prometheus))
+ .configure(|m| {
+ if let Some(ref callback) = paths.callback {
+ // callback path will be some only if callback is enabled and if
+ // its listen is not configured to be on another server
+ add_callback_handler(m, callback);
+ }
+ })
.default_service(web::to(move || {
landing_page_handler(landing_page_path.clone())
}))
@@ -243,6 +317,8 @@ pub async fn configure_app_from_config(
};
let plugins_arc = plugin_registry.initialize_plugins(&router_config, bg_tasks_manager)?;
+ let active_subscriptions =
+ ActiveSubscriptions::new(router_config.subscriptions.broadcast_capacity);
let router_config_arc = Arc::new(router_config);
let telemetry_context_arc = Arc::new(telemetry_context);
let cache_state = Arc::new(CacheState::new());
@@ -257,6 +333,7 @@ pub async fn configure_app_from_config(
router_config_arc.clone(),
plugins_arc.clone(),
cache_state.clone(),
+ active_subscriptions.clone(),
)
.await?;
let schema_state_arc = Arc::new(schema_state);
@@ -284,6 +361,7 @@ pub async fn configure_app_from_config(
telemetry_context_arc,
plugins_arc,
cache_state,
+ active_subscriptions.clone(),
)?);
Ok((shared_state, schema_state_arc))
@@ -292,14 +370,18 @@ pub async fn configure_app_from_config(
#[derive(Clone)]
pub struct RouterPaths {
graphql: String,
+ websocket: Option,
+ callback: Option,
health: String,
readiness: String,
}
impl RouterPaths {
- pub fn new(graphql: String) -> Self {
+ pub fn new(graphql: String, websocket: Option, callback: Option) -> Self {
RouterPaths {
graphql,
+ websocket,
+ callback,
health: "/health".to_string(),
readiness: "/readiness".to_string(),
}
@@ -309,13 +391,24 @@ impl RouterPaths {
&self,
prometheus: &Option,
) -> Result<(), RouterInitError> {
- // A pair of context and actual path
+ // A pair of context and actual path (only include optional paths when present)
let mut paths = vec![
("graphql", self.graphql.as_str()),
("health", self.health.as_str()),
("readiness", self.readiness.as_str()),
];
+ if let Some(ws) = self.websocket.as_deref() {
+ // its safe to have graphql and websocket on same path
+ if ws != self.graphql.as_str() {
+ paths.push(("websocket", ws));
+ }
+ }
+
+ if let Some(cb) = self.callback.as_deref() {
+ paths.push(("callback", cb));
+ }
+
if let Some(prom) = prometheus {
paths.push(("prometheus", prom.endpoint.as_str()));
}
@@ -338,11 +431,35 @@ impl RouterPaths {
}
}
+pub fn add_callback_handler(cfg: &mut web::ServiceConfig, callback_path: &str) {
+ let callback_route = format!(
+ "{}/{{subscription_id}}",
+ callback_path.trim_end_matches('/'),
+ );
+ cfg.route(&callback_route, web::post().to(handler));
+}
+
pub fn configure_ntex_app(
cfg: &mut web::ServiceConfig,
paths: &RouterPaths,
prometheus: Option,
) {
+ if let Some(websocket) = &paths.websocket {
+ cfg.service(
+ web::resource(websocket.as_str())
+ // guard ensures this resource is only matched for actual ws upgrade requests,
+ // so a plain GET to the same path (e.g. graphql GET request) falls through
+ // to the next registered resource instead of hitting the ws handshake
+ .guard(web::guard::fn_guard(|head| {
+ head.headers()
+ .get(ntex::http::header::UPGRADE)
+ .and_then(|v| v.to_str().ok())
+ .is_some_and(|v| v.eq_ignore_ascii_case("websocket"))
+ }))
+ .route(web::get().to(ws_index)),
+ );
+ }
+
cfg.route(paths.graphql.as_str(), web::to(graphql_endpoint_handler))
.route(paths.health.as_str(), web::to(health_check_handler))
.route(paths.readiness.as_str(), web::to(readiness_check_handler));
diff --git a/bin/router/src/pipeline/active_subscriptions.rs b/bin/router/src/pipeline/active_subscriptions.rs
new file mode 100644
index 000000000..f51d18c64
--- /dev/null
+++ b/bin/router/src/pipeline/active_subscriptions.rs
@@ -0,0 +1,98 @@
+use std::sync::Arc;
+
+use bytes::Bytes;
+use dashmap::DashMap;
+use hive_router_plan_executor::response::graphql_error::GraphQLError;
+use tokio::sync::broadcast;
+use tracing::trace;
+use ulid::Ulid;
+
+use crate::shared_state::SharedRouterResponseGuard;
+
+pub type SubscriptionId = String;
+
+#[derive(Clone, Debug)]
+pub enum SubscriptionEvent {
+ /// A normal subscription event from the upstream, already serialized.
+ /// Uses Bytes for zero-copy cloning across broadcast receivers.
+ Raw(Bytes),
+ /// An error pushed externally (e.g. supergraph reload, shutdown).
+ /// Consumers should yield this as the final event and then stop.
+ Error(Vec),
+}
+
+#[derive(Clone)]
+pub struct ActiveSubscriptions {
+ map: Arc>>,
+ broadcast_capacity: usize,
+}
+
+impl ActiveSubscriptions {
+ pub fn new(broadcast_capacity: usize) -> Self {
+ Self {
+ map: Arc::new(DashMap::new()),
+ broadcast_capacity,
+ }
+ }
+
+ /// Register a new active subscription. Returns a producer handle for the upstream pump
+ /// and a pre-subscribed receiver for the leader consumer. The pump task owns the handle
+ /// for the full lifetime of the upstream stream - when the handle drops (pump done or all
+ /// receivers gone) the broadcast channel closes and all consumer receivers terminate.
+ pub fn register(
+ &self,
+ guard: Option,
+ ) -> (ProducerHandle, broadcast::Receiver) {
+ let (sender, receiver) = broadcast::channel(self.broadcast_capacity);
+ let id = Ulid::new().to_string();
+ self.map.insert(id.clone(), sender.clone());
+
+ let handle = ProducerHandle {
+ id: id.clone(),
+ map: self.map.clone(),
+ sender,
+ _guard: guard,
+ };
+
+ trace!(subscription_id = %id, "registered new subscription");
+
+ (handle, receiver)
+ }
+
+ /// Close all active subscriptions with an error and clear the registry.
+ pub fn close_all_with_error(&self, errors: Vec) {
+ let item = SubscriptionEvent::Error(errors);
+ for entry in self.map.iter() {
+ let _ = entry.send(item.clone());
+ }
+ self.map.clear();
+ }
+}
+
+/// Held by the upstream pump task for the full lifetime of the stream. Dropping it removes
+/// the subscription from the registry, closes the broadcast channel, and drops the inflight
+/// cleanup guard - which removes the dedupe entry so new requests start a fresh upstream.
+pub struct ProducerHandle {
+ id: SubscriptionId,
+ map: Arc>>,
+ sender: broadcast::Sender,
+ _guard: Option,
+}
+
+impl ProducerHandle {
+ pub fn sender(&self) -> &broadcast::Sender {
+ &self.sender
+ }
+
+ /// Returns false when all consumers have gone and the event cannot be delivered.
+ pub fn send(&self, item: SubscriptionEvent) -> bool {
+ self.sender.send(item).is_ok()
+ }
+}
+
+impl Drop for ProducerHandle {
+ fn drop(&mut self) {
+ self.map.remove(&self.id);
+ trace!(subscription_id = %self.id, "producer dropped, upstream closed");
+ }
+}
diff --git a/bin/router/src/pipeline/error.rs b/bin/router/src/pipeline/error.rs
index b9c2ae65e..e5f53bd01 100644
--- a/bin/router/src/pipeline/error.rs
+++ b/bin/router/src/pipeline/error.rs
@@ -1,8 +1,11 @@
use std::{sync::Arc, vec};
+use futures_util::stream;
use graphql_tools::validation::utils::ValidationError;
use hive_router_plan_executor::{
- execution::{error::PlanExecutionError, jwt_forward::JwtForwardingError},
+ execution::{
+ error::PlanExecutionError, jwt_forward::JwtForwardingError, plan::FailedExecutionResult,
+ },
headers::errors::HeaderRuleRuntimeError,
hooks::on_graphql_error::handle_graphql_errors_with_plugins,
response::graphql_error::GraphQLError,
@@ -10,19 +13,25 @@ use hive_router_plan_executor::{
use hive_router_query_planner::{
ast::normalization::error::NormalizationError, planner::PlannerError,
};
+use http::header;
use http::{header::RETRY_AFTER, HeaderName, Method, StatusCode};
use ntex::{
http::ResponseBuilder,
web::{self, error::QueryPayloadError},
};
-use serde::Serialize;
use strum::IntoStaticStr;
use crate::{
jwt::errors::JwtError,
pipeline::{
- authorization::AuthorizationError, body_read::ReadBodyStreamError, header::ResponseMode,
+ authorization::AuthorizationError,
+ body_read::ReadBodyStreamError,
+ header::{ResponseMode, StreamContentType},
+ multipart_subscribe::{
+ self, APOLLO_MULTIPART_HTTP_CONTENT_TYPE, INCREMENTAL_DELIVERY_CONTENT_TYPE,
+ },
progressive_override::LabelEvaluationError,
+ sse,
},
RouterSharedState,
};
@@ -230,38 +239,32 @@ impl PipelineError {
}
}
-#[derive(Serialize)]
-struct FailedExecutionResult {
- errors: Vec,
-}
-
#[inline]
pub fn handle_pipeline_error(
err: PipelineError,
shared_state: &RouterSharedState,
response_mode: &ResponseMode,
) -> web::HttpResponse {
- let single_content_type = response_mode.single_content_type();
-
- let prefer_ok = response_mode.prefer_status_ok_for_errors();
-
- let status = err.default_status_code(prefer_ok);
+ let status = if matches!(response_mode, ResponseMode::StreamOnly(_)) {
+ // alwats status OK for streaming response modes, because we accept
+ // the stream and then stream the error from within the stream by default
+ StatusCode::OK
+ } else {
+ let prefer_ok = response_mode.prefer_status_ok_for_errors();
+ err.default_status_code(prefer_ok)
+ };
let mut res = ResponseBuilder::new(status);
- if let Some(single_content_type) = single_content_type {
- res.content_type(single_content_type.as_ref());
- }
-
if matches!(err, PipelineError::NoSupergraphAvailable) {
res.header(RETRY_AFTER, "10");
}
let mut errors = match err {
- PipelineError::ValidationErrors(validation_errors) => {
+ PipelineError::ValidationErrors(ref validation_errors) => {
validation_errors.iter().map(|error| error.into()).collect()
}
- PipelineError::AuthorizationFailed(authorization_errors) => authorization_errors
+ PipelineError::AuthorizationFailed(ref authorization_errors) => authorization_errors
.iter()
.map(|error| error.into())
.collect(),
@@ -291,5 +294,42 @@ pub fn handle_pipeline_error(
.record_errors(|| errors.iter().map(|error| error.extensions.code.as_deref()));
}
- res.json(&FailedExecutionResult { errors })
+ let data = FailedExecutionResult { errors }.serialize();
+
+ match response_mode {
+ ResponseMode::SingleOnly(content_type) | ResponseMode::Dual(content_type, _) => res
+ .header(header::CONTENT_TYPE, content_type.as_ref())
+ .body(data),
+ ResponseMode::StreamOnly(StreamContentType::IncrementalDelivery) => res
+ .header(
+ header::CONTENT_TYPE,
+ http::HeaderValue::from_static(INCREMENTAL_DELIVERY_CONTENT_TYPE),
+ )
+ .streaming(multipart_subscribe::create_incremental_delivery_stream(
+ Box::pin(stream::once(async move { data })),
+ )),
+ ResponseMode::StreamOnly(StreamContentType::SSE) => res
+ .header(
+ header::CONTENT_TYPE,
+ http::HeaderValue::from_static("text/event-stream"),
+ )
+ .streaming(sse::create_stream(
+ Box::pin(stream::once(async move { data })),
+ std::time::Duration::from_secs(10),
+ )),
+ ResponseMode::StreamOnly(StreamContentType::ApolloMultipartHTTP) => res
+ .header(
+ header::CONTENT_TYPE,
+ http::HeaderValue::from_static(APOLLO_MULTIPART_HTTP_CONTENT_TYPE),
+ )
+ .streaming(multipart_subscribe::create_apollo_multipart_http_stream(
+ Box::pin(stream::once(async move { data })),
+ std::time::Duration::from_secs(10),
+ )),
+ ResponseMode::Laboratory => {
+ unreachable!(
+ "Laboratory can not be a response mode because Laboratory requests can not execute operations"
+ )
+ }
+ }
}
diff --git a/bin/router/src/pipeline/execution.rs b/bin/router/src/pipeline/execution.rs
index 57da014be..e4a6870be 100644
--- a/bin/router/src/pipeline/execution.rs
+++ b/bin/router/src/pipeline/execution.rs
@@ -1,4 +1,5 @@
use std::collections::HashMap;
+use std::sync::Arc;
use crate::pipeline::authorization::AuthorizationError;
use crate::pipeline::coerce_variables::CoerceVariablesPayload;
@@ -11,7 +12,7 @@ use hive_router_internal::telemetry::traces::spans::graphql::{
use hive_router_plan_executor::execution::client_request_details::ClientRequestDetails;
use hive_router_plan_executor::execution::jwt_forward::JwtAuthForwardingPlan;
use hive_router_plan_executor::execution::plan::{
- execute_query_plan, PlanExecutionOutput, QueryPlanExecutionOpts,
+ execute_query_plan, PlanExecutionOutput, QueryPlanExecutionOpts, QueryPlanExecutionResult,
};
use hive_router_plan_executor::hooks::on_supergraph_load::SupergraphData;
use hive_router_plan_executor::introspection::resolve::IntrospectionContext;
@@ -31,32 +32,31 @@ pub enum ExposeQueryPlanMode {
}
pub struct PlannedRequest<'req> {
- pub normalized_payload: &'req GraphQLNormalizationPayload,
+ pub normalized_payload: Arc,
pub query_plan_payload: &'req QueryPlan,
- pub variable_payload: &'req CoerceVariablesPayload,
- pub client_request_details: &'req ClientRequestDetails<'req>,
+ pub variable_payload: CoerceVariablesPayload,
+ pub client_request_details: Arc>,
pub authorization_errors: Vec,
- pub plugin_req_state: &'req Option>,
+ pub plugin_req_state: Option>,
}
#[inline]
-pub async fn execute_plan(
+pub async fn execute_plan<'exec>(
supergraph: &SupergraphData,
app_state: &RouterSharedState,
- planned_request: PlannedRequest<'_>,
- span: &GraphQLOperationSpan,
-) -> Result {
+ planned_request: PlannedRequest<'exec>,
+ span: GraphQLOperationSpan,
+) -> Result {
let execute_span = GraphQLExecuteSpan::new();
+ let introspection_context = IntrospectionContext {
+ query: planned_request
+ .normalized_payload
+ .operation_for_introspection
+ .clone(),
+ schema: Arc::clone(&supergraph.planner.consumer_schema.document),
+ metadata: Arc::clone(&supergraph.metadata),
+ };
async {
- let introspection_context = IntrospectionContext {
- query: planned_request
- .normalized_payload
- .operation_for_introspection
- .as_deref(),
- schema: &supergraph.planner.consumer_schema.document,
- metadata: &supergraph.metadata,
- };
-
let mut extensions = HashMap::new();
let mut expose_query_plan = ExposeQueryPlanMode::No;
@@ -92,10 +92,10 @@ pub async fn execute_plan(
}))
.map_err(PipelineError::QueryPlanSerializationFailed)?;
- return Ok(PlanExecutionOutput {
+ return Ok(QueryPlanExecutionResult::Single(PlanExecutionOutput {
body,
..Default::default()
- });
+ }));
}
let jwt_auth_forwarding: Option = if app_state
@@ -119,17 +119,20 @@ pub async fn execute_plan(
let result = execute_query_plan(QueryPlanExecutionOpts {
query_plan: planned_request.query_plan_payload,
- operation_for_plan: &planned_request.normalized_payload.operation_for_plan,
- projection_plan: &planned_request.normalized_payload.projection_plan,
- headers_plan: &app_state.headers_plan,
- variable_values: &planned_request.variable_payload.variables_map,
+ operation_for_plan: planned_request
+ .normalized_payload
+ .operation_for_plan
+ .clone(),
+ projection_plan: planned_request.normalized_payload.projection_plan.clone(),
+ headers_plan: app_state.headers_plan.clone(),
+ variable_values: Arc::new(planned_request.variable_payload.variables_map),
extensions,
client_request: planned_request.client_request_details,
- introspection_context: &introspection_context,
+ introspection_context: introspection_context.into(),
operation_type_name: planned_request.normalized_payload.root_type_name,
- jwt_auth_forwarding,
+ jwt_auth_forwarding: jwt_auth_forwarding.map(|j| j.into()),
graphql_error_recorder: app_state.telemetry_context.metrics.graphql.error_recorder(),
- executors: &supergraph.subgraph_executor_map,
+ executors: Arc::clone(&supergraph.subgraph_executor_map),
initial_errors: planned_request
.authorization_errors
.iter()
diff --git a/bin/router/src/pipeline/http_callback.rs b/bin/router/src/pipeline/http_callback.rs
new file mode 100644
index 000000000..66c13de74
--- /dev/null
+++ b/bin/router/src/pipeline/http_callback.rs
@@ -0,0 +1,251 @@
+use bytes::Bytes as BytesLib;
+use dashmap::mapref::one::Ref;
+use hive_router_plan_executor::executors::http_callback::{
+ CallbackMessage, CallbackSubscription, CallbackSubscriptionsMap, CALLBACK_PROTOCOL_VERSION,
+ SUBSCRIPTION_PROTOCOL_HEADER,
+};
+use hive_router_plan_executor::response::graphql_error::GraphQLError;
+use http::StatusCode;
+use ntex::util::Bytes;
+use ntex::web::WebResponseError;
+use ntex::web::{self, types::Path, HttpRequest, HttpResponse};
+use serde::Deserialize;
+use strum::EnumString;
+use tokio::sync::mpsc;
+use tracing::{debug, error, trace, warn};
+
+#[derive(Debug, Deserialize, EnumString)]
+#[serde(rename_all = "lowercase")]
+#[strum(serialize_all = "lowercase")]
+enum CallbackKind {
+ Subscription,
+}
+
+#[derive(Debug, Deserialize, EnumString)]
+#[serde(rename_all = "lowercase")]
+#[strum(serialize_all = "lowercase")]
+enum CallbackAction {
+ Check,
+ Next,
+ Complete,
+}
+
+#[derive(Debug, Deserialize)]
+struct CallbackPayload<'a> {
+ // unused in code, but used for validation
+ #[allow(unused)]
+ kind: CallbackKind,
+ action: CallbackAction,
+ id: String,
+ verifier: String,
+ #[serde(borrow, default)]
+ payload: Option>,
+ #[serde(default)]
+ errors: Option>,
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum CallbackError {
+ #[error(
+ "Invalid or missing {} header, expected {}",
+ SUBSCRIPTION_PROTOCOL_HEADER,
+ CALLBACK_PROTOCOL_VERSION
+ )]
+ InvalidProtocolHeader,
+ #[error("Failed to parse callback payload: {0}")]
+ PayloadParseError(#[from] sonic_rs::Error),
+ #[error("Subscription ID mismatch: path='{path}', body='{body}'")]
+ SubscriptionIdMismatch { path: String, body: String },
+ #[error("Missing payload in next message for subscription ID '{subscription_id}'")]
+ MissingPayload { subscription_id: String },
+ #[error(
+ "Subscription not found, may have been terminated for subscription ID '{subscription_id}'"
+ )]
+ SubscriptionNotFound { subscription_id: String },
+ #[error("Invalid verifier for subscription ID '{subscription_id}'")]
+ InvalidVerifier { subscription_id: String },
+ #[error("Subscription receiver dropped for subscription ID '{subscription_id}'")]
+ SubscriptionDropped { subscription_id: String },
+ // NOTE: intentionally a different variant from SubscriptionDropped
+ #[error(
+ "Client consuming too slowly. Event buffer full for subscription ID '{subscription_id}'"
+ )]
+ ClientTooSlow { subscription_id: String },
+}
+
+impl CallbackError {
+ fn log(&self) {
+ match self {
+ CallbackError::InvalidProtocolHeader => warn!("{}", self),
+ CallbackError::PayloadParseError(_) => error!("{}", self),
+ CallbackError::SubscriptionIdMismatch { .. } => warn!("{}", self),
+ CallbackError::MissingPayload { .. } => warn!("{}", self),
+ CallbackError::SubscriptionNotFound { .. } => warn!("{}", self),
+ CallbackError::InvalidVerifier { .. } => warn!("{}", self),
+ CallbackError::SubscriptionDropped { .. } => debug!("{}", self),
+ CallbackError::ClientTooSlow { .. } => warn!("{}", self),
+ }
+ }
+}
+
+impl WebResponseError for CallbackError {
+ fn status_code(&self) -> StatusCode {
+ match self {
+ CallbackError::InvalidProtocolHeader
+ | CallbackError::PayloadParseError(_)
+ | CallbackError::MissingPayload { .. }
+ | CallbackError::InvalidVerifier { .. } => StatusCode::BAD_REQUEST,
+ CallbackError::SubscriptionNotFound { .. }
+ | CallbackError::SubscriptionDropped { .. }
+ | CallbackError::SubscriptionIdMismatch { .. } => StatusCode::NOT_FOUND,
+ // 503 signals the subgraph that the router is temporarily unable to accept events,
+ // the subgraph can decide to retry or close the subscription on its end
+ CallbackError::ClientTooSlow { .. } => StatusCode::SERVICE_UNAVAILABLE,
+ }
+ }
+ fn error_response(&self, _: &HttpRequest) -> HttpResponse {
+ self.log();
+ HttpResponse::build(self.status_code())
+ .header(SUBSCRIPTION_PROTOCOL_HEADER, CALLBACK_PROTOCOL_VERSION)
+ .finish()
+ }
+}
+
+fn validate_protocol(req: &HttpRequest) -> Result<(), CallbackError> {
+ let protocol_header = req
+ .headers()
+ .get(SUBSCRIPTION_PROTOCOL_HEADER)
+ .and_then(|v| v.to_str().ok());
+
+ if protocol_header != Some(CALLBACK_PROTOCOL_VERSION) {
+ return Err(CallbackError::InvalidProtocolHeader);
+ }
+
+ Ok(())
+}
+
+fn parse_payload(body: &Bytes) -> Result, CallbackError> {
+ Ok(sonic_rs::from_slice(body)?)
+}
+
+fn validate_payload(
+ payload: &CallbackPayload<'_>,
+ subscription_id_from_path: &str,
+) -> Result<(), CallbackError> {
+ if payload.id != subscription_id_from_path {
+ return Err(CallbackError::SubscriptionIdMismatch {
+ path: subscription_id_from_path.to_string(),
+ body: payload.id.to_string(),
+ });
+ }
+
+ Ok(())
+}
+
+fn handle_check(subscription_id: &str, subscription: &Ref<'_, String, CallbackSubscription>) {
+ trace!(subscription_id = %subscription_id, "Received check message");
+ subscription.record_heartbeat();
+}
+
+fn handle_next(
+ subscription_id: &str,
+ payload: &CallbackPayload<'_>,
+ subscription: Ref<'_, String, CallbackSubscription>,
+ callback_subscriptions: &CallbackSubscriptionsMap,
+) -> Result<(), CallbackError> {
+ trace!(subscription_id = %subscription_id, "Received next message");
+
+ let data = match &payload.payload {
+ Some(p) => BytesLib::copy_from_slice(p.as_raw_str().as_bytes()),
+ None => {
+ return Err(CallbackError::MissingPayload {
+ subscription_id: subscription_id.to_string(),
+ });
+ }
+ };
+
+ match subscription
+ .sender
+ .try_send(CallbackMessage::Next { payload: data })
+ {
+ Ok(()) => Ok(()),
+ Err(mpsc::error::TrySendError::Full(_)) => {
+ // if the channel is full it means the consuming client is too slow and unable to keep
+ // up. we terminate the subscription without an error message because it anyways cant go through
+ warn!(subscription_id = %subscription_id, "Subscription client is too slow");
+ drop(subscription);
+ callback_subscriptions.remove(subscription_id);
+ Err(CallbackError::ClientTooSlow {
+ subscription_id: subscription_id.to_string(),
+ })
+ }
+ Err(mpsc::error::TrySendError::Closed(_)) => {
+ debug!(subscription_id = %subscription_id, "Subscription receiver dropped");
+ drop(subscription);
+ callback_subscriptions.remove(subscription_id);
+ Err(CallbackError::SubscriptionDropped {
+ subscription_id: subscription_id.to_string(),
+ })
+ }
+ }
+}
+
+fn handle_complete(
+ subscription_id: &str,
+ payload: &CallbackPayload<'_>,
+ subscription: Ref<'_, String, CallbackSubscription>,
+ callback_subscriptions: &CallbackSubscriptionsMap,
+) {
+ trace!(subscription_id = %subscription_id, "Received complete message");
+ // if the buffer is full or closed we ignore and remove the subscription, we dont send
+ // the final error message because the client is already unable to consume
+ let _ = subscription.sender.try_send(CallbackMessage::Complete {
+ errors: payload.errors.clone(),
+ });
+ drop(subscription);
+ callback_subscriptions.remove(subscription_id);
+}
+
+pub async fn handler(
+ req: HttpRequest,
+ path: Path,
+ body: Bytes,
+ callback_subscriptions: web::types::State,
+) -> Result {
+ let subscription_id_from_path = path.into_inner();
+
+ validate_protocol(&req)?;
+
+ let payload = parse_payload(&body)?;
+
+ validate_payload(&payload, &subscription_id_from_path)?;
+
+ let subscription = match callback_subscriptions.get(&payload.id) {
+ Some(sub) => sub,
+ None => {
+ return Err(CallbackError::SubscriptionNotFound {
+ subscription_id: payload.id.clone(),
+ });
+ }
+ };
+
+ if subscription.verifier != payload.verifier {
+ return Err(CallbackError::InvalidVerifier {
+ subscription_id: payload.id.clone(),
+ });
+ }
+
+ match payload.action {
+ CallbackAction::Check => handle_check(&payload.id, &subscription),
+ CallbackAction::Next => {
+ handle_next(&payload.id, &payload, subscription, &callback_subscriptions)?;
+ }
+ CallbackAction::Complete => {
+ handle_complete(&payload.id, &payload, subscription, &callback_subscriptions)
+ }
+ };
+
+ Ok(HttpResponse::NoContent()
+ .header(SUBSCRIPTION_PROTOCOL_HEADER, CALLBACK_PROTOCOL_VERSION)
+ .finish())
+}
diff --git a/bin/router/src/pipeline/long_lived_client_limit.rs b/bin/router/src/pipeline/long_lived_client_limit.rs
new file mode 100644
index 000000000..fc65768b8
--- /dev/null
+++ b/bin/router/src/pipeline/long_lived_client_limit.rs
@@ -0,0 +1,191 @@
+use std::{
+ rc::Rc,
+ sync::{
+ atomic::{AtomicUsize, Ordering},
+ Arc,
+ },
+ task::{Context, Poll},
+};
+
+use http::{header, StatusCode};
+use ntex::{
+ http::body::{Body, BodySize, MessageBody},
+ service::{Service, ServiceCtx},
+ util::Bytes,
+ web::{self, DefaultError},
+ Middleware, SharedCfg,
+};
+
+use crate::RouterSharedState;
+
+#[derive(Clone)]
+pub struct LongLivedClientLimitService {
+ // false means the middleware is entirely bypassed on every request
+ enabled: bool,
+}
+
+impl LongLivedClientLimitService {
+ pub fn new(router_config: &hive_router_config::HiveRouterConfig) -> Self {
+ let limit = router_config.traffic_shaping.router.max_long_lived_clients;
+ let has_long_lived = router_config.subscriptions.enabled || router_config.websocket.enabled;
+ Self {
+ enabled: limit > 0 && has_long_lived,
+ }
+ }
+}
+
+impl Middleware for LongLivedClientLimitService {
+ type Service = LongLivedClientLimitMiddleware;
+
+ fn create(&self, service: S, _cfg: SharedCfg) -> Self::Service {
+ LongLivedClientLimitMiddleware {
+ service,
+ enabled: self.enabled,
+ }
+ }
+}
+
+pub struct LongLivedClientLimitMiddleware {
+ service: S,
+ enabled: bool,
+}
+
+impl Service> for LongLivedClientLimitMiddleware
+where
+ S: Service, Response = web::WebResponse, Error = web::Error>,
+{
+ type Response = web::WebResponse;
+ type Error = S::Error;
+
+ ntex::forward_ready!(service);
+
+ async fn call(
+ &self,
+ req: web::WebRequest,
+ ctx: ServiceCtx<'_, Self>,
+ ) -> Result {
+ if !self.enabled {
+ return ctx.call(&self.service, req).await;
+ }
+
+ if !is_long_lived_request(req.headers()) {
+ return ctx.call(&self.service, req).await;
+ }
+
+ let shared_state = match req.app_state::>() {
+ Some(s) => s,
+ None => return ctx.call(&self.service, req).await,
+ };
+
+ let limit = shared_state
+ .router_config
+ .traffic_shaping
+ .router
+ .max_long_lived_clients;
+ let counter = shared_state.long_lived_client_count.clone();
+
+ // try to reserve a slot, bail if at the limit
+ let prev = counter.fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
+ if current < limit {
+ Some(current + 1)
+ } else {
+ None
+ }
+ });
+ if prev.is_err() {
+ let error_response = web::HttpResponse::build(StatusCode::SERVICE_UNAVAILABLE)
+ .header(header::RETRY_AFTER, "5")
+ .body("Too many long-lived clients");
+ return Ok(req.into_response(error_response));
+ }
+
+ let guard = LongLivedClientGuard(counter);
+ let response = ctx.call(&self.service, req).await?;
+
+ // wrap the body so the guard lives until the stream is fully consumed
+ let response = response.map_body(|_head, body| {
+ let wrapped = GuardedBody {
+ inner: body.into_body().into(),
+ _guard: guard,
+ };
+ Body::from_message(wrapped).into()
+ });
+
+ Ok(response)
+ }
+}
+
+// decrements the counter when dropped
+struct LongLivedClientGuard(Arc);
+
+impl Drop for LongLivedClientGuard {
+ fn drop(&mut self) {
+ self.0.fetch_sub(1, Ordering::AcqRel);
+ }
+}
+
+// wraps the body and keeps the guard alive until it's fully consumed and dropped.
+// one extra vtable call per chunk on top of the Box dispatch streaming bodies
+// already go through - negligible next to actual I/O cost per chunk.
+struct GuardedBody {
+ inner: Body,
+ _guard: LongLivedClientGuard,
+}
+
+impl MessageBody for GuardedBody {
+ fn size(&self) -> BodySize {
+ self.inner.size()
+ }
+
+ fn poll_next_chunk(
+ &mut self,
+ cx: &mut Context<'_>,
+ ) -> Poll>>> {
+ self.inner.poll_next_chunk(cx)
+ }
+}
+
+// cheapest check first:
+// 1. upgrade: websocket - two header lookups, no parsing
+// 2. accept: streaming - one header lookup + fast substring pre-filter, full parse only if needed
+#[inline]
+fn is_long_lived_request(headers: &ntex::http::HeaderMap) -> bool {
+ // websocket: Connection: Upgrade + Upgrade: websocket
+ // both headers must be present and contain the expected values (case-insensitive)
+ if headers
+ .get(header::UPGRADE)
+ .and_then(|v| v.to_str().ok())
+ .is_some_and(|v| v.eq_ignore_ascii_case("websocket"))
+ && headers
+ .get(header::CONNECTION)
+ .and_then(|v| v.to_str().ok())
+ .is_some_and(|v| v.to_ascii_lowercase().contains("upgrade"))
+ {
+ return true;
+ }
+
+ // fast substring scan before full Accept parse to avoid cost on regular requests
+ let accept = match headers.get(header::ACCEPT).and_then(|v| v.to_str().ok()) {
+ Some(v) if !v.is_empty() => v,
+ _ => return false,
+ };
+
+ if !looks_like_streaming_accept(accept) {
+ return false;
+ }
+
+ use crate::pipeline::header::StreamContentType;
+ use headers_accept::Accept;
+ use std::str::FromStr;
+
+ Accept::from_str(accept)
+ .ok()
+ .and_then(|a| a.negotiate(StreamContentType::media_types().iter()))
+ .is_some()
+}
+
+#[inline]
+fn looks_like_streaming_accept(accept: &str) -> bool {
+ // covers: multipart/mixed, text/event-stream
+ accept.contains("multipart") || accept.contains("event-stream")
+}
diff --git a/bin/router/src/pipeline/mod.rs b/bin/router/src/pipeline/mod.rs
index ee6722dca..352365da0 100644
--- a/bin/router/src/pipeline/mod.rs
+++ b/bin/router/src/pipeline/mod.rs
@@ -1,33 +1,37 @@
-use std::{
- collections::HashMap,
- hash::{Hash, Hasher},
- sync::Arc,
- time::Instant,
-};
-use tracing::{error, Instrument};
-use xxhash_rust::xxh3::Xxh3;
-
+use futures::StreamExt;
use hive_router_internal::telemetry::traces::spans::{
graphql::GraphQLOperationSpan, http_request::HttpServerRequestSpan,
};
use hive_router_plan_executor::{
execution::{
client_request_details::{ClientRequestDetails, JwtRequestDetails, OperationDetails},
- plan::PlanExecutionOutput,
+ plan::QueryPlanExecutionResult,
},
- hooks::on_graphql_params::GraphQLParams,
- hooks::on_supergraph_load::SupergraphData,
+ hooks::{on_graphql_params::GraphQLParams, on_supergraph_load::SupergraphData},
plugin_context::{PluginContext, PluginRequestState},
};
use hive_router_query_planner::{
state::supergraph_state::OperationKind, utils::cancellation::CancellationToken,
};
use http::{header::CONTENT_TYPE, Method};
-use ntex::web::{self, HttpRequest};
+use ntex::{
+ http::HeaderMap,
+ rt,
+ web::{self, HttpRequest},
+};
use sonic_rs::{JsonContainerTrait, JsonType, JsonValueTrait, Value};
+use std::{
+ collections::HashMap,
+ hash::{Hash, Hasher},
+ sync::Arc,
+ time::Instant,
+};
+use tracing::{error, Instrument};
+use xxhash_rust::xxh3::Xxh3;
use crate::{
pipeline::{
+ active_subscriptions::SubscriptionEvent,
authorization::enforce_operation_authorization,
body_read::read_body_stream,
coerce_variables::{coerce_request_variables, CoerceVariablesPayload},
@@ -35,7 +39,7 @@ use crate::{
error::PipelineError,
execution::{execute_plan, PlannedRequest},
execution_request::{deserialize_graphql_params, DeserializationResult, GetQueryStr},
- header::{RequestAccepts, ResponseMode, SingleContentType, TEXT_HTML_MIME},
+ header::{RequestAccepts, ResponseMode, TEXT_HTML_MIME},
introspection_policy::handle_introspection_policy,
normalize::{normalize_request_with_cache, GraphQLNormalizationPayload},
parser::{parse_operation_with_cache, ParseResult},
@@ -48,12 +52,16 @@ use crate::{
validation::validate_operation_with_cache,
},
schema_state::SchemaState,
- shared_state::{RouterRequestDedupeHeaderPolicy, RouterSharedState, SharedRouterResponse},
+ shared_state::{
+ RouterRequestDedupeHeaderPolicy, RouterSharedState, SharedRouterResponse,
+ SharedRouterResponseGuard, SharedRouterSingleResponse, SharedRouterStreamResponse,
+ },
LABORATORY_HTML,
};
use hive_router_internal::telemetry::metrics::catalog::values::GraphQLResponseStatus;
+pub mod active_subscriptions;
pub mod authorization;
pub mod body_read;
pub mod coerce_variables;
@@ -63,15 +71,20 @@ pub mod error;
pub mod execution;
pub mod execution_request;
pub mod header;
+pub mod http_callback;
pub mod introspection_policy;
+pub mod long_lived_client_limit;
+pub mod multipart_subscribe;
pub mod normalize;
pub mod parser;
pub mod progressive_override;
pub mod query_plan;
pub mod request_extensions;
+pub mod sse;
pub mod timeout;
pub mod usage_reporting;
pub mod validation;
+pub mod websocket_server;
#[inline]
pub async fn graphql_request_handler(
@@ -106,6 +119,7 @@ pub async fn graphql_request_handler(
let started_at = Instant::now();
let operation_span = GraphQLOperationSpan::new();
+ let span_clone = operation_span.clone();
async {
perform_csrf_prevention(req, &shared_state.router_config.csrf)?;
@@ -233,24 +247,20 @@ pub async fn graphql_request_handler(
);
if is_subscription
- // coming soon
- // && !response_mode.can_stream()
+ && (!shared_state.router_config.subscriptions.enabled || !response_mode.can_stream())
{
+ // check early, even though we check again planned execution below
return Err(PipelineError::SubscriptionsNotSupported);
}
- let Some(single_content_type) = response_mode.single_content_type() else {
- // streaming responses coming soon
- return Err(PipelineError::UnsupportedContentType);
- };
-
let request_dedupe_enabled =
shared_state.router_config.traffic_shaping.router.dedupe.enabled;
- let shared_response = if request_dedupe_enabled
+ let fingerprint = if request_dedupe_enabled
&& matches!(
normalize_payload.operation_for_plan.operation_kind,
- Some(OperationKind::Query) | None
+ // same deduplication applies for queries and subscriptions
+ None | Some(OperationKind::Query) | Some(OperationKind::Subscription)
) {
let variables_hash = hash_graphql_variables(&graphql_params.variables);
let extensions_hash = graphql_params
@@ -259,47 +269,52 @@ pub async fn graphql_request_handler(
.map_or(0, hash_graphql_extensions);
let schema_checksum = supergraph.schema_checksum();
- let fingerprint = inbound_request_fingerprint(
- req,
+ Some(inbound_request_fingerprint(
+ req.method(),
+ req.path(),
+ req.headers(),
&shared_state.in_flight_requests_header_policy,
schema_checksum,
normalize_payload.normalized_operation_hash,
variables_hash,
extensions_hash,
- );
- let (shared_response, _role) = shared_state
- .in_flight_requests
- .claim(fingerprint)
- .get_or_try_init(|| async {
- execute_planned_request(
- req,
- graphql_params,
- &normalize_payload,
- supergraph,
- shared_state,
- schema_state,
- &operation_span,
- &plugin_req_state,
- single_content_type,
- )
- .await
- })
- .await?;
+ ))
+ } else {
+ None
+ };
+
+ let exec = |guard| execute_planned_request(
+ req.method(),
+ req.uri(),
+ req.headers(),
+ graphql_params,
+ &normalize_payload,
+ supergraph,
+ shared_state,
+ schema_state,
+ operation_span,
+ plugin_req_state,
+ response_mode,
+ guard,
+ );
+ let shared_response = if let Some(fp) = fingerprint {
+ let (shared_response, _role) = if is_subscription {
+ shared_state
+ .in_flight_requests
+ .claim(fp)
+ .get_or_try_init_with_guard(|guard| exec(Some(guard)))
+ .await?
+ } else {
+ shared_state
+ .in_flight_requests
+ .claim(fp)
+ .get_or_try_init(|| exec(None))
+ .await?
+ };
Arc::unwrap_or_clone(shared_response)
} else {
- execute_planned_request(
- req,
- graphql_params,
- &normalize_payload,
- supergraph,
- shared_state,
- schema_state,
- &operation_span,
- &plugin_req_state,
- single_content_type,
- )
- .await?
+ exec(None).await?
};
if let Some(hive_usage_agent) = &shared_state.hive_usage_agent {
@@ -323,26 +338,23 @@ pub async fn graphql_request_handler(
// Thus, this expect should never panic.
"Expected Usage Reporting options to be present when Hive Usage Agent is initialized",
),
- shared_response.error_count,
+ shared_response.error_count(),
)
.await;
}
- let pipeline_error_count = shared_response.error_count;
- let response = shared_response.into();
-
write_graphql_response_metric_status(
req,
- if pipeline_error_count > 0 {
+ if shared_response.error_count() > 0 {
GraphQLResponseStatus::Error
} else {
GraphQLResponseStatus::Ok
},
);
- Ok(response)
+ shared_response.into_response(response_mode)
}
- .instrument(operation_span.clone())
+ .instrument(span_clone)
.await
.inspect_err(|_| {
write_graphql_response_metric_status(req, GraphQLResponseStatus::Error);
@@ -350,20 +362,23 @@ pub async fn graphql_request_handler(
}
#[allow(clippy::too_many_arguments)]
-async fn execute_planned_request<'exec>(
- req: &'exec HttpRequest,
+pub async fn execute_planned_request<'exec>(
+ method: &'exec Method,
+ url: &'exec http::Uri,
+ headers: &'exec HeaderMap,
mut graphql_params: GraphQLParams,
normalize_payload: &Arc,
supergraph: &'exec SupergraphData,
shared_state: &'exec Arc,
schema_state: &'exec Arc,
- operation_span: &'exec GraphQLOperationSpan,
- plugin_req_state: &'exec Option>,
- single_content_type: &'exec SingleContentType,
+ operation_span: GraphQLOperationSpan,
+ plugin_req_state: Option>,
+ response_mode: &'exec ResponseMode,
+ guard: Option,
) -> Result {
let jwt_request_details = match &shared_state.jwt_auth_runtime {
Some(jwt_auth_runtime) => match jwt_auth_runtime
- .validate_headers(req.headers(), &shared_state.jwt_claims_cache)
+ .validate_headers(headers, &shared_state.jwt_claims_cache)
.await?
{
Some(jwt_context) => JwtRequestDetails::Authenticated {
@@ -381,9 +396,9 @@ async fn execute_planned_request<'exec>(
coerce_request_variables(supergraph, &mut graphql_params.variables, normalize_payload)?;
let client_request_details = ClientRequestDetails {
- method: req.method(),
- url: req.uri(),
- headers: req.headers(),
+ method,
+ url,
+ headers,
operation: OperationDetails {
name: normalize_payload.operation_for_plan.name.as_deref(),
kind: match normalize_payload.operation_for_plan.operation_kind {
@@ -394,55 +409,101 @@ async fn execute_planned_request<'exec>(
},
query: graphql_params.get_query()?,
},
- jwt: jwt_request_details,
- };
+ jwt: jwt_request_details.into(),
+ }
+ .into();
- let pipeline_result = execute_pipeline(
+ match execute_pipeline(
&client_request_details,
normalize_payload,
- &variable_payload,
+ variable_payload,
supergraph,
shared_state,
schema_state,
operation_span,
plugin_req_state,
)
- .await?;
-
- let error_count = pipeline_result.error_count;
- let mut response_builder = web::HttpResponse::Ok();
+ .await?
+ {
+ QueryPlanExecutionResult::Stream(result) => {
+ // we dont use the stream content type because subscriptions
+ // can be deduplicated across transports - but we do store
+ // the header value in the shared response because the user
+ // might choose to not deduplicate across transport boundaries
+ let stream_content_type = response_mode
+ .stream_content_type()
+ .ok_or(PipelineError::SubscriptionsTransportNotSupported)?;
+
+ let (producer_handle, receiver) = shared_state.active_subscriptions.register(guard);
+
+ // subscribe the sender before spawning the pump so the channel always has
+ // at least one receiver - prevents events from being lost in the window
+ // between spawn and the consumer calling subscribe()
+ let sender = producer_handle.sender().clone();
+
+ let mut body_stream = result.body;
+ rt::spawn(async move {
+ while let Some(chunk) = body_stream.next().await {
+ if !producer_handle.send(SubscriptionEvent::Raw(bytes::Bytes::from(chunk))) {
+ // all receivers gone, stop draining
+ break;
+ }
+ }
+ // dropping producer_handle closes the broadcast channel
+ });
- if let Some(response_headers_aggregator) = pipeline_result.response_headers_aggregator {
- response_headers_aggregator.modify_client_response_headers(&mut response_builder)?;
+ let mut builder = web::HttpResponse::Ok();
+ if let Some(aggregator) = result.response_headers_aggregator {
+ aggregator.modify_client_response_headers(&mut builder)?;
+ };
+ builder.content_type(stream_content_type.as_ref());
+ let headers = Arc::new(builder.finish().headers().clone());
+
+ Ok(SharedRouterResponse::Stream(SharedRouterStreamResponse {
+ body: sender,
+ headers,
+ error_count: result.error_count,
+ receiver: Some(receiver),
+ }))
+ }
+ QueryPlanExecutionResult::Single(result) => {
+ let single_content_type = response_mode.
+ single_content_type().
+ // TODO: streaming single responses
+ ok_or(PipelineError::UnsupportedContentType)?.
+ clone();
+
+ // drop the `guard` as soon as the response is ready
+
+ let mut builder = web::HttpResponse::Ok();
+ if let Some(aggregator) = result.response_headers_aggregator {
+ aggregator.modify_client_response_headers(&mut builder)?;
+ };
+ builder.content_type(single_content_type.as_ref());
+ let headers = Arc::new(builder.finish().headers().clone());
+
+ Ok(SharedRouterResponse::Single(SharedRouterSingleResponse {
+ body: ntex::util::Bytes::from(result.body),
+ headers,
+ status: result.status_code,
+ error_count: result.error_count,
+ }))
+ }
}
-
- let body = ntex::util::Bytes::from(pipeline_result.body);
-
- let response = response_builder
- .content_type(single_content_type.as_ref())
- .status(pipeline_result.status_code)
- .body(body.clone());
-
- Ok(SharedRouterResponse {
- body,
- headers: Arc::new(response.headers().clone()),
- status: response.status(),
- error_count,
- })
}
#[inline]
#[allow(clippy::too_many_arguments)]
pub async fn execute_pipeline<'exec>(
- client_request_details: &ClientRequestDetails<'exec>,
+ client_request_details: &Arc>,
normalize_payload: &Arc,
- variable_payload: &CoerceVariablesPayload,
+ variable_payload: CoerceVariablesPayload,
supergraph: &SupergraphData,
shared_state: &Arc,
schema_state: &Arc,
- operation_span: &GraphQLOperationSpan,
- plugin_req_state: &Option>,
-) -> Result {
+ operation_span: GraphQLOperationSpan,
+ plugin_req_state: Option>,
+) -> Result {
if normalize_payload.operation_for_introspection.is_some() {
handle_introspection_policy(&shared_state.introspection_policy, client_request_details)?;
}
@@ -460,7 +521,7 @@ pub async fn execute_pipeline<'exec>(
normalize_payload,
&supergraph.authorization,
&supergraph.metadata,
- variable_payload,
+ &variable_payload,
&client_request_details.jwt,
)?;
@@ -470,22 +531,22 @@ pub async fn execute_pipeline<'exec>(
&normalize_payload,
&progressive_override_ctx,
&cancellation_token,
- plugin_req_state,
+ &plugin_req_state,
)
.await?;
let query_plan_payload = match query_plan_result {
QueryPlanResult::QueryPlan(plan) => plan,
QueryPlanResult::EarlyResponse(response) => {
- return Ok(response);
+ return Ok(QueryPlanExecutionResult::Single(response));
}
};
let planned_request = PlannedRequest {
- normalized_payload: &normalize_payload,
+ normalized_payload: normalize_payload,
query_plan_payload: &query_plan_payload,
variable_payload,
- client_request_details,
+ client_request_details: client_request_details.clone(),
authorization_errors,
plugin_req_state,
};
@@ -493,8 +554,11 @@ pub async fn execute_pipeline<'exec>(
execute_plan(supergraph, shared_state, planned_request, operation_span).await
}
-fn inbound_request_fingerprint(
- req: &HttpRequest,
+#[allow(clippy::too_many_arguments)]
+pub fn inbound_request_fingerprint(
+ method: &http::Method,
+ path: &str,
+ request_headers: &HeaderMap,
dedupe_header_policy: &RouterRequestDedupeHeaderPolicy,
schema_checksum: u64,
normalized_operation_hash: u64,
@@ -503,8 +567,7 @@ fn inbound_request_fingerprint(
) -> u64 {
let mut hasher = Xxh3::new();
- let mut headers: Vec<(&str, &str)> = req
- .headers()
+ let mut headers: Vec<(&str, &str)> = request_headers
.iter()
.filter(|(name, _)| dedupe_header_policy.should_include(name.as_str()))
.filter_map(|(name, value)| value.to_str().ok().map(|v_str| (name.as_str(), v_str)))
@@ -515,8 +578,8 @@ fn inbound_request_fingerprint(
.then_with(|| left_value.cmp(right_value))
});
- req.method().hash(&mut hasher);
- req.path().hash(&mut hasher);
+ method.hash(&mut hasher);
+ path.hash(&mut hasher);
headers.hash(&mut hasher);
schema_checksum.hash(&mut hasher);
normalized_operation_hash.hash(&mut hasher);
@@ -526,7 +589,7 @@ fn inbound_request_fingerprint(
hasher.finish()
}
-fn hash_graphql_variables(variables: &HashMap) -> u64 {
+pub fn hash_graphql_variables(variables: &HashMap) -> u64 {
let mut hasher = Xxh3::new();
let mut keys: Vec<&str> = variables.keys().map(String::as_str).collect();
@@ -543,7 +606,7 @@ fn hash_graphql_variables(variables: &HashMap) -> u64 {
hasher.finish()
}
-fn hash_graphql_extensions(extensions: &HashMap) -> u64 {
+pub fn hash_graphql_extensions(extensions: &HashMap) -> u64 {
// reused as hash_graphql_variables has the same function signature
hash_graphql_variables(extensions)
}
diff --git a/bin/router/src/pipeline/multipart_subscribe.rs b/bin/router/src/pipeline/multipart_subscribe.rs
new file mode 100644
index 000000000..0a3317a41
--- /dev/null
+++ b/bin/router/src/pipeline/multipart_subscribe.rs
@@ -0,0 +1,133 @@
+// TODO: test thoroughly
+
+use const_str::concat;
+
+use futures_util::{Stream, StreamExt};
+use ntex::util::Bytes;
+use std::time::Duration;
+use tokio_util::bytes::BufMut;
+
+// we use macros to retain constness
+macro_rules! make_content_type {
+ ($boundary:expr) => {
+ // wrapping with "" is not necessary in our case since the boundaries we use do not contain special
+ // characters - but clients out there probably rely on the quotes so we add them just in case
+ concat!("multipart/mixed;boundary=\"", $boundary, "\"")
+ };
+}
+macro_rules! make_boundaries {
+ ($boundary:expr) => {
+ (
+ // start
+ concat!(
+ "--",
+ $boundary,
+ "\r\nContent-Type: application/json\r\n\r\n"
+ ),
+ // end
+ concat!("--", $boundary, "--"),
+ )
+ };
+}
+
+const INCREMENTAL_DELIVERY_BOUNDARY: &str = "-";
+
+pub const INCREMENTAL_DELIVERY_CONTENT_TYPE: &str =
+ make_content_type!(INCREMENTAL_DELIVERY_BOUNDARY);
+
+/// Create a multipart subscription stream following the Official GraphQL over HTTP Incremental Delivery RFC.
+///
+/// Will use `-` as boundary.
+///
+/// NOTE: Incremental Delivery over HTTP does not support heartbeats. Please prefer Apollo's multiple HTTP where applicable.
+///
+/// Read more: https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol
+pub fn create_incremental_delivery_stream(
+ input: impl Stream- > + Send + Unpin + 'static,
+) -> impl Stream
- > + Unpin {
+ let mut input = input;
+ let (start_boundary, end_boundary) = make_boundaries!(INCREMENTAL_DELIVERY_BOUNDARY);
+ async_stream::stream! {
+ loop {
+ match input.next().await {
+ Some(resp) => {
+ match std::str::from_utf8(&resp) {
+ Ok(_) => {
+ yield Ok(Bytes::from(start_boundary));
+ yield Ok(Bytes::from(resp));
+ yield Ok(Bytes::from("\r\n"));
+ }
+ Err(e) => {
+ yield Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e));
+ break;
+ }
+ }
+ }
+ None => {
+ yield Ok(Bytes::from(end_boundary));
+ break;
+ },
+ }
+ }
+ }
+ .boxed()
+}
+
+const APOLLO_MULTIPART_HTTP_BOUNDARY: &str = "graphql";
+
+pub const APOLLO_MULTIPART_HTTP_CONTENT_TYPE: &str = concat!(
+ make_content_type!(APOLLO_MULTIPART_HTTP_BOUNDARY),
+ ";subscriptionSpec=1.0"
+);
+
+/// Create a multipart subscription stream following Apollo's Multipart HTTP spec.
+///
+/// Will use `graphql` as boundary.
+///
+/// Read more: https://github.com/graphql/graphql-over-http/blob/d312e43384006fa323b918d49cfd9fbd76ac1257/rfcs/IncrementalDelivery.md
+pub fn create_apollo_multipart_http_stream(
+ input: impl Stream
- > + Send + Unpin + 'static,
+ heartbeat_interval: Duration,
+) -> impl Stream
- > + Unpin {
+ let mut input = input;
+ let (start_boundary, end_boundary) = make_boundaries!(APOLLO_MULTIPART_HTTP_BOUNDARY);
+ let ping = "{}\r\n";
+ async_stream::stream! {
+ loop {
+ tokio::select! {
+ item = input.next() => {
+ match item {
+ Some(resp) => {
+ match std::str::from_utf8(&resp) {
+ Ok(_) => {
+ yield Ok(Bytes::from(start_boundary));
+ // Wrap the GraphQL response in a payload field
+ // As per the spec.
+ let mut payload = ntex::util::BytesMut::with_capacity(resp.len() + 15);
+ payload.put_slice(br#"{"payload":"#);
+ payload.put_slice(&resp);
+ payload.put_slice(br"}");
+ yield Ok(payload.freeze());
+ yield Ok(Bytes::from("\r\n"));
+ }
+ Err(e) => {
+ // TODO: use transport level errors as per spec
+ yield Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e));
+ break;
+ }
+ }
+ }
+ None => {
+ yield Ok(Bytes::from(end_boundary));
+ break;
+ },
+ }
+ }
+ _ = tokio::time::sleep(heartbeat_interval) => {
+ yield Ok(Bytes::from(start_boundary));
+ yield Ok(Bytes::from(ping));
+ }
+ }
+ }
+ }.boxed()
+}
diff --git a/bin/router/src/pipeline/sse.rs b/bin/router/src/pipeline/sse.rs
new file mode 100644
index 000000000..9c9acb7d4
--- /dev/null
+++ b/bin/router/src/pipeline/sse.rs
@@ -0,0 +1,48 @@
+// TODO: test thoroughly
+
+use futures_util::{Stream, StreamExt};
+use ntex::util::Bytes;
+use std::time::Duration;
+use tokio_util::bytes::BufMut;
+
+pub const SSE_HEADER: &str = "text/event-stream";
+
+pub fn create_stream(
+ input: impl Stream
- > + Send + Unpin + 'static,
+ heartbeat_interval: Duration,
+) -> impl Stream
- > + Unpin {
+ let mut input = input;
+ async_stream::stream! {
+ loop {
+ tokio::select! {
+ item = input.next() => {
+ match item {
+ Some(resp) => {
+ match std::str::from_utf8(&resp) {
+ Ok(json_str) => {
+ let mut sse_event = ntex::util::BytesMut::with_capacity(json_str.len() + 25);
+ sse_event.put_slice(b"event: next\ndata: ");
+ sse_event.put_slice(json_str.as_bytes());
+ sse_event.put_slice(b"\n\n");
+ yield Ok(sse_event.freeze());
+ }
+ Err(e) => {
+ yield Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e));
+ break;
+ }
+ }
+ }
+ None => {
+ yield Ok(Bytes::from("event: complete\n\n"));
+ break;
+ },
+ }
+ }
+ _ = tokio::time::sleep(heartbeat_interval) => {
+ yield Ok(Bytes::from(":\n\n"));
+ }
+ }
+ }
+ }
+ .boxed()
+}
diff --git a/bin/router/src/pipeline/websocket_server.rs b/bin/router/src/pipeline/websocket_server.rs
new file mode 100644
index 000000000..03c3d4394
--- /dev/null
+++ b/bin/router/src/pipeline/websocket_server.rs
@@ -0,0 +1,754 @@
+use http::Method;
+use ntex::channel::oneshot;
+use ntex::http::{header::HeaderName, header::HeaderValue, HeaderMap};
+use ntex::router::Path;
+use ntex::service::{fn_factory_with_config, fn_service, fn_shutdown, Service};
+use ntex::web::{self, ws, Error, HttpRequest, HttpResponse};
+use ntex::{chain, rt};
+use sonic_rs::{JsonContainerTrait, JsonValueTrait, Value};
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::io;
+use std::rc::Rc;
+use std::sync::Arc;
+use std::time::Instant;
+use tokio::sync::mpsc;
+use tracing::{debug, error, trace, warn, Instrument};
+
+use hive_router_internal::telemetry::traces::spans::graphql::GraphQLOperationSpan;
+use hive_router_plan_executor::executors::graphql_transport_ws::{
+ ClientMessage, CloseCode, ConnectionInitPayload, ServerMessage, WS_SUBPROTOCOL,
+};
+use hive_router_plan_executor::executors::websocket_common::{
+ handshake_timeout, heartbeat, parse_frame_to_text, FrameNotParsedToText, WsState,
+};
+use hive_router_plan_executor::hooks::on_graphql_params::GraphQLParams;
+use hive_router_plan_executor::plugin_context::{
+ PluginContext, PluginRequestState, RouterHttpRequest,
+};
+use hive_router_plan_executor::response::graphql_error::{GraphQLError, GraphQLErrorExtensions};
+use hive_router_query_planner::state::supergraph_state::OperationKind;
+
+use crate::jwt::errors::JwtError;
+use crate::pipeline::active_subscriptions::SubscriptionEvent;
+use crate::pipeline::error::PipelineError;
+use crate::pipeline::execute_planned_request;
+use crate::pipeline::header::{ResponseMode, SingleContentType, StreamContentType};
+use crate::pipeline::{
+ hash_graphql_extensions, hash_graphql_variables, inbound_request_fingerprint,
+ normalize::normalize_request_with_cache, parser::parse_operation_with_cache, usage_reporting,
+ validation::validate_operation_with_cache,
+};
+use crate::schema_state::SchemaState;
+use crate::shared_state::{RouterSharedState, SharedRouterResponse};
+
+type WsStateRef = Rc
>>>;
+
+pub async fn ws_index(
+ req: HttpRequest,
+ schema_state: web::types::State>,
+ shared_state: web::types::State>,
+) -> Result {
+ let schema_state = schema_state.get_ref().clone();
+ let shared_state = shared_state.get_ref().clone();
+
+ let accepted_subprotocol = ws::subprotocols(&req)
+ .find(|p| *p == WS_SUBPROTOCOL)
+ .map(|_| WS_SUBPROTOCOL);
+
+ let plugin_context = req.extensions().get::>().cloned();
+
+ ws::start(
+ req,
+ accepted_subprotocol,
+ fn_factory_with_config(move |sink: ws::WsSink| {
+ let schema_state = schema_state.clone();
+ let shared_state = shared_state.clone();
+ let plugin_context = plugin_context.clone();
+ async move {
+ ws_service(
+ accepted_subprotocol.is_some(),
+ sink,
+ schema_state,
+ shared_state,
+ plugin_context,
+ )
+ .await
+ }
+ }),
+ )
+ .await
+}
+
+async fn ws_service(
+ has_accepted_subprotocol: bool,
+ sink: ws::WsSink,
+ schema_state: Arc,
+ shared_state: Arc,
+ plugin_context: Option>,
+) -> Result, Error = io::Error>, web::Error>
+{
+ if !has_accepted_subprotocol {
+ debug!("WebSocket connection rejecting due to unacceptable subprotocol");
+ let _ = sink.send(CloseCode::SubprotocolNotAcceptable.into()).await;
+ // we dont return an Err here because we want to gracefully close the
+ // connection for the client side with a close frame. returning an Err
+ // would result in an abrupt termination of the connection
+ } else {
+ debug!("WebSocket connection accepted");
+ }
+
+ let ws_uri: Rc = Rc::new(
+ shared_state
+ .router_config
+ .websocket_path()
+ .expect("websocket path must exist because the websocket handler wouldn't have been mounted otherwise")
+ .parse()
+ .unwrap_or_else(|_| http::Uri::from_static("/graphql")),
+ );
+ let ws_path: Rc> = Rc::new(Path::new((*ws_uri).clone()));
+
+ let (heartbeat_tx, heartbeat_rx) = oneshot::channel();
+ let (acknowledged_tx, acknowledged_rx) = oneshot::channel();
+
+ let state: WsStateRef = Rc::new(RefCell::new(WsState::new(acknowledged_tx)));
+
+ rt::spawn(heartbeat(state.clone(), sink.clone(), heartbeat_rx));
+ rt::spawn(handshake_timeout(
+ state.clone(),
+ sink.clone(),
+ acknowledged_rx,
+ CloseCode::ConnectionInitTimeout,
+ ));
+
+ let state_for_service = state.clone();
+ let service = fn_service(move |frame| {
+ let sink = sink.clone();
+ let state = state_for_service.clone();
+ let schema_state = schema_state.clone();
+ let shared_state = shared_state.clone();
+ let plugin_context = plugin_context.clone();
+ let ws_uri = ws_uri.clone();
+ let ws_path = ws_path.clone();
+ async move {
+ match parse_frame_to_text(frame, &state) {
+ Ok(text) => Ok(handle_text_frame(
+ text,
+ sink,
+ state,
+ &schema_state,
+ &shared_state,
+ plugin_context,
+ &ws_uri,
+ &ws_path,
+ )
+ .await),
+ Err(FrameNotParsedToText::Message(msg)) => Ok(Some(msg)),
+ Err(FrameNotParsedToText::Closed) => {
+ // we dont need to emit anything here because the conneciton is already closed
+ Ok(None)
+ }
+ Err(FrameNotParsedToText::None) => Ok(None),
+ }
+ }
+ });
+
+ let on_shutdown = fn_shutdown(async move || {
+ // stop heartbeat and handshake timeout tasks on shutdown
+ let _ = heartbeat_tx.send(());
+ if let Some(tx) = state.borrow_mut().acknowledged_tx.take() {
+ let _ = tx.send(());
+ }
+ // clearing the map will drop all the senders, which will
+ // in turn cancel all active subscription streams and perform
+ // the cleanup in there
+ state.borrow_mut().subscriptions.clear();
+ });
+
+ Ok(chain(service).and_then(on_shutdown))
+}
+
+/// Ensure a subscription is removed from active subscriptions when dropped (server-side).
+struct SubscriptionGuard {
+ state: WsStateRef,
+ id: String,
+}
+
+impl Drop for SubscriptionGuard {
+ fn drop(&mut self) {
+ self.state.borrow_mut().subscriptions.remove(&self.id);
+ trace!(id = %self.id, "Subscription removed from active subscriptions");
+ }
+}
+
+lazy_static::lazy_static! {
+ static ref WS_URI_PATH: http::Uri = http::Uri::from_static("/graphql");
+}
+
+#[allow(clippy::too_many_arguments)]
+async fn handle_text_frame(
+ text: String,
+ sink: ws::WsSink,
+ state: WsStateRef,
+ schema_state: &Arc,
+ shared_state: &Arc,
+ plugin_context: Option>,
+ ws_uri: &http::Uri,
+ ws_path: &Path,
+) -> Option {
+ let client_msg: ClientMessage = match sonic_rs::from_str(&text) {
+ Ok(msg) => msg,
+ Err(e) => {
+ error!("Failed to parse client message to JSON: {}", e);
+ return Some(CloseCode::BadRequest("Invalid message received from client").into());
+ }
+ };
+
+ trace!("type" = client_msg.as_ref(), "Received client message");
+
+ match client_msg {
+ ClientMessage::ConnectionInit { payload } => {
+ if state.borrow().handshake_received {
+ return Some(CloseCode::TooManyInitialisationRequests.into());
+ }
+ state.borrow_mut().handshake_received = true;
+ state.borrow_mut().init_payload = payload;
+ state.borrow_mut().complete_handshake();
+
+ let _ = sink.send(ServerMessage::ack()).await;
+
+ debug!("Connection acknowledged");
+
+ let header_map =
+ parse_headers_from_connection_init_payload(state.borrow().init_payload.as_ref());
+ if !header_map.is_empty() {
+ trace!("Connection init message contains headers in the payload");
+ } else {
+ trace!("Connection init message does not contain headers in the payload");
+ }
+
+ None
+ }
+ ClientMessage::Subscribe { id, payload } => {
+ if let Some(msg) = state.borrow().check_acknowledged() {
+ return Some(msg);
+ }
+
+ if state.borrow().subscriptions.contains_key(&id) {
+ return Some(CloseCode::SubscriberAlreadyExists(id).into());
+ }
+
+ let started_at = Instant::now();
+ let operation_span = GraphQLOperationSpan::new();
+ let span_clone = operation_span.clone();
+
+ let result = async {
+ let maybe_supergraph = schema_state.current_supergraph();
+ let supergraph = match maybe_supergraph.as_ref() {
+ Some(supergraph) => supergraph,
+ None => {
+ warn!(
+ "No supergraph available yet, unable to process client subscribe message"
+ );
+ return Some(ServerMessage::error(
+ &id,
+ &[GraphQLError::from_message_and_extensions(
+ "No supergraph available yet".to_string(),
+ GraphQLErrorExtensions::new_from_code("SERVICE_UNAVAILABLE"),
+ )],
+ ));
+ }
+ };
+
+ let config = &shared_state.router_config.websocket;
+
+ let connection_init_headers = if config.headers.accepts_connection_headers() {
+ let state_borrow = state.borrow();
+ parse_headers_from_connection_init_payload(state_borrow.init_payload.as_ref())
+ } else {
+ HeaderMap::new()
+ };
+
+ let extensions_headers = if config.headers.accepts_operation_headers() {
+ parse_headers_from_extensions(payload.extensions.as_ref())
+ } else {
+ HeaderMap::new()
+ };
+
+ // merge, extensions have precedence
+ let mut headers = connection_init_headers;
+ for (key, value) in extensions_headers.iter() {
+ headers.insert(key.clone(), value.clone());
+ }
+
+ // store the merged headers back to init_payload if configured to do so
+ if config.headers.persist {
+ if let Some(ref mut init_payload) = state.borrow_mut().init_payload {
+ for (key, value) in headers.iter() {
+ if let Ok(val_str) = value.to_str() {
+ init_payload
+ .fields
+ .insert(key.to_string(), Value::from(val_str));
+ }
+ }
+ }
+ }
+
+ let payload = GraphQLParams {
+ query: Some(payload.query),
+ operation_name: payload.operation_name,
+ variables: payload.variables.unwrap_or_default(),
+ extensions: payload.extensions,
+ };
+
+ // synthetic router http request for plugins - there's no real http request
+ // in the ws subscribe flow, so we assemble one from the ws path and merged headers.
+ // of course there is the http upgrade request, but that one is useless for the plugin system
+ let plugin_req_state = if let (Some(plugins), Some(ref plugin_context)) = (
+ shared_state.plugins.as_ref(),
+ plugin_context,
+ ) {
+ Some(PluginRequestState {
+ plugins: plugins.clone(),
+ router_http_request: RouterHttpRequest {
+ uri: ws_uri,
+ method: &Method::POST,
+ version: http::Version::HTTP_11,
+ headers: &headers,
+ path: ws_uri.path(),
+ query_string: ws_uri.query().unwrap_or(""),
+ match_info: ws_path,
+ },
+ context: plugin_context.clone(),
+ })
+ } else {
+ None
+ };
+
+ let client_name = headers
+ .get(
+ &shared_state
+ .router_config
+ .telemetry
+ .client_identification
+ .name_header,
+ )
+ .and_then(|v| v.to_str().ok());
+ let client_version = headers
+ .get(
+ &shared_state
+ .router_config
+ .telemetry
+ .client_identification
+ .version_header,
+ )
+ .and_then(|v| v.to_str().ok());
+
+ let parser_result =
+ match parse_operation_with_cache(shared_state, &payload, &plugin_req_state).await {
+ Ok(result) => result,
+ Err(err) => return Some(err.into_server_message(&id)),
+ };
+
+ let parser_payload = match parser_result {
+ crate::pipeline::parser::ParseResult::Payload(payload) => payload,
+ crate::pipeline::parser::ParseResult::EarlyResponse(_) => {
+ return Some(ServerMessage::error(
+ &id,
+ &[GraphQLError::from_message_and_code(
+ "Unexpected early response during parse",
+ "INTERNAL_SERVER_ERROR",
+ )],
+ ));
+ }
+ };
+
+ operation_span.record_details(
+ &parser_payload.minified_document,
+ (&parser_payload).into(),
+ client_name,
+ client_version,
+ &parser_payload.hive_operation_hash,
+ );
+
+ match validate_operation_with_cache(
+ supergraph,
+ schema_state,
+ shared_state,
+ &parser_payload,
+ &plugin_req_state,
+ )
+ .await
+ {
+ Ok(Some(_)) => {
+ return Some(ServerMessage::error(
+ &id,
+ &[GraphQLError::from_message_and_code(
+ "Unexpected early response during validation",
+ "INTERNAL_SERVER_ERROR",
+ )],
+ ));
+ }
+ Ok(None) => {}
+ Err(err) => return Some(err.into_server_message(&id)),
+ }
+
+ let normalize_payload = match normalize_request_with_cache(
+ supergraph,
+ schema_state,
+ &payload,
+ &parser_payload,
+ )
+ .await
+ {
+ Ok(payload) => payload,
+ Err(err) => return Some(err.into_server_message(&id)),
+ };
+
+ let is_subscription = matches!(
+ normalize_payload.operation_for_plan.operation_kind,
+ Some(OperationKind::Subscription)
+ );
+
+ if is_subscription && !shared_state.router_config.subscriptions.enabled {
+ return Some(PipelineError::SubscriptionsNotSupported.into_server_message(&id));
+ }
+
+ let request_dedupe_enabled =
+ shared_state.router_config.traffic_shaping.router.dedupe.enabled;
+
+ let fingerprint = if request_dedupe_enabled
+ && matches!(
+ normalize_payload.operation_for_plan.operation_kind,
+ // same deduplication applies for queries and subscriptions
+ None | Some(OperationKind::Query) | Some(OperationKind::Subscription)
+ ) {
+ let variables_hash = hash_graphql_variables(&payload.variables);
+ let extensions_hash = payload
+ .extensions
+ .as_ref()
+ .map_or(0, hash_graphql_extensions);
+ Some(inbound_request_fingerprint(
+ &Method::POST,
+ ws_uri.path(),
+ &headers,
+ &shared_state.in_flight_requests_header_policy,
+ supergraph.schema_checksum(),
+ normalize_payload.normalized_operation_hash,
+ variables_hash,
+ extensions_hash,
+ ))
+ } else {
+ None
+ };
+
+ // synthetic request details for plan executor
+ let response_mode = ResponseMode::Dual(
+ SingleContentType::default(),
+ StreamContentType::default(),
+ );
+ let exec = |guard| execute_planned_request(
+ &Method::POST,
+ ws_uri,
+ &headers,
+ payload,
+ &normalize_payload,
+ supergraph,
+ shared_state,
+ schema_state,
+ operation_span,
+ plugin_req_state,
+ &response_mode,
+ guard,
+ );
+
+ let shared_response = if let Some(fp) = fingerprint {
+ let result = if is_subscription {
+ shared_state
+ .in_flight_requests
+ .claim(fp)
+ .get_or_try_init_with_guard(|guard| exec(Some(guard)))
+ .await
+ } else {
+ shared_state
+ .in_flight_requests
+ .claim(fp)
+ .get_or_try_init(|| exec(None))
+ .await
+ };
+ let (shared_response, _role) = match result {
+ Ok(result) => result,
+ Err(PipelineError::JwtError(err)) => {
+ let _ = sink.send(err.clone().into_server_message(&id)).await;
+ // we report error as graphql error, but we also close the
+ // connection since we're dealing with auth so let's be safe
+ return Some(err.into_close_message());
+ },
+ Err(err) => return Some(err.into_server_message(&id)),
+ };
+ Arc::unwrap_or_clone(shared_response)
+ } else {
+ match exec(None).await {
+ Ok(result) => result,
+ Err(PipelineError::JwtError(err)) => {
+ let _ = sink.send(err.clone().into_server_message(&id)).await;
+ // we report error as graphql error, but we also close the
+ // connection since we're dealing with auth so let's be safe
+ return Some(err.into_close_message());
+ },
+ Err(err) => return Some(err.into_server_message(&id)),
+ }
+ };
+
+ if let Some(hive_usage_agent) = &shared_state.hive_usage_agent {
+ usage_reporting::collect_usage_report(
+ supergraph.supergraph_schema.clone(),
+ started_at.elapsed(),
+ client_name,
+ client_version,
+ normalize_payload.operation_for_plan.name.as_deref(),
+ &parser_payload.minified_document,
+ hive_usage_agent,
+ shared_state
+ .router_config
+ .telemetry
+ .hive
+ .as_ref()
+ .map(|c| &c.usage_reporting)
+ .expect("Expected Usage Reporting options to be present when Hive Usage Agent is initialized"),
+ shared_response.error_count(),
+ )
+ .await;
+ }
+
+ match shared_response {
+ SharedRouterResponse::Single(response) => {
+ let _ = sink.send(ServerMessage::next(&id, &response.body)).await;
+ Some(ServerMessage::complete(&id))
+ }
+ SharedRouterResponse::Stream(response) => {
+ let (cancel_tx, mut cancel_rx) = mpsc::channel::<()>(1);
+
+ state
+ .borrow_mut()
+ .subscriptions
+ .insert(id.clone(), cancel_tx);
+
+ let guard = SubscriptionGuard {
+ state: state.clone(),
+ id: id.clone(),
+ };
+
+ let mut receiver = response
+ .receiver
+ .unwrap_or_else(|| response.body.subscribe());
+
+ trace!(id = %id, "Subscription started");
+
+ let sink = sink.clone();
+ let id_for_loop = id.clone();
+ // must be spawned - blocking the frame handler would prevent
+ // ClientMessage::Complete from being received and processed,
+ // making cancellation impossible
+ rt::spawn(async move {
+ let _guard = guard;
+ let mut cancelled = false;
+
+ loop {
+ tokio::select! {
+ maybe_item = receiver.recv() => {
+ match maybe_item {
+ Ok(SubscriptionEvent::Raw(data)) => {
+ let _ = sink.send(ServerMessage::next(&id_for_loop, &data)).await;
+ }
+ Ok(SubscriptionEvent::Error(errors)) => {
+ let _ = sink.send(ServerMessage::error(&id_for_loop, &errors)).await;
+ break;
+ }
+ Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
+ trace!(id = %id_for_loop, lagged = n, "broadcast receiver lagged, skipping missed messages");
+ continue;
+ }
+ Err(tokio::sync::broadcast::error::RecvError::Closed) => {
+ break;
+ }
+ }
+ }
+ _ = cancel_rx.recv() => {
+ cancelled = true;
+ break;
+ }
+ }
+ }
+
+ if cancelled {
+ trace!(id = %id_for_loop, "Subscription cancelled");
+ } else {
+ trace!(id = %id_for_loop, "Subscription completed");
+ let _ = sink.send(ServerMessage::complete(&id_for_loop)).await;
+ }
+ });
+
+ None
+ }
+ }
+ }
+ .instrument(span_clone)
+ .await;
+
+ result
+ }
+ ClientMessage::Complete { id } => {
+ if let Some(msg) = state.borrow().check_acknowledged() {
+ return Some(msg);
+ }
+
+ if let Some(cancel_tx) = state.borrow_mut().subscriptions.remove(&id) {
+ trace!(id = %id, "Client requested subscription cancellation");
+ let _ = cancel_tx.try_send(());
+ }
+ None
+ }
+ ClientMessage::Ping {} => {
+ // respond with pong always, regardless of acknowledged state
+ // the client should be able to use subprotocol pings/pongs to check liveness
+ Some(ServerMessage::pong())
+ }
+ ClientMessage::Pong {} => None,
+ }
+}
+
+/// Parses headers from a sonic_rs::Object into a HeaderMap. Only stringifiable
+/// values are included; nulls, arrays, objects and other non-primitive values
+/// are ignored.
+fn parse_headers_from_object(headers_obj: &sonic_rs::Object) -> HeaderMap {
+ let mut header_map = HeaderMap::new();
+
+ for (key, value) in headers_obj.iter() {
+ let Ok(name) = HeaderName::try_from(key as &str) else {
+ continue;
+ };
+ let header_value = if let Some(s) = value.as_str() {
+ HeaderValue::try_from(s).ok()
+ } else if let Some(b) = value.as_bool() {
+ Some(HeaderValue::from_static(if b { "true" } else { "false" }))
+ } else if let Some(n) = value.as_i64() {
+ Some(HeaderValue::from(n))
+ } else if let Some(n) = value.as_u64() {
+ Some(HeaderValue::from(n))
+ } else if let Some(f) = value.as_f64() {
+ // f64 has no HeaderValue::From impl, string alloc unavoidable
+ HeaderValue::try_from(f.to_string()).ok()
+ } else {
+ None // ignore nulls, arrays, objects
+ };
+ if let Some(val) = header_value {
+ header_map.insert(name, val);
+ }
+ }
+
+ header_map
+}
+
+fn parse_headers_from_connection_init_payload(
+ payload: Option<&ConnectionInitPayload>,
+) -> HeaderMap {
+ let mut header_map = HeaderMap::new();
+ if let Some(payload) = payload {
+ // First check if there's a nested "headers" object
+ if let Some(headers_prop) = payload.fields.get("headers") {
+ if let Some(headers_obj) = headers_prop.as_object() {
+ header_map = parse_headers_from_object(headers_obj);
+ return header_map;
+ }
+ }
+
+ // If no nested "headers" object, treat all top-level fields as potential headers
+ // Convert the entire fields HashMap to a sonic_rs::Object
+ let mut obj = sonic_rs::Object::new();
+ for (k, v) in payload.fields.iter() {
+ obj.insert(k, v.clone());
+ }
+ header_map = parse_headers_from_object(&obj);
+ }
+ header_map
+}
+
+fn parse_headers_from_extensions(extensions: Option<&HashMap>) -> HeaderMap {
+ let mut header_map = HeaderMap::new();
+ if let Some(ext) = extensions {
+ if let Some(headers_value) = ext.get("headers") {
+ if let Some(headers_obj) = headers_value.as_object() {
+ header_map = parse_headers_from_object(headers_obj);
+ }
+ }
+ }
+ header_map
+}
+
+// NOTE: no `From` trait because it can into ws message and ws closecode but both are ws::Message
+impl PipelineError {
+ fn into_server_message(self, id: &str) -> ws::Message {
+ let code = self.graphql_error_code();
+ let message = self.graphql_error_message();
+
+ let graphql_error = GraphQLError::from_message_and_extensions(
+ message,
+ GraphQLErrorExtensions::new_from_code(code),
+ );
+
+ ServerMessage::error(id, &[graphql_error])
+ }
+}
+
+// NOTE: no `From` trait because it can into ws message and ws closecode but both are ws::Message
+impl JwtError {
+ fn into_server_message(self, id: &str) -> ws::Message {
+ ServerMessage::error(
+ id,
+ &[GraphQLError::from_message_and_code(
+ self.to_string(),
+ self.error_code(),
+ )],
+ )
+ }
+ fn into_close_message(self) -> ws::Message {
+ CloseCode::Forbidden(self.error_code().to_string()).into()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use sonic_rs::json;
+
+ use super::*;
+
+ #[test]
+ fn should_parse_headers_from_object() {
+ let headers_json = json!({
+ "authorization": "Bearer token123",
+ "x-custom-header": "custom-value",
+ "x-number": 42,
+ "x-bool": true,
+ "x-float": 3.14
+ });
+
+ let headers_obj = headers_json.as_object().expect("Failed to get object");
+
+ let headers = parse_headers_from_object(&headers_obj);
+
+ assert_eq!(
+ headers.get("authorization").unwrap().to_str().unwrap(),
+ "Bearer token123"
+ );
+ assert_eq!(
+ headers.get("x-custom-header").unwrap().to_str().unwrap(),
+ "custom-value"
+ );
+ assert_eq!(headers.get("x-number").unwrap().to_str().unwrap(), "42");
+ assert_eq!(headers.get("x-bool").unwrap().to_str().unwrap(), "true");
+ assert_eq!(headers.get("x-float").unwrap().to_str().unwrap(), "3.14");
+ }
+
+ // TODO: hella tests
+}
diff --git a/bin/router/src/schema_state.rs b/bin/router/src/schema_state.rs
index b95f806b4..61c10ed38 100644
--- a/bin/router/src/schema_state.rs
+++ b/bin/router/src/schema_state.rs
@@ -1,6 +1,8 @@
+use crate::pipeline::active_subscriptions::ActiveSubscriptions;
use crate::pipeline::authorization::metadata::AuthorizationMetadataExt;
use arc_swap::{ArcSwap, Guard};
use async_trait::async_trait;
+use dashmap::DashMap;
use graphql_tools::static_graphql::schema::Document;
use graphql_tools::validation::utils::ValidationError;
use hive_router_config::{supergraph::SupergraphSource, HiveRouterConfig};
@@ -9,6 +11,10 @@ use hive_router_internal::{
authorization::metadata::AuthorizationMetadata,
background_tasks::{BackgroundTask, BackgroundTasksManager},
};
+use hive_router_plan_executor::executors::http_callback::{
+ CallbackMessage, CallbackSubscriptionsMap,
+};
+use hive_router_plan_executor::response::graphql_error::GraphQLErrorExtensions;
use hive_router_plan_executor::{
executors::error::SubgraphExecutorError,
hooks::on_supergraph_load::{
@@ -16,6 +22,7 @@ use hive_router_plan_executor::{
},
introspection::schema::SchemaWithMetadata,
plugin_trait::{EndControlFlow, RouterPluginBoxed, StartControlFlow},
+ response::graphql_error::GraphQLError,
SubgraphExecutorMap,
};
use hive_router_query_planner::planner::plan_nodes::QueryPlan;
@@ -25,6 +32,7 @@ use hive_router_query_planner::{
};
use moka::future::Cache;
use std::sync::Arc;
+use std::time::Duration;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, trace};
@@ -44,6 +52,7 @@ pub struct SchemaState {
pub validate_cache: Cache>>,
pub normalize_cache: Cache>,
pub telemetry_context: Arc,
+ pub callback_subscriptions: CallbackSubscriptionsMap,
}
#[derive(Debug, thiserror::Error)]
@@ -79,6 +88,7 @@ impl SchemaState {
router_config: Arc,
plugins: Option>>,
cache_state: Arc,
+ active_subscriptions: ActiveSubscriptions,
) -> Result {
let (tx, mut rx) = mpsc::channel::(1);
let background_loader = SupergraphBackgroundLoader::new(
@@ -93,12 +103,27 @@ impl SchemaState {
let plan_cache = cache_state.plan_cache.clone();
let validate_cache = cache_state.validate_cache.clone();
let normalize_cache = cache_state.normalize_cache.clone();
+ let callback_subscriptions: CallbackSubscriptionsMap = Arc::new(DashMap::new());
// This is cheap clone, as Cache is thread-safe and can be cloned without any performance penalty.
let cache_state_for_invalidation = cache_state.clone();
+ let callback_subscriptions_for_build_data = callback_subscriptions.clone();
+
+ // kick off subscriptions/subgraphs that are idling/timed out due to missed heartbeats
+ if let Some(ref callback_config) = router_config.subscriptions.callback {
+ if !callback_config.heartbeat_interval.is_zero() {
+ let enforcer_subs = callback_subscriptions.clone();
+ let heartbeat_interval = callback_config.heartbeat_interval;
+ bg_tasks_manager.register_task(CallbackHeartbeatEnforcerTask {
+ callback_subscriptions: enforcer_subs,
+ heartbeat_interval,
+ });
+ }
+ }
let metrics = telemetry_context.metrics.clone();
let task_telemetry = telemetry_context.clone();
+ let active_subscriptions_for_reload = active_subscriptions.clone();
bg_tasks_manager.register_handle(async move {
let supergraph_metrics = &metrics.supergraph;
while let Some(new_sdl) = rx.recv().await {
@@ -141,7 +166,12 @@ impl SchemaState {
}
match new_supergraph_data.unwrap_or_else(|| {
- Self::build_data(router_config.clone(), task_telemetry.clone(), new_ast)
+ Self::build_data(
+ router_config.clone(),
+ task_telemetry.clone(),
+ new_ast,
+ callback_subscriptions_for_build_data.clone(),
+ )
}) {
Ok(mut new_supergraph_data) => {
if !on_end_callbacks.is_empty() {
@@ -180,6 +210,16 @@ impl SchemaState {
new_supergraph_data = end_payload.new_supergraph_data;
}
+ // close all active subscriptions before swapping supergraph data
+ active_subscriptions_for_reload.close_all_with_error(vec![
+ // this is litearaly the same message apollo sends - reasoning is
+ // drop in replacement - is that oke? should we have our own?
+ GraphQLError::from_message_and_code(
+ "subscription has been closed due to a schema reload",
+ "SUBSCRIPTION_SCHEMA_RELOAD",
+ ),
+ ]);
+
swappable_data_spawn_clone.store(Arc::new(Some(new_supergraph_data)));
debug!("Supergraph updated successfully");
@@ -201,6 +241,7 @@ impl SchemaState {
validate_cache,
normalize_cache,
telemetry_context: telemetry_context.clone(),
+ callback_subscriptions,
})
}
@@ -208,15 +249,17 @@ impl SchemaState {
router_config: Arc,
telemetry_context: Arc,
parsed_supergraph_sdl: Document,
+ callback_subscriptions: CallbackSubscriptionsMap,
) -> Result {
let planner = Planner::new_from_supergraph(&parsed_supergraph_sdl)?;
- let metadata = planner.consumer_schema.schema_metadata();
+ let metadata = Arc::new(planner.consumer_schema.schema_metadata());
let authorization = AuthorizationMetadata::build(&planner.supergraph, &metadata)?;
- let subgraph_executor_map = SubgraphExecutorMap::from_http_endpoint_map(
+ let subgraph_executor_map = Arc::new(SubgraphExecutorMap::from_http_endpoint_map(
&planner.supergraph.subgraph_endpoint_map,
router_config,
telemetry_context,
- )?;
+ callback_subscriptions,
+ )?);
Ok(SupergraphData {
supergraph_schema: Arc::new(parsed_supergraph_sdl),
@@ -305,3 +348,68 @@ impl BackgroundTask for SupergraphBackgroundLoaderTask {
}
}
}
+
+struct CallbackHeartbeatEnforcerTask {
+ callback_subscriptions: CallbackSubscriptionsMap,
+ heartbeat_interval: Duration,
+}
+
+#[async_trait]
+impl BackgroundTask for CallbackHeartbeatEnforcerTask {
+ fn id(&self) -> &str {
+ "http-callback-heartbeat-enforcer"
+ }
+
+ async fn run(&self, token: CancellationToken) {
+ use std::time::Instant;
+
+ loop {
+ tokio::select! {
+ _ = token.cancelled() => {
+ debug!("heartbeat enforcer cancelled, stopping");
+ return;
+ }
+ _ = ntex::time::sleep(self.heartbeat_interval) => {}
+ }
+
+ let mut timed_out = Vec::new();
+ for entry in self.callback_subscriptions.iter() {
+ let last = *entry.value().last_heartbeat.lock().unwrap();
+ // heartbeat interval and some grace period to account for potential network delays
+ #[cfg(not(feature = "testing"))]
+ let grace_period = std::time::Duration::from_millis(1000);
+ // when dealing with tests that run in parallel in the CI, we need to increase the
+ // grace period to avoid flaky tests due to timing issues with runner under pressure
+ #[cfg(feature = "testing")]
+ let grace_period = std::time::Duration::from_millis(2000);
+ let deadline = self.heartbeat_interval + grace_period;
+ let elapsed = match last {
+ // first check hasn't arrived yet, measure from creation time instead
+ None => Instant::now().duration_since(entry.value().created_at),
+ Some(last) => Instant::now().duration_since(last),
+ };
+ if elapsed > deadline {
+ timed_out.push(entry.key().clone());
+ }
+ }
+
+ // separate iter so that we dont mess up the slice while looping
+ for id in timed_out {
+ debug!(
+ subscription_id = %id,
+ "terminating subscription due to http callback subgraph missed heartbeat"
+ );
+ if let Some((_, sub)) = self.callback_subscriptions.remove(&id) {
+ // we dont care about the result of this send, if it fails it means the client
+ // is already gone or too slow, either way we just terminate the subscription
+ let _ = sub.sender.try_send(CallbackMessage::Complete {
+ errors: Some(vec![GraphQLError::from_message_and_extensions(
+ "Subgraph gone due to heartbeat timeout".to_string(),
+ GraphQLErrorExtensions::new_from_code("SUBGRAPH_GONE"),
+ )]),
+ });
+ }
+ }
+ }
+ }
+}
diff --git a/bin/router/src/shared_state.rs b/bin/router/src/shared_state.rs
index 22823db44..de96706d3 100644
--- a/bin/router/src/shared_state.rs
+++ b/bin/router/src/shared_state.rs
@@ -1,3 +1,4 @@
+use futures::Stream;
use graphql_tools::validation::validate::ValidationPlan;
use hive_console_sdk::agent::usage_agent::{AgentError, UsageAgent};
use hive_router_config::traffic_shaping::{
@@ -6,8 +7,9 @@ use hive_router_config::traffic_shaping::{
use hive_router_config::HiveRouterConfig;
use hive_router_internal::expressions::values::boolean::BooleanOrProgram;
use hive_router_internal::expressions::ExpressionCompileError;
-use hive_router_internal::inflight::InFlightMap;
+use hive_router_internal::inflight::{InFlightCleanupGuard, InFlightMap};
use hive_router_internal::telemetry::TelemetryContext;
+use hive_router_plan_executor::execution::plan::FailedExecutionResult;
use hive_router_plan_executor::headers::{
compile::compile_headers_plan, errors::HeaderRuleCompileError, plan::HeaderRulesPlan,
};
@@ -17,16 +19,25 @@ use moka::future::Cache;
use moka::Expiry;
use ntex::web;
use ntex::{http::HeaderMap, util::Bytes};
+use std::sync::atomic::AtomicUsize;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::{collections::HashSet, sync::Arc};
+use tracing::trace;
use crate::cache_state::CacheState;
use crate::jwt::context::JwtTokenPayload;
use crate::jwt::JwtAuthRuntime;
+use crate::pipeline::active_subscriptions::{ActiveSubscriptions, SubscriptionEvent};
use crate::pipeline::cors::{CORSConfigError, Cors};
+use crate::pipeline::error::PipelineError;
+use crate::pipeline::header::{ResponseMode, StreamContentType};
use crate::pipeline::introspection_policy::compile_introspection_policy;
+use crate::pipeline::multipart_subscribe::{
+ self, APOLLO_MULTIPART_HTTP_CONTENT_TYPE, INCREMENTAL_DELIVERY_CONTENT_TYPE,
+};
use crate::pipeline::parser::ParseCacheEntry;
use crate::pipeline::progressive_override::{OverrideLabelsCompileError, OverrideLabelsEvaluator};
+use crate::pipeline::sse;
pub type JwtClaimsCache = Cache>;
pub type RouterInflightRequestsMap = InFlightMap;
@@ -74,27 +85,148 @@ impl From<&TrafficShapingRouterDedupeHeadersConfig> for RouterRequestDedupeHeade
}
}
+pub type SharedRouterResponseGuard = InFlightCleanupGuard;
+
+#[derive(Clone)]
+pub enum SharedRouterResponse {
+ Single(SharedRouterSingleResponse),
+ Stream(SharedRouterStreamResponse),
+}
+
+impl SharedRouterResponse {
+ pub fn error_count(&self) -> usize {
+ match self {
+ SharedRouterResponse::Single(resp) => resp.error_count,
+ SharedRouterResponse::Stream(resp) => resp.error_count,
+ }
+ }
+ pub fn into_response(
+ self,
+ response_mode: &ResponseMode,
+ ) -> Result {
+ match self {
+ SharedRouterResponse::Single(single) => Ok(single.into()),
+ SharedRouterResponse::Stream(stream) => {
+ let stream_content_type = response_mode
+ .stream_content_type()
+ .ok_or(PipelineError::SubscriptionsTransportNotSupported)?;
+ Ok(stream.into_response(stream_content_type))
+ }
+ }
+ }
+}
+
#[derive(Clone)]
-pub struct SharedRouterResponse {
+pub struct SharedRouterSingleResponse {
pub body: Bytes,
pub headers: Arc,
pub status: StatusCode,
pub error_count: usize,
}
-impl From for web::HttpResponse {
- fn from(shared_response: SharedRouterResponse) -> Self {
+impl From for web::HttpResponse {
+ fn from(shared_response: SharedRouterSingleResponse) -> Self {
let mut response = web::HttpResponse::Ok();
response.status(shared_response.status);
-
for (header_name, header_value) in shared_response.headers.iter() {
response.set_header(header_name, header_value);
}
-
response.body(shared_response.body)
}
}
+// status is always 200 for streaming responses, errors are sent through the stream.
+// stream content type is not included because we can deduplicate subscriptions across
+// different content types, the response format is decided when converting to response
+pub struct SharedRouterStreamResponse {
+ pub body: tokio::sync::broadcast::Sender,
+ pub headers: Arc,
+ pub error_count: usize,
+ // the leader gets the receiver that was subscribed before the pump was spawned, so
+ // there is no window where the channel has zero receivers and events can be lost.
+ // joiners get None and subscribe via body.subscribe() when consumed.
+ pub receiver: Option>,
+}
+
+impl Clone for SharedRouterStreamResponse {
+ fn clone(&self) -> Self {
+ Self {
+ body: self.body.clone(),
+ headers: self.headers.clone(),
+ error_count: self.error_count,
+ receiver: None,
+ }
+ }
+}
+
+impl SharedRouterStreamResponse {
+ pub fn into_response(self, stream_content_type: &StreamContentType) -> web::HttpResponse {
+ // leader already has a pre-subscribed receiver to avoid missing
+ // any potential events emitted. joiners, on the other hand, subscribe
+ let mut receiver = self.receiver.unwrap_or_else(|| self.body.subscribe());
+
+ let stream = Box::pin(async_stream::stream! {
+ loop {
+ match receiver.recv().await {
+ Ok(SubscriptionEvent::Raw(data)) => {
+ yield data.to_vec();
+ }
+ Ok(SubscriptionEvent::Error(errors)) => {
+ yield FailedExecutionResult { errors }.serialize();
+ break;
+ }
+ Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
+ trace!(lagged = n, "broadcast receiver lagged, skipping missed messages");
+ continue;
+ }
+ Err(tokio::sync::broadcast::error::RecvError::Closed) => {
+ break;
+ }
+ }
+ }
+ });
+
+ let content_type_header = match stream_content_type {
+ StreamContentType::IncrementalDelivery => {
+ http::HeaderValue::from_static(INCREMENTAL_DELIVERY_CONTENT_TYPE)
+ }
+ StreamContentType::SSE => http::HeaderValue::from_static("text/event-stream"),
+ StreamContentType::ApolloMultipartHTTP => {
+ http::HeaderValue::from_static(APOLLO_MULTIPART_HTTP_CONTENT_TYPE)
+ }
+ };
+
+ let body: std::pin::Pin<
+ Box> + Send>,
+ > = match stream_content_type {
+ StreamContentType::IncrementalDelivery => Box::pin(
+ multipart_subscribe::create_incremental_delivery_stream(stream),
+ ),
+ StreamContentType::SSE => Box::pin(sse::create_stream(
+ stream,
+ std::time::Duration::from_secs(10),
+ )),
+ StreamContentType::ApolloMultipartHTTP => {
+ Box::pin(multipart_subscribe::create_apollo_multipart_http_stream(
+ stream,
+ std::time::Duration::from_secs(10),
+ ))
+ }
+ };
+
+ let mut response = web::HttpResponse::Ok();
+
+ for (header_name, header_value) in self.headers.iter() {
+ response.set_header(header_name, header_value);
+ }
+
+ // we set content type after so that we can override the shared header
+ response.content_type(content_type_header);
+
+ response.streaming(body)
+ }
+}
+
/// Default TTL for JWT claims cache entries (5 seconds)
const DEFAULT_JWT_CACHE_TTL_SECS: u64 = 5;
@@ -138,7 +270,7 @@ pub struct RouterSharedState {
pub validation_plan: Arc,
pub parse_cache: Cache,
pub router_config: Arc,
- pub headers_plan: HeaderRulesPlan,
+ pub headers_plan: Arc,
pub override_labels_evaluator: OverrideLabelsEvaluator,
pub cors_runtime: Option,
/// Cache for validated JWT claims to avoid re-parsing on every request.
@@ -153,9 +285,14 @@ pub struct RouterSharedState {
pub plugins: Option>>,
pub in_flight_requests: RouterInflightRequestsMap,
pub in_flight_requests_header_policy: RouterRequestDedupeHeaderPolicy,
+ /// Tracks the number of active long-lived clients (websockets + http streams)
+ pub long_lived_client_count: Arc,
+ /// Tracks all active subscriptions from clients to the router.
+ pub active_subscriptions: ActiveSubscriptions,
}
impl RouterSharedState {
+ #[allow(clippy::too_many_arguments)]
pub fn new(
router_config: Arc,
jwt_auth_runtime: Option,
@@ -164,11 +301,12 @@ impl RouterSharedState {
telemetry_context: Arc,
plugins: Option>>,
cache_state: Arc,
+ active_subscriptions: ActiveSubscriptions,
) -> Result {
let parse_cache = cache_state.parse_cache.clone();
Ok(Self {
validation_plan: Arc::new(validation_plan),
- headers_plan: compile_headers_plan(&router_config.headers).map_err(Box::new)?,
+ headers_plan: Arc::new(compile_headers_plan(&router_config.headers).map_err(Box::new)?),
parse_cache,
cors_runtime: Cors::from_config(&router_config.cors).map_err(Box::new)?,
jwt_claims_cache: Cache::builder()
@@ -195,6 +333,8 @@ impl RouterSharedState {
.dedupe
.headers)
.into(),
+ long_lived_client_count: Arc::new(AtomicUsize::new(0)),
+ active_subscriptions,
})
}
}
diff --git a/bin/router/src/telemetry.rs b/bin/router/src/telemetry.rs
index c26aa1f84..c3181216d 100644
--- a/bin/router/src/telemetry.rs
+++ b/bin/router/src/telemetry.rs
@@ -284,7 +284,7 @@ fn create_prometheus_runtime(
};
let registry = prometheus_config.registry.clone();
- let router_port = config.http.port();
+ let router_port = config.port();
let port = prometheus_config.port.unwrap_or(router_port);
let same_listener = router_port == port;
@@ -301,7 +301,7 @@ fn create_prometheus_runtime(
let registry_for_result = registry.clone();
let path_for_result = path.clone();
- let listen_address = (config.http.host(), port);
+ let listen_address = (config.host(), port);
let server = HttpServer::new(move || {
let registry = registry.clone();
let path = path.clone();
diff --git a/docs/README.md b/docs/README.md
index f8c4c205f..e4bf7fa64 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -18,9 +18,11 @@
|[**override\_subgraph\_urls**](#override_subgraph_urls)|`object`|Configuration for overriding subgraph URLs. Default: `{}` ||
|[**plugins**](#plugins)|`object`|Configuration for custom plugins ||
|[**query\_planner**](#query_planner)|`object`|Query planning configuration. Default: `{"allow_expose":false,"timeout":"10s"}` ||
+|[**subscriptions**](#subscriptions)|`object`|Configuration for subscriptions. Default: `{"broadcast_capacity":0,"enabled":false}` ||
|[**supergraph**](#supergraph)|`object`|Configuration for the Federation supergraph source. By default, the router will use a local file-based supergraph source (`./supergraph.graphql`). ||
|[**telemetry**](#telemetry)|`object`|Default: `{"client_identification":{"name_header":"graphql-client-name","version_header":"graphql-client-version"},"hive":null,"metrics":{"exporters":[],"instrumentation":{"common":{"histogram":{"aggregation":"explicit","bytes":{"buckets":[128,512,1024,2048,4096,8192,16384,32768,65536,131072,262144,524288,1048576,2097152,3145728,4194304,5242880],"record_min_max":false},"seconds":{"buckets":[0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1,2.5,5,7.5,10],"record_min_max":false}}},"instruments":{}}},"resource":{"attributes":{}},"tracing":{"collect":{"max_attributes_per_event":16,"max_attributes_per_link":32,"max_attributes_per_span":128,"max_events_per_span":128,"parent_based_sampler":false,"sampling":1},"exporters":[],"instrumentation":{"spans":{"mode":"spec_compliant"}},"propagation":{"b3":false,"baggage":false,"jaeger":false,"trace_context":true}}}` ||
-|[**traffic\_shaping**](#traffic_shaping)|`object`|Configuration for the traffic-shaping of the executor. Use these configurations to control how requests are being executed to subgraphs. Default: `{"all":{"dedupe_enabled":true,"pool_idle_timeout":"50s","request_timeout":"30s"},"max_connections_per_host":100,"router":{"dedupe":{"enabled":false,"headers":"all"},"request_timeout":"1m"}}` ||
+|[**traffic\_shaping**](#traffic_shaping)|`object`|Configuration for the traffic-shaping of the executor. Use these configurations to control how requests are being executed to subgraphs. Default: `{"all":{"dedupe_enabled":true,"pool_idle_timeout":"50s","request_timeout":"30s"},"max_connections_per_host":100,"router":{"dedupe":{"enabled":false,"headers":"all"},"max_long_lived_clients":128,"request_timeout":"1m"}}` ||
+|[**websocket**](#websocket)|`object`|Configuration of router's WebSocket server. Default: `{"enabled":false,"headers":{"persist":false,"source":"connection"},"path":null}` ||
**Additional Properties:** not allowed
**Example**
@@ -119,6 +121,9 @@ plugins: {}
query_planner:
allow_expose: false
timeout: 10s
+subscriptions:
+ broadcast_capacity: 0
+ enabled: false
supergraph: {}
telemetry:
client_identification:
@@ -198,7 +203,14 @@ traffic_shaping:
dedupe:
enabled: false
headers: all
+ max_long_lived_clients: 128
request_timeout: 1m
+websocket:
+ enabled: false
+ headers:
+ persist: false
+ source: connection
+ path: null
```
@@ -1929,6 +1941,119 @@ timeout: 10s
```
+
+## subscriptions: object
+
+Configuration for subscriptions.
+
+
+**Properties**
+
+|Name|Type|Description|Required|
+|----|----|-----------|--------|
+|**broadcast\_capacity**|`integer`|The capacity of the broadcast channel used to fan out subscription events to all active listeners. Each active subscription has its own broadcast channel. This value controls how many events can be buffered in that channel before slow consumers start lagging. If a consumer falls too far behind and the buffer is full, it will skip the missed messages and continue from the latest available event. Subscription events are typically low-frequency, so the default of 32 is sufficient for most use cases. Increase this value if you expect bursts of events or have slow consumers that need more headroom to catch up. Defaults to 32. Default: `32` Format: `"uint"` Minimum: `0` ||
+|[**callback**](#subscriptionscallback)|`object`, `null`|Configuration for subgraphs using the HTTP Callback protocol. |yes|
+|**enabled**|`boolean`|Enables/disables subscriptions. By default, the subscriptions are disabled. You can override this setting by setting the `SUBSCRIPTIONS_ENABLED` environment variable to `true` or `false`. Default: `false` ||
+|[**websocket**](#subscriptionswebsocket)|`object`, `null`|Configuration for subgraphs using WebSocket protocol. ||
+
+**Additional Properties:** not allowed
+**Example**
+
+```yaml
+broadcast_capacity: 0
+enabled: false
+
+```
+
+
+### subscriptions\.callback: object,null
+
+Configuration for subgraphs using the HTTP Callback protocol.
+
+
+**Properties**
+
+|Name|Type|Description|Required|
+|----|----|-----------|--------|
+|**heartbeat\_interval**|`string`|The interval at which the subgraph must send heartbeat messages. If set to 0, heartbeats are disabled. Defaults to 5 seconds. Default: `"5s"` |no|
+|**listen**|`string`, `null`|The IP address and port the router will listen on for subscription callbacks. When set, the router will start a dedicated HTTP server bound to this address for receiving callback messages from subgraphs, separate from the main GraphQL server. When not set, the callback handler is registered on the main server. Example: `0.0.0.0:4001` |no|
+|**path**|`string`|The path of the router's callback endpoint. Must be an absolute path starting with `/`. Defaults to `/callback`. Default: `"/callback"` Pattern: `^/` |no|
+|**public\_url**|`string`|The public URL that subgraphs will use to send callback messages to this router. Your public_url must match the server address combined with the router's path. Meaning, if your server is `http://localhost:4000` and the path is `/callback`, your `public_url` should be `http://localhost:4000/callback`. Example: `https://example.com:4000/callback` Format: `"uri"` |yes|
+|[**subgraphs**](#subscriptionscallbacksubgraphs)|`string[]`|The list of subgraph names that use the HTTP callback protocol. Default: |no|
+
+**Additional Properties:** not allowed
+
+#### subscriptions\.callback\.subgraphs\[\]: array
+
+The list of subgraph names that use the HTTP callback protocol.
+
+
+**Items**
+
+**Item Type:** `string`
+**Unique Items:** yes
+
+### subscriptions\.websocket: object,null
+
+Configuration for subgraphs using WebSocket protocol.
+
+
+**Properties**
+
+|Name|Type|Description|Required|
+|----|----|-----------|--------|
+|[**all**](#subscriptionswebsocketall)|`object`, `null`|The default configuration that will be applied to all subgraphs using ||
+|[**subgraphs**](#subscriptionswebsocketsubgraphs)|`object`|Optional per-subgraph configurations that will override the default configuration for specific subgraphs. ||
+
+**Additional Properties:** not allowed
+
+#### subscriptions\.websocket\.all: object,null
+
+The default configuration that will be applied to all subgraphs using
+WebSocket protocol, unless overridden by a specific subgraph configuration.
+
+When specified, all subgraphs (not claimed by `callback`) will use the WebSocket protocol.
+
+
+**Properties**
+
+|Name|Type|Description|Required|
+|----|----|-----------|--------|
+|**path**|`string`, `null`|Determines the URL path to use for the subscription endpoint: - For WebSocket connections, the URL will be `ws://`. - If `path` is not set, the default subgraph URL is used, with the scheme adjusted to `ws` for WebSocket connections where applicable. Note to always provide the absolute path starting with a `/`, e.g., `/ws`. For example, if the subgraph URL is `http://example.com/graphql` and the path is set to `/ws`, the resulting WebSocket URL will be `ws://example.com/ws`. Pattern: `^/` ||
+
+**Additional Properties:** not allowed
+
+#### subscriptions\.websocket\.subgraphs: object
+
+Optional per-subgraph configurations that will override the default configuration for specific subgraphs.
+
+
+**Additional Properties**
+
+|Name|Type|Description|Required|
+|----|----|-----------|--------|
+|[**Additional Properties**](#subscriptionswebsocketsubgraphsadditionalproperties)|`object`|WebSocket configuration for a specific subgraph or the default for all subgraphs. ||
+
+
+##### subscriptions\.websocket\.subgraphs\.additionalProperties: object
+
+WebSocket configuration for a specific subgraph or the default for all subgraphs.
+
+
+**Properties**
+
+|Name|Type|Description|Required|
+|----|----|-----------|--------|
+|**path**|`string`, `null`|Determines the URL path to use for the subscription endpoint: - For WebSocket connections, the URL will be `ws://`. - If `path` is not set, the default subgraph URL is used, with the scheme adjusted to `ws` for WebSocket connections where applicable. Note to always provide the absolute path starting with a `/`, e.g., `/ws`. For example, if the subgraph URL is `http://example.com/graphql` and the path is set to `/ws`, the resulting WebSocket URL will be `ws://example.com/ws`. Pattern: `^/` ||
+
+**Additional Properties:** not allowed
+**Example**
+
+```yaml
+path: null
+
+```
+
## supergraph: object
@@ -2917,7 +3042,7 @@ Configuration for the traffic-shaping of the executor. Use these configurations
|----|----|-----------|--------|
|[**all**](#traffic_shapingall)|`object`|The default configuration that will be applied to all subgraphs, unless overridden by a specific subgraph configuration. Default: `{"dedupe_enabled":true,"pool_idle_timeout":"50s","request_timeout":"30s"}` ||
|**max\_connections\_per\_host**|`integer`|Limits the concurrent amount of requests/connections per host/subgraph. Default: `100` Format: `"uint"` Minimum: `0` ||
-|[**router**](#traffic_shapingrouter)|`object`|Configuration for the router itself, e.g., for handling incoming requests, or other router-level traffic shaping configurations. Default: `{"dedupe":{"enabled":false,"headers":"all"},"request_timeout":"1m"}` ||
+|[**router**](#traffic_shapingrouter)|`object`|Configuration for the router itself, e.g., for handling incoming requests, or other router-level traffic shaping configurations. Default: `{"dedupe":{"enabled":false,"headers":"all"},"max_long_lived_clients":128,"request_timeout":"1m"}` ||
|[**subgraphs**](#traffic_shapingsubgraphs)|`object`|Optional per-subgraph configurations that will override the default configuration for specific subgraphs. ||
**Additional Properties:** not allowed
@@ -2933,6 +3058,7 @@ router:
dedupe:
enabled: false
headers: all
+ max_long_lived_clients: 128
request_timeout: 1m
```
@@ -2972,6 +3098,7 @@ Configuration for the router itself, e.g., for handling incoming requests, or ot
|Name|Type|Description|Required|
|----|----|-----------|--------|
|[**dedupe**](#traffic_shapingrouterdedupe)|`object`|Default: `{"enabled":false,"headers":"all"}` ||
+|**max\_long\_lived\_clients**|`integer`|Maximum number of concurrent long-lived clients (WebSocket connections and HTTP streaming responses). Regular non-streaming requests are not counted toward this limit. When the limit is reached, new WebSocket and streaming HTTP requests are rejected with 503. If both WebSockets and Subscriptions are disabled, this setting has no effect. Default: `128` Format: `"uint"` Minimum: `0` ||
|**request\_timeout**|`string`|Optional timeout configuration for incoming requests to the router. It starts from the moment the request is received by the router, and includes the entire processing of the request (validation, execution, etc.) until a response is sent back to the client. If a request takes longer than the specified duration, it will be aborted and a timeout error will be returned to the client. Default: `"1m"` ||
**Additional Properties:** not allowed
@@ -2981,6 +3108,7 @@ Configuration for the router itself, e.g., for handling incoming requests, or ot
dedupe:
enabled: false
headers: all
+max_long_lived_clients: 128
request_timeout: 1m
```
@@ -2992,7 +3120,7 @@ request_timeout: 1m
|Name|Type|Description|Required|
|----|----|-----------|--------|
-|**enabled**|`boolean`|Enables/disables in-flight request deduplication at the router endpoint level. When enabled, identical incoming GraphQL query requests that are processed at the same time share the same in-flight execution result. Default: `false` ||
+|**enabled**|`boolean`|Enables/disables in-flight request and active subscriptions deduplication at the router level. When enabled, the router deduplicates both queries and subscriptions using the same fingerprint key (method, path, selected headers, schema checksum, normalized operation hash, variables, and extensions). The `headers` configuration below controls which headers participate in that key for all operation types. For queries, concurrent HTTP requests that produce the same fingerprint share a single in-flight execution - only the first one runs, and the rest wait for and receive the same result. For subscriptions, the mechanism is broadcast-based rather than request-sharing. The first client with a given fingerprint becomes the leader: it runs the upstream subscription and its events are fanned out through a broadcast channel backed by an active subscriptions registry. Any subsequent client that arrives with an identical fingerprint while that subscription is still active joins as a listener on the same broadcast channel instead of starting a new upstream connection. When all listeners have dropped and the leader finishes, the entry is removed from the registry. WebSocket connections participate in the same deduplication space as HTTP. Each subscribe message is processed with a synthetic request assembled from the WebSocket path and the headers derived from the `websocket.headers` config. The fingerprint is computed from those synthetic headers using the same header policy, so a subscription started over HTTP and an identical one started over WebSocket will deduplicate against each other. The deduplication is transport agnostic. A query over WebSocket would get deduplicated with an identical query over HTTP if they arrive at the same time and have the same fingerprint. Note: `content-type` is part of the fingerprint when `headers` includes it (e.g. `all`). Since HTTP streaming clients send different `accept` headers than WebSocket clients, cross-transport deduplication for subscriptions only applies when `content-type` (and transport-specific headers) are excluded from the key. Configure `headers: none` or `headers: { include: [] }` (or exclude the relevant headers) to enable true cross-transport deduplication, where a WebSocket subscription and an SSE subscription with the same operation share a single upstream connection and the events are fanned out to both. Default: `false` ||
|**headers**||Header configuration participating in the dedupe key. Accepted forms: - `all` - `none` - `{ include: ["authorization", "cookie"] }` Header names are case-insensitive and validated as standard HTTP header names. Default: `"all"` ||
**Additional Properties:** not allowed
@@ -3028,4 +3156,52 @@ Optional per-subgraph configurations that will override the default configuratio
|**request\_timeout**||Optional timeout configuration for requests to subgraphs. Example with a fixed duration: ```yaml timeout: duration: 5s ``` Or with a VRL expression that can return a duration based on the operation kind: ```yaml timeout: expression: \| if (.request.operation.type == "mutation") { "10s" } else { "15s" } ``` ||
**Additional Properties:** not allowed
+
+## websocket: object
+
+Configuration of router's WebSocket server.
+
+
+**Properties**
+
+|Name|Type|Description|Required|
+|----|----|-----------|--------|
+|**enabled**|`boolean`|Enables/disables WebSocket connections. By default, WebSockets are disabled. You can override this setting by setting the `WEBSOCKET_ENABLED` environment variable to `true` or `false`. Default: `false` ||
+|[**headers**](#websocketheaders)|`object`|Configuration for handling headers for WebSocket connections. Default: `{"persist":false,"source":"connection"}` |yes|
+|**path**|`string`, `null`|The path to use for the WebSocket endpoint on the router. Note to always provide the absolute path starting with a `/`, e.g., `/ws`. By default, the WebSocket endpoint will be available at the `http.graphql_endpoint` (defaults to `/graphql`) if no path is specified and the clients will connect using `ws:///`. Pattern: `^/` ||
+
+**Additional Properties:** not allowed
+**Example**
+
+```yaml
+enabled: false
+headers:
+ persist: false
+ source: connection
+path: null
+
+```
+
+
+### websocket\.headers: object
+
+Configuration for handling headers for WebSocket connections.
+
+
+**Properties**
+
+|Name|Type|Description|Required|
+|----|----|-----------|--------|
+|**persist**|`boolean`|Whether to persist merged headers for the duration of the WebSocket connection when using the `both` source (headers are accepted from multiple sources). Only has effect when `source` is set to `both`. This is useful when dealing with authentication using tokens that expire, where the initial connection might use one token, but subsequent operations might need to provide updated tokens in the operation extensions and then use that for further authentication. For example: 1. Client connects with connection init payload containing an Authorization header with a token. 2. Client sends a subscription operation with an updated Authorization header in the operation extensions. 3. If `persist` is enabled, the updated Authorization header will be stored and used for subsequent operations. Default: `false` |no|
+|**source**||The source(s) from which to accept headers for WebSocket connections. |yes|
+
+**Additional Properties:** not allowed
+**Example**
+
+```yaml
+persist: false
+source: connection
+
+```
+
diff --git a/e2e/configs/header_propagation.router.yaml b/e2e/configs/header_propagation.router.yaml
index 662e49210..fef9d7238 100644
--- a/e2e/configs/header_propagation.router.yaml
+++ b/e2e/configs/header_propagation.router.yaml
@@ -6,4 +6,6 @@ headers:
all:
request:
- propagate:
- named: x-context
\ No newline at end of file
+ named: x-context
+subscriptions:
+ enabled: true
diff --git a/e2e/src/file_supergraph.rs b/e2e/src/file_supergraph.rs
index dc3daf0e3..c0322d209 100644
--- a/e2e/src/file_supergraph.rs
+++ b/e2e/src/file_supergraph.rs
@@ -46,7 +46,7 @@ mod file_supergraph_e2e_tests {
.unwrap()
.as_array()
.unwrap();
- assert_eq!(types_arr.len(), 21);
+ assert_eq!(types_arr.len(), 22);
}
#[ntex::test]
diff --git a/e2e/src/hive_cdn_supergraph.rs b/e2e/src/hive_cdn_supergraph.rs
index 7f8b67f5a..5091b58ad 100644
--- a/e2e/src/hive_cdn_supergraph.rs
+++ b/e2e/src/hive_cdn_supergraph.rs
@@ -255,7 +255,7 @@ mod hive_cdn_supergraph_e2e_tests {
.unwrap()
.as_array()
.unwrap();
- assert_eq!(types_arr.len(), 21);
+ assert_eq!(types_arr.len(), 22);
}
#[ntex::test]
@@ -277,7 +277,7 @@ mod hive_cdn_supergraph_e2e_tests {
.with_body("type Query { dummy: String }")
.create();
- let router = TestRouter::builder()
+ let _router = TestRouter::builder()
.inline_config(&format!(
r#"
supergraph:
@@ -289,13 +289,10 @@ mod hive_cdn_supergraph_e2e_tests {
max_retries: 10
"#,
))
- .skip_wait_for_ready_on_start() // this one will time out after 3 seconds, we need more
.build()
.start()
.await;
- router.wait_for_ready(Some(Duration::from_secs(7))).await;
-
one.assert();
two.assert();
}
diff --git a/e2e/src/http.rs b/e2e/src/http.rs
index 488591c6f..86dc9bc7b 100644
--- a/e2e/src/http.rs
+++ b/e2e/src/http.rs
@@ -426,7 +426,7 @@ mod http_tests {
source: hive
endpoint: http://{host}/supergraph
key: dummy_key
- poll_interval: 100ms
+ poll_interval: 500ms
traffic_shaping:
all:
dedupe_enabled: false
@@ -457,10 +457,10 @@ mod http_tests {
.is_empty()
{
assert!(
- started.elapsed() < Duration::from_secs(3),
+ started.elapsed() < Duration::from_secs(5),
"first request did not reach products subgraph in time"
);
- time::sleep(Duration::from_millis(25)).await;
+ time::sleep(Duration::from_millis(100)).await;
}
mock_initial.remove();
diff --git a/e2e/src/http_callback.rs b/e2e/src/http_callback.rs
new file mode 100644
index 000000000..f1621d2c0
--- /dev/null
+++ b/e2e/src/http_callback.rs
@@ -0,0 +1,338 @@
+#[cfg(test)]
+mod http_callback_e2e_tests {
+ use futures::StreamExt;
+ use ntex::http;
+ use sonic_rs::{json, JsonValueTrait};
+
+ use crate::testkit::{some_header_map, ClientResponseExt, TestRouter, TestSubgraphs};
+
+ #[ntex::test]
+ async fn listen_on_different_port() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+
+ // on slow systems when running tests concurrently, the available
+ // port might become unavailable by the time the router starts and binds
+ // the callback handler to it, causing the test to fail. in order to avoid
+ // we use a fixed high port that is unlikely to be used by other processes
+ // and cause conflicts, or get allocated (OS starts with 50000)
+ let callback_port = 61000;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ // .with_port() router is on a different port than the callback listener anyways
+ .inline_config(format!(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ callback:
+ listen: 0.0.0.0:{callback_port}
+ public_url: http://0.0.0.0:{callback_port}/callback
+ subgraphs:
+ - reviews
+ "#
+ ))
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ id
+ product {
+ name
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map!(
+ http::header::ACCEPT => "text/event-stream",
+ ),
+ )
+ .await;
+
+ assert_eq!(res.status(), 200, "Expected 200 OK");
+
+ let body = res.string_body().await;
+
+ assert!(
+ body.contains(
+ r#"data: {"data":{"reviewAdded":{"id":"1","product":{"name":"Table"}}}}"#
+ ),
+ "Expected at least one emitted event, got: {}",
+ body
+ );
+ assert!(body.contains("event: complete"));
+ }
+
+ #[ntex::test]
+ async fn complete_active_subscription_on_heartbeat_timeout() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+
+ let router_listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let router_port = router_listener.local_addr().unwrap().port();
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .with_listener(router_listener)
+ .inline_config(format!(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ headers:
+ all:
+ request:
+ - propagate:
+ named: x-disable-http-callback-heartbeats
+ subscriptions:
+ enabled: true
+ callback:
+ heartbeat_interval: 500ms
+ public_url: http://0.0.0.0:{router_port}/callback
+ subgraphs:
+ - reviews
+ "#
+ ))
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(
+ # emitted messages do not count as heartbeats
+ intervalInMs: 300
+ ) {
+ id
+ product {
+ name
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map!(
+ http::header::ACCEPT => "text/event-stream",
+ http::header::HeaderName::from_static("x-disable-http-callback-heartbeats") => "true"
+ ),
+ )
+ .await;
+
+ assert_eq!(res.status(), 200, "Expected 200 OK");
+
+ let body = res.string_body().await;
+
+ // emitted at least one event
+ assert!(
+ body.contains(
+ r#"data: {"data":{"reviewAdded":{"id":"1","product":{"name":"Table"}}}}"#
+ ),
+ "Expected at least one emitted event, got: {}",
+ body
+ );
+
+ // kicked off client, eventually
+ assert!(body.contains(r#"{"data":null,"errors":[{"message":"Subgraph gone due to heartbeat timeout","extensions":{"code":"SUBGRAPH_GONE"}}]}"#));
+
+ // completed stream
+ assert!(body.contains("event: complete"));
+ }
+
+ #[ntex::test]
+ async fn client_disconnect_removes_subscription() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+
+ let router_listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let router_port = router_listener.local_addr().unwrap().port();
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .with_listener(router_listener)
+ .inline_config(format!(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ callback:
+ public_url: http://0.0.0.0:{router_port}/callback
+ subgraphs:
+ - reviews
+ "#
+ ))
+ .build()
+ .start()
+ .await;
+
+ // Use a longer interval so we have time to cancel before the stream completes
+ let mut res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 100) {
+ id
+ }
+ }
+ "#,
+ None,
+ some_header_map!(
+ http::header::ACCEPT => "text/event-stream"
+ ),
+ )
+ .await;
+
+ assert_eq!(res.status(), 200, "Expected 200 OK");
+
+ // Read first chunk to ensure the subscription is active
+ let chunk_bytes = res.next().await.unwrap().unwrap();
+ let chunk_str = std::str::from_utf8(&chunk_bytes).unwrap();
+ assert!(
+ chunk_str.contains(r#"{"data":{"reviewAdded":{"id":"1"}}}"#),
+ "Expected first emission, got: {}",
+ chunk_str
+ );
+
+ // Extract subscriptionId and verifier from the request the router sent to the subgraph
+ let subgraph_requests = subgraphs
+ .get_requests_log("reviews")
+ .expect("expected requests sent to reviews subgraph");
+ let body_bytes = subgraph_requests[0]
+ .body
+ .as_ref()
+ .expect("expected request body");
+ let body_json: sonic_rs::Value =
+ sonic_rs::from_slice(body_bytes).expect("expected valid JSON body");
+ let subscription_id = body_json["extensions"]["subscription"]["subscriptionId"]
+ .as_str()
+ .expect("expected subscriptionId in request extensions")
+ .to_string();
+ let verifier = body_json["extensions"]["subscription"]["verifier"]
+ .as_str()
+ .expect("expected verifier in request extensions")
+ .to_string();
+
+ // Disconnect the client — this should propagate to the router and remove the subscription
+ drop(res);
+
+ // Give the router a moment to process the disconnect and clean up
+ tokio::time::sleep(std::time::Duration::from_millis(100)).await;
+
+ // A check for the now-removed subscription should return 404
+ let check_res = router
+ .serv()
+ .post(format!("/callback/{subscription_id}"))
+ .set_header("subscription-protocol", "callback/1.0")
+ .send_json(&json!({
+ "kind": "subscription",
+ "action": "check",
+ "id": subscription_id,
+ "verifier": verifier,
+ }))
+ .await
+ .expect("failed to send callback check request");
+
+ assert_eq!(
+ check_res.status(),
+ 404,
+ "Expected 404 after client disconnect removed the subscription"
+ );
+ }
+
+ #[ntex::test]
+ async fn invalid_verifier_is_rejected() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+
+ let router_listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let router_port = router_listener.local_addr().unwrap().port();
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .with_listener(router_listener)
+ .inline_config(format!(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ callback:
+ public_url: http://0.0.0.0:{router_port}/callback
+ subgraphs:
+ - reviews
+ "#
+ ))
+ .build()
+ .start()
+ .await;
+
+ let mut res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 100) {
+ id
+ }
+ }
+ "#,
+ None,
+ some_header_map!(
+ http::header::ACCEPT => "text/event-stream"
+ ),
+ )
+ .await;
+
+ assert_eq!(res.status(), 200, "Expected 200 OK");
+
+ let chunk_bytes = res.next().await.unwrap().unwrap();
+ let chunk_str = std::str::from_utf8(&chunk_bytes).unwrap();
+ assert!(
+ chunk_str.contains(r#"{"data":{"reviewAdded":{"id":"1"}}}"#),
+ "Expected first emission, got: {}",
+ chunk_str
+ );
+
+ let subgraph_requests = subgraphs
+ .get_requests_log("reviews")
+ .expect("expected requests sent to reviews subgraph");
+ let body_bytes = subgraph_requests[0]
+ .body
+ .as_ref()
+ .expect("expected request body");
+ let body_json: sonic_rs::Value =
+ sonic_rs::from_slice(body_bytes).expect("expected valid JSON body");
+ let subscription_id = body_json["extensions"]["subscription"]["subscriptionId"]
+ .as_str()
+ .expect("expected subscriptionId in request extensions")
+ .to_string();
+
+ // Send a check with a verifier that doesn't match the subscription's verifier
+ let wrong_verifier = "this-is-not-the-correct-verifier";
+ let check_res = router
+ .serv()
+ .post(format!("/callback/{subscription_id}"))
+ .set_header("subscription-protocol", "callback/1.0")
+ .send_json(&json!({
+ "kind": "subscription",
+ "action": "check",
+ "id": subscription_id,
+ "verifier": wrong_verifier,
+ }))
+ .await
+ .expect("failed to send callback check request");
+
+ assert_eq!(
+ check_res.status(),
+ 400,
+ "Expected 400 when using an invalid verifier"
+ );
+
+ // keep alive until the end of the test so the subscription stays active
+ drop(res);
+ }
+}
diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs
index a8cc50597..f42fcfd76 100644
--- a/e2e/src/lib.rs
+++ b/e2e/src/lib.rs
@@ -21,6 +21,8 @@ mod hive_cdn_supergraph;
#[cfg(test)]
mod http;
#[cfg(test)]
+mod http_callback;
+#[cfg(test)]
mod introspection;
#[cfg(test)]
mod issues;
@@ -41,11 +43,15 @@ mod probes;
#[cfg(test)]
mod router_timeout;
#[cfg(test)]
+mod subscriptions;
+#[cfg(test)]
mod supergraph;
#[cfg(test)]
mod telemetry;
#[cfg(test)]
mod timeout_per_subgraph;
+#[cfg(test)]
+mod websocket;
pub use insta;
pub use mockito;
diff --git a/e2e/src/subscriptions.rs b/e2e/src/subscriptions.rs
new file mode 100644
index 000000000..f1268b401
--- /dev/null
+++ b/e2e/src/subscriptions.rs
@@ -0,0 +1,1829 @@
+#[cfg(test)]
+mod subscriptions_e2e_tests {
+
+ use insta::assert_snapshot;
+ use ntex::http;
+ use reqwest::StatusCode;
+ use sonic_rs::json;
+
+ use crate::testkit::{
+ some_header_map, ClientResponseExt, ResponseLike, TestRouter, TestSubgraphs,
+ };
+
+ #[ntex::test]
+ async fn subscription_not_allowed_when_disabled() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ # disabled by default
+ # subscriptions:
+ # enabled: false
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ product {
+ upc
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ },
+ )
+ .await;
+
+ // even though subscriptions are disabled, we accept the stream
+ assert_eq!(res.status(), 200, "Expected 200 OK");
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert_snapshot!(body_str, @r#"
+ event: next
+ data: {"errors":[{"message":"Subscriptions are not supported","extensions":{"code":"SUBSCRIPTIONS_NOT_SUPPORTED"}}]}
+
+ event: complete
+ "#);
+ }
+
+ #[ntex::test]
+ async fn subscription_no_entity_resolution_sse_subgraph() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_http_streaming_subscriptions_protocol(
+ subgraphs::HTTPStreamingSubscriptionProtocol::SseOnly,
+ )
+ .build()
+ .start()
+ .await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ product {
+ upc
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ },
+ )
+ .await;
+
+ assert_eq!(res.status(), 200, "Expected 200 OK");
+
+ let content_type_header = res
+ .header("content-type")
+ .expect("must have content-type header");
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert_snapshot!(body_str, @r#"
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"1"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"1"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"1"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"1"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"2"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"2"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"2"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"2"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"3"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"4"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"4"}}}}
+
+ event: complete
+ "#);
+
+ // we check this at the end because the body will hold clues to why the test fails
+ assert_eq!(
+ content_type_header, "text/event-stream",
+ "Expected Content-Type to be text/event-stream"
+ );
+ }
+
+ #[ntex::test]
+ async fn subscription_no_entity_resolution_multipart_subgraph() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_http_streaming_subscriptions_protocol(
+ subgraphs::HTTPStreamingSubscriptionProtocol::MultipartOnly,
+ )
+ .build()
+ .start()
+ .await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ product {
+ upc
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ },
+ )
+ .await;
+
+ assert!(res.status().is_success(), "Expected 200 OK");
+
+ let content_type_header = res
+ .header("content-type")
+ .expect("must have content-type header");
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert_snapshot!(body_str, @r#"
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"1"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"1"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"1"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"1"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"2"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"2"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"2"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"2"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"3"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"4"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"upc":"4"}}}}
+
+ event: complete
+ "#);
+
+ // we check this at the end because the body will hold clues to why the test fails
+ assert_eq!(
+ content_type_header, "text/event-stream",
+ "Expected Content-Type to be text/event-stream"
+ );
+ }
+
+ #[ntex::test]
+ async fn subscription_yes_entity_resolution() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ id
+ product {
+ name
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ },
+ )
+ .await;
+
+ assert!(res.status().is_success(), "Expected 200 OK");
+
+ let content_type_header = res
+ .header("content-type")
+ .expect("must have content-type header");
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert_snapshot!(body_str, @r#"
+ event: next
+ data: {"data":{"reviewAdded":{"id":"1","product":{"name":"Table"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"2","product":{"name":"Table"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"3","product":{"name":"Table"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"4","product":{"name":"Table"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"5","product":{"name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"6","product":{"name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"7","product":{"name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"8","product":{"name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"9","product":{"name":"Glass"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"10","product":{"name":"Chair"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"11","product":{"name":"Chair"}}}}
+
+ event: complete
+ "#);
+
+ // we check this at the end because the body will hold clues to why the test fails
+ assert_eq!(
+ content_type_header, "text/event-stream",
+ "Expected Content-Type to be text/event-stream"
+ );
+ }
+
+ #[ntex::test]
+ async fn subscription_yes_entity_resolution_multipart_client_unquoted_spec() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ id
+ product {
+ name
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => r#"multipart/mixed;subscriptionSpec=1.0"#
+ },
+ )
+ .await;
+
+ assert!(res.status().is_success(), "Expected 200 OK");
+
+ let content_type_header = res
+ .header("content-type")
+ .expect("must have content-type header");
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert_snapshot!(body_str, @r#"
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"1","product":{"name":"Table"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"2","product":{"name":"Table"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"3","product":{"name":"Table"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"4","product":{"name":"Table"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"5","product":{"name":"Couch"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"6","product":{"name":"Couch"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"7","product":{"name":"Couch"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"8","product":{"name":"Couch"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"9","product":{"name":"Glass"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"10","product":{"name":"Chair"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"11","product":{"name":"Chair"}}}}}
+ --graphql--
+ "#);
+
+ // we check this at the end because the body will hold clues to why the test fails
+ assert_eq!(
+ content_type_header,
+ "multipart/mixed;boundary=\"graphql\";subscriptionSpec=1.0",
+ );
+ }
+
+ #[ntex::test]
+ async fn subscription_yes_entity_resolution_multipart_client_quoted_spec() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ id
+ product {
+ name
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ // exactly as per https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol#executing-a-subscription
+ http::header::ACCEPT => r#"multipart/mixed;subscriptionSpec="1.0", application/json"#
+ },
+ )
+ .await;
+
+ assert!(res.status().is_success(), "Expected 200 OK");
+
+ let content_type_header = res
+ .header("content-type")
+ .expect("must have content-type header");
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert_snapshot!(body_str, @r#"
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"1","product":{"name":"Table"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"2","product":{"name":"Table"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"3","product":{"name":"Table"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"4","product":{"name":"Table"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"5","product":{"name":"Couch"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"6","product":{"name":"Couch"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"7","product":{"name":"Couch"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"8","product":{"name":"Couch"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"9","product":{"name":"Glass"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"10","product":{"name":"Chair"}}}}}
+ --graphql
+ Content-Type: application/json
+
+ {"payload":{"data":{"reviewAdded":{"id":"11","product":{"name":"Chair"}}}}}
+ --graphql--
+ "#);
+
+ // we check this at the end because the body will hold clues to why the test fails
+ assert_eq!(
+ content_type_header,
+ "multipart/mixed;boundary=\"graphql\";subscriptionSpec=1.0",
+ );
+ }
+
+ #[ntex::test]
+ async fn subscription_yes_entity_resolution_incremental_delivery_client() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ id
+ product {
+ name
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => r#"multipart/mixed"#
+ },
+ )
+ .await;
+
+ assert!(res.status().is_success(), "Expected 200 OK");
+
+ let content_type_header = res
+ .header("content-type")
+ .expect("must have content-type header");
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert_snapshot!(body_str, @r#"
+ ---
+ Content-Type: application/json
+
+ {"data":{"reviewAdded":{"id":"1","product":{"name":"Table"}}}}
+ ---
+ Content-Type: application/json
+
+ {"data":{"reviewAdded":{"id":"2","product":{"name":"Table"}}}}
+ ---
+ Content-Type: application/json
+
+ {"data":{"reviewAdded":{"id":"3","product":{"name":"Table"}}}}
+ ---
+ Content-Type: application/json
+
+ {"data":{"reviewAdded":{"id":"4","product":{"name":"Table"}}}}
+ ---
+ Content-Type: application/json
+
+ {"data":{"reviewAdded":{"id":"5","product":{"name":"Couch"}}}}
+ ---
+ Content-Type: application/json
+
+ {"data":{"reviewAdded":{"id":"6","product":{"name":"Couch"}}}}
+ ---
+ Content-Type: application/json
+
+ {"data":{"reviewAdded":{"id":"7","product":{"name":"Couch"}}}}
+ ---
+ Content-Type: application/json
+
+ {"data":{"reviewAdded":{"id":"8","product":{"name":"Couch"}}}}
+ ---
+ Content-Type: application/json
+
+ {"data":{"reviewAdded":{"id":"9","product":{"name":"Glass"}}}}
+ ---
+ Content-Type: application/json
+
+ {"data":{"reviewAdded":{"id":"10","product":{"name":"Chair"}}}}
+ ---
+ Content-Type: application/json
+
+ {"data":{"reviewAdded":{"id":"11","product":{"name":"Chair"}}}}
+ -----
+ "#);
+
+ // we check this at the end because the body will hold clues to why the test fails
+ assert_eq!(content_type_header, "multipart/mixed;boundary=\"-\"",);
+ }
+
+ #[ntex::test]
+ async fn subscription_yes_entity_resolution_websocket_subgraph() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ websocket:
+ subgraphs:
+ reviews:
+ path: /reviews/ws
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ id
+ product {
+ name
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ },
+ )
+ .await;
+
+ assert!(res.status().is_success(), "Expected 200 OK");
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert_snapshot!(body_str, @r#"
+ event: next
+ data: {"data":{"reviewAdded":{"id":"1","product":{"name":"Table"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"2","product":{"name":"Table"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"3","product":{"name":"Table"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"4","product":{"name":"Table"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"5","product":{"name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"6","product":{"name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"7","product":{"name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"8","product":{"name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"9","product":{"name":"Glass"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"10","product":{"name":"Chair"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"11","product":{"name":"Chair"}}}}
+
+ event: complete
+ "#);
+ }
+
+ #[ntex::test]
+ async fn subscription_yes_entity_resolution_http_callback_subgraph() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+
+ let router_listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let router_port = router_listener.local_addr().unwrap().port();
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .with_listener(router_listener)
+ .inline_config(format!(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ callback:
+ public_url: http://0.0.0.0:{router_port}/callback
+ subgraphs:
+ - reviews
+ "#
+ ))
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ id
+ product {
+ name
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map!(
+ http::header::ACCEPT => "text/event-stream"
+ ),
+ )
+ .await;
+
+ assert_eq!(res.status(), 200, "Expected 200 OK");
+
+ assert_snapshot!(res.string_body().await, @r#"
+ event: next
+ data: {"data":{"reviewAdded":{"id":"1","product":{"name":"Table"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"2","product":{"name":"Table"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"3","product":{"name":"Table"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"4","product":{"name":"Table"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"5","product":{"name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"6","product":{"name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"7","product":{"name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"8","product":{"name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"9","product":{"name":"Glass"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"10","product":{"name":"Chair"}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"11","product":{"name":"Chair"}}}}
+
+ event: complete
+ "#);
+ }
+
+ #[ntex::test]
+ async fn subscription_entity_resolution_with_requires() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ product {
+ name
+ shippingEstimate
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ },
+ )
+ .await;
+
+ assert!(res.status().is_success(), "Expected 200 OK");
+
+ let content_type_header = res
+ .header("content-type")
+ .expect("must have content-type header");
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert_snapshot!(body_str, @r#"
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"name":"Table","shippingEstimate":50}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"name":"Table","shippingEstimate":50}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"name":"Table","shippingEstimate":50}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"name":"Table","shippingEstimate":50}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"name":"Couch","shippingEstimate":0}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"name":"Couch","shippingEstimate":0}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"name":"Couch","shippingEstimate":0}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"name":"Couch","shippingEstimate":0}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"name":"Glass","shippingEstimate":10}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"name":"Chair","shippingEstimate":50}}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"product":{"name":"Chair","shippingEstimate":50}}}}
+
+ event: complete
+ "#);
+
+ // we check this at the end because the body will hold clues to why the test fails
+ assert_eq!(
+ content_type_header, "text/event-stream",
+ "Expected Content-Type to be text/event-stream"
+ );
+ }
+
+ #[ntex::test]
+ async fn subscription_with_variable_forwarding() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription ($upc: String!) {
+ reviewAddedForProduct(productUpc: $upc, intervalInMs: 0) {
+ product {
+ upc
+ name
+ }
+ }
+ }
+ "#,
+ Some(json!({
+ "upc": "2"
+ })),
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ },
+ )
+ .await;
+
+ assert!(res.status().is_success(), "Expected 200 OK");
+
+ let content_type_header = res
+ .header("content-type")
+ .expect("must have content-type header");
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert_snapshot!(body_str, @r#"
+ event: next
+ data: {"data":{"reviewAddedForProduct":{"product":{"upc":"2","name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAddedForProduct":{"product":{"upc":"2","name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAddedForProduct":{"product":{"upc":"2","name":"Couch"}}}}
+
+ event: next
+ data: {"data":{"reviewAddedForProduct":{"product":{"upc":"2","name":"Couch"}}}}
+
+ event: complete
+ "#);
+
+ // we check this at the end because the body will hold clues to why the test fails
+ assert_eq!(
+ content_type_header, "text/event-stream",
+ "Expected Content-Type to be text/event-stream"
+ );
+ }
+
+ #[ntex::test]
+ async fn subscription_http_accept_multipart_and_sse() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription ($upc: String!) {
+ reviewAddedForProduct(productUpc: $upc, intervalInMs: 0) {
+ product {
+ upc
+ name
+ }
+ }
+ }
+ "#,
+ Some(json!({
+ "upc": "2"
+ })),
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ },
+ )
+ .await;
+
+ assert!(res.status().is_success(), "Expected 200 OK");
+
+ let subgraph_request = subgraphs
+ .get_requests_log("reviews")
+ .expect("expected requests sent to reviews subgraph");
+
+ let Ok(accept_header) = subgraph_request
+ .get(0)
+ .expect("expected at least one request to reviews")
+ .headers
+ .get("accept")
+ .expect("expected accept header to be sent with the subgraph request")
+ .to_str()
+ else {
+ panic!("accept header could not be converted to string")
+ };
+
+ assert_snapshot!(accept_header, @r#"multipart/mixed;subscriptionSpec="1.0", text/event-stream"#);
+ }
+
+ #[ntex::test]
+ async fn subscription_stream_failed_source_subgraph_requests() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_on_request(|_req| {
+ Some(ResponseLike::new(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ None,
+ None,
+ ))
+ })
+ .build()
+ .start()
+ .await;
+
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ id
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ },
+ )
+ .await;
+
+ assert_eq!(res.status(), 200, "Expected 200 OK");
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert!(
+ body_str.contains("SUBGRAPH_STREAM_STATUS_CODE_NOT_OK"),
+ "Expected '{}' to contain the subgraph stream response not-ok failure error code",
+ body_str
+ );
+ }
+
+ #[ntex::test]
+ async fn subscription_stream_failed_entity_resolution_requests() {
+ let subgraphs = TestSubgraphs::builder()
+ .with_on_request(|req| {
+ if req.path.contains("products") {
+ // entity resolution
+ Some(ResponseLike::new(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Some(
+ json!({
+ "errors": [{"message": "Something Went Wrong!"}]
+ })
+ .to_string(),
+ ),
+ some_header_map! {
+ http::header::CONTENT_TYPE => "application/json"
+ },
+ ))
+ } else {
+ // subscription itself (on "reviews" subgraph)
+ None
+ }
+ })
+ .build()
+ .start()
+ .await;
+
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription ($upc: String!) {
+ reviewAddedForProduct(productUpc: $upc, intervalInMs: 0) {
+ product {
+ name
+ }
+ }
+ }
+ "#,
+ Some(json!({
+ "upc": "2"
+ })),
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ },
+ )
+ .await;
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert_snapshot!(body_str, @r#"
+ event: next
+ data: {"data":{"reviewAddedForProduct":{"product":{"name":null}}},"errors":[{"message":"Something Went Wrong!","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR","serviceName":"products","affectedPath":"reviewAddedForProduct.product"}}]}
+
+ event: next
+ data: {"data":{"reviewAddedForProduct":{"product":{"name":null}}},"errors":[{"message":"Something Went Wrong!","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR","serviceName":"products","affectedPath":"reviewAddedForProduct.product"}}]}
+
+ event: next
+ data: {"data":{"reviewAddedForProduct":{"product":{"name":null}}},"errors":[{"message":"Something Went Wrong!","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR","serviceName":"products","affectedPath":"reviewAddedForProduct.product"}}]}
+
+ event: next
+ data: {"data":{"reviewAddedForProduct":{"product":{"name":null}}},"errors":[{"message":"Something Went Wrong!","extensions":{"code":"DOWNSTREAM_SERVICE_ERROR","serviceName":"products","affectedPath":"reviewAddedForProduct.product"}}]}
+
+ event: complete
+ "#);
+ }
+
+ #[ntex::test]
+ async fn subscription_stream_client_cancelled() {
+ use futures::StreamExt;
+
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ // Use a longer interval so we have time to cancel
+ let mut res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 100) {
+ id
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ },
+ )
+ .await;
+
+ assert!(res.status().is_success(), "Expected 200 OK");
+
+ // read first chunk
+ let chunk_bytes = res.next().await.unwrap().unwrap();
+ let chunk_str = std::str::from_utf8(&chunk_bytes).unwrap();
+
+ assert_snapshot!(chunk_str, @r#"
+ event: next
+ data: {"data":{"reviewAdded":{"id":"1"}}}
+ "#);
+
+ // read second chunk to ensure stream is flowing
+ let chunk_bytes = res.next().await.unwrap().unwrap();
+ let chunk_str = std::str::from_utf8(&chunk_bytes).unwrap();
+
+ assert_snapshot!(chunk_str, @r#"
+ event: next
+ data: {"data":{"reviewAdded":{"id":"2"}}}
+ "#);
+
+ // cancel
+ drop(res);
+
+ // TODO: check if propagated?
+ }
+
+ #[ntex::test]
+ async fn subscription_header_propagation_for_subscription() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .file_config("configs/header_propagation.router.yaml")
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ id
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream",
+ http::header::HeaderName::from_static("x-context") => "maybe-propagate"
+ },
+ )
+ .await;
+
+ assert_eq!(res.status(), 200, "Expected 200 OK");
+
+ // we have to consume the body to ensure the subscription is fully processed
+ let body = res.body().await.unwrap();
+ std::str::from_utf8(&body).unwrap();
+
+ let subgraph_requests = subgraphs
+ .get_requests_log("reviews")
+ .expect("expected requests sent to reviews subgraph");
+
+ let context_header = subgraph_requests[0]
+ .headers
+ .get("x-context")
+ .expect("expected x-context header to be present");
+
+ assert_eq!(
+ context_header, "maybe-propagate",
+ "expected x-context header to be propagated to subgraph"
+ );
+ }
+
+ #[ntex::test]
+ async fn subscription_header_propagation_for_entity_resolution() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .file_config("configs/header_propagation.router.yaml")
+ .build()
+ .start()
+ .await;
+
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 0) {
+ product {
+ name
+ }
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream",
+ http::header::HeaderName::from_static("x-context") => "maybe-propagate"
+ },
+ )
+ .await;
+
+ assert_eq!(res.status(), 200, "Expected 200 OK");
+
+ // we have to consume the body to ensure all entity resolutions were made
+ let body = res.body().await.unwrap();
+ std::str::from_utf8(&body).unwrap();
+
+ let subgraph_requests = subgraphs
+ .get_requests_log("products")
+ .expect("expected requests sent to products subgraph");
+
+ // every entity resolution request must have the propagated header
+ for subgraph_request in subgraph_requests {
+ let context_header = subgraph_request
+ .headers
+ .get("x-context")
+ .expect("expected x-context header to be present");
+
+ assert_eq!(
+ context_header, "maybe-propagate",
+ "expected x-context header to be propagated to subgraph"
+ );
+ }
+ }
+
+ #[ntex::test]
+ async fn subscription_propagate_connection_termination_subgraph() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ headers:
+ all:
+ request:
+ - propagate:
+ named: x-break-after
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ // NOTE: we add a 100ms interval because providing 0 will end the connection while the buffer is still being written to leading to a different error
+ let res = router
+ .send_graphql_request(
+ r#"
+ subscription {
+ reviewAdded(intervalInMs: 100) {
+ id
+ }
+ }
+ "#,
+ None,
+ some_header_map! {
+ http::header::ACCEPT => "text/event-stream",
+ http::header::HeaderName::from_static("x-break-after") => "3"
+ },
+ )
+ .await;
+
+ let body = res.body().await.unwrap();
+ let body_str = std::str::from_utf8(&body).unwrap();
+
+ assert_snapshot!(body_str, @r#"
+ event: next
+ data: {"data":{"reviewAdded":{"id":"1"}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"2"}}}
+
+ event: next
+ data: {"data":{"reviewAdded":{"id":"3"}}}
+
+ event: next
+ data: {"errors":[{"message":"Error reading SSE subscription stream: Stream read error: error reading a body from connection","extensions":{"code":"SUBGRAPH_SUBSCRIPTION_SSE_STREAM_ERROR","serviceName":"reviews"}}]}
+
+ event: complete
+ "#);
+ }
+
+ #[ntex::test]
+ async fn active_subscriptions_deduplication() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ traffic_shaping:
+ router:
+ dedupe:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let query = r#"
+ subscription {
+ reviewAdded(intervalInMs: 100) {
+ id
+ product {
+ name
+ }
+ }
+ }
+ "#;
+ let headers = some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ };
+
+ let (sub1, sub2, sub3) = tokio::join!(
+ router.send_graphql_request(query, None, headers.clone()),
+ router.send_graphql_request(query, None, headers.clone()),
+ router.send_graphql_request(query, None, headers.clone()),
+ );
+
+ for sub in [&sub1, &sub2, &sub3] {
+ let body = sub.string_body().await;
+ assert!(
+ body.contains("event: next") && body.contains("event: complete"),
+ "Expected subscription to receive events and complete"
+ );
+ }
+
+ let reviews_requests = subgraphs.get_requests_log("reviews").unwrap_or_default();
+ assert_eq!(
+ reviews_requests.len(),
+ 1,
+ "Expected requests to reviews subgraph to be deduplicated"
+ );
+ }
+
+ #[ntex::test]
+ async fn active_subscriptions_deduplication_promotion() {
+ use futures::StreamExt;
+
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ traffic_shaping:
+ router:
+ dedupe:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let query = r#"
+ subscription {
+ reviewAdded(intervalInMs: 100) {
+ id
+ }
+ }
+ "#;
+ let headers = some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ };
+
+ let mut sub1 = router
+ .send_graphql_request(query, None, headers.clone())
+ .await;
+
+ assert!(sub1.status().is_success(), "Expected 200 OK");
+
+ // consume 2 events from sub1 to let the source stream advance
+ let chunk = sub1.next().await.unwrap().unwrap();
+ assert!(
+ std::str::from_utf8(&chunk).unwrap().contains(r#""id":"1""#),
+ "Expected first event to be id=1"
+ );
+ let chunk = sub1.next().await.unwrap().unwrap();
+ assert!(
+ std::str::from_utf8(&chunk).unwrap().contains(r#""id":"2""#),
+ "Expected second event to be id=2"
+ );
+ let chunk = sub1.next().await.unwrap().unwrap();
+ assert!(
+ std::str::from_utf8(&chunk).unwrap().contains(r#""id":"3""#),
+ "Expected third event to be id=3"
+ );
+
+ // subscribe again with the same query - dedup promotes sub2 onto the live source
+ let sub2 = router
+ .send_graphql_request(query, None, headers.clone())
+ .await;
+
+ assert!(sub2.status().is_success(), "Expected 200 OK");
+
+ // drop sub1 now that sub2 is connected; sub2 must become the active subscriber
+ drop(sub1);
+
+ // sub2 should receive the remainder of the stream from where the source left off
+ let body = sub2.string_body().await;
+ assert!(
+ body.contains("event: next") && body.contains("event: complete"),
+ "Expected sub2 to receive remaining events and complete, got: {body}"
+ );
+
+ // sub2 must not have received the first 3 events that were already consumed by sub1
+ assert!(
+ !body.contains(r#""id":"1""#)
+ && !body.contains(r#""id":"2""#)
+ && !body.contains(r#""id":"3""#),
+ "Expected sub2 to not replay events already consumed by sub1, got: {body}"
+ );
+
+ // only one subgraph request should have been made
+ let reviews_requests = subgraphs.get_requests_log("reviews").unwrap_or_default();
+ assert_eq!(
+ reviews_requests.len(),
+ 1,
+ "Expected requests to reviews subgraph to be deduplicated"
+ );
+ }
+
+ #[ntex::test]
+ async fn active_across_transports_subscriptions_deduplication() {
+ use futures::StreamExt;
+ use hive_router_plan_executor::executors::{
+ graphql_transport_ws::SubscribePayload, websocket_client::WsClient,
+ };
+
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ websocket:
+ enabled: true
+ traffic_shaping:
+ router:
+ dedupe:
+ enabled: true
+ headers: none
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let query = r#"
+ subscription {
+ reviewAdded(intervalInMs: 100) {
+ id
+ product {
+ name
+ }
+ }
+ }
+ "#;
+
+ let sse_headers = some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ };
+ let multipart_headers = some_header_map! {
+ http::header::ACCEPT => "multipart/mixed;subscriptionSpec=1.0"
+ };
+
+ let wsconn = router.ws().await;
+ let mut ws_client = WsClient::init(wsconn, None)
+ .await
+ .expect("Failed to init WsClient");
+ let ws_payload = SubscribePayload {
+ query: query.into(),
+ ..Default::default()
+ };
+ let mut ws_stream = ws_client.subscribe(ws_payload).await;
+
+ let (sub_sse, sub_multipart) = tokio::join!(
+ router.send_graphql_request(query, None, sse_headers),
+ router.send_graphql_request(query, None, multipart_headers),
+ );
+
+ let sse_body = sub_sse.string_body().await;
+ assert!(
+ sse_body.contains("event: next") && sse_body.contains("event: complete"),
+ "Expected SSE subscription to receive events and complete"
+ );
+
+ let multipart_body = sub_multipart.string_body().await;
+ assert!(
+ multipart_body.contains("--graphql") && multipart_body.contains("--graphql--"),
+ "Expected multipart subscription to receive events and complete"
+ );
+
+ let mut ws_received = 0;
+ while let Some(response) = ws_stream.next().await {
+ assert!(
+ response.errors.is_none(),
+ "Expected no errors from WS subscription"
+ );
+ assert!(
+ !response.data.is_null(),
+ "Expected data from WS subscription"
+ );
+ ws_received += 1;
+ }
+ assert!(
+ ws_received > 0,
+ "Expected WS subscription to receive at least one event"
+ );
+
+ let reviews_requests = subgraphs.get_requests_log("reviews").unwrap_or_default();
+ assert_eq!(
+ reviews_requests.len(),
+ 1,
+ "Expected requests to reviews subgraph to be deduplicated across transports"
+ );
+ }
+
+ #[ntex::test]
+ async fn active_across_transports_subscriptions_deduplication_promotion() {
+ use futures::StreamExt;
+ use hive_router_plan_executor::executors::{
+ graphql_transport_ws::SubscribePayload, websocket_client::WsClient,
+ };
+
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ websocket:
+ enabled: true
+ traffic_shaping:
+ router:
+ dedupe:
+ enabled: true
+ headers: none
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let query = r#"
+ subscription {
+ reviewAdded(intervalInMs: 100) {
+ id
+ }
+ }
+ "#;
+
+ let wsconn = router.ws().await;
+ let mut ws_client = WsClient::init(wsconn, None)
+ .await
+ .expect("Failed to init WsClient");
+ let ws_payload = SubscribePayload {
+ query: query.into(),
+ ..Default::default()
+ };
+ let mut ws_stream = ws_client.subscribe(ws_payload).await;
+
+ // consume 3 events from sub1 to let the source stream advance
+ let response = ws_stream.next().await.unwrap();
+ assert!(
+ response.data.to_string().contains(r#""id": "1""#),
+ "Expected first event to be id=1"
+ );
+ let response = ws_stream.next().await.unwrap();
+ assert!(
+ response.data.to_string().contains(r#""id": "2""#),
+ "Expected second event to be id=2"
+ );
+ let response = ws_stream.next().await.unwrap();
+ assert!(
+ response.data.to_string().contains(r#""id": "3""#),
+ "Expected third event to be id=3"
+ );
+
+ // subscribe again with SSE - dedup promotes sub2 onto the live source
+ let sse_headers = some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ };
+ let sub2 = router.send_graphql_request(query, None, sse_headers).await;
+
+ assert!(sub2.status().is_success(), "Expected 200 OK");
+
+ // drop the WS sub now that sub2 is connected; sub2 must become the active subscriber
+ drop(ws_stream);
+ drop(ws_client);
+
+ // sub2 should receive the remainder of the stream from where the source left off
+ let body = sub2.string_body().await;
+ assert!(
+ body.contains("event: next") && body.contains("event: complete"),
+ "Expected sub2 to receive remaining events and complete, got: {body}"
+ );
+
+ // sub2 must not have received the first 3 events already consumed by the WS sub
+ assert!(
+ !body.contains(r#""id":"1""#)
+ && !body.contains(r#""id":"2""#)
+ && !body.contains(r#""id":"3""#),
+ "Expected sub2 to not replay events already consumed by the WS sub, got: {body}"
+ );
+
+ // only one subgraph request should have been made
+ let reviews_requests = subgraphs.get_requests_log("reviews").unwrap_or_default();
+ assert_eq!(
+ reviews_requests.len(),
+ 1,
+ "Expected requests to reviews subgraph to be deduplicated across transports"
+ );
+ }
+
+ #[ntex::test]
+ async fn max_long_lived_clients_rejects_over_limit() {
+ use futures::StreamExt;
+
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ subscriptions:
+ enabled: true
+ traffic_shaping:
+ router:
+ max_long_lived_clients: 2
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let query = r#"
+ subscription {
+ reviewAdded(intervalInMs: 200) {
+ id
+ }
+ }
+ "#;
+ let headers = some_header_map! {
+ http::header::ACCEPT => "text/event-stream"
+ };
+
+ // open two subscriptions and keep them alive by reading the first event
+ let mut sub1 = router
+ .send_graphql_request(query, None, headers.clone())
+ .await;
+ assert!(sub1.status().is_success(), "sub1 should be accepted");
+ let _ = sub1.next().await;
+
+ let mut sub2 = router
+ .send_graphql_request(query, None, headers.clone())
+ .await;
+ assert!(sub2.status().is_success(), "sub2 should be accepted");
+ let _ = sub2.next().await;
+
+ // the third subscriber exceeds the limit and must be rejected
+ let sub3 = router
+ .send_graphql_request(query, None, headers.clone())
+ .await;
+ assert_eq!(
+ sub3.status(),
+ reqwest::StatusCode::SERVICE_UNAVAILABLE,
+ "sub3 should be rejected with 503 when the limit is reached"
+ );
+ let retry_after = sub3.header("retry-after");
+ assert!(
+ retry_after.is_some(),
+ "rejected response should include a Retry-After header"
+ );
+ let body = sub3.string_body().await;
+ assert_eq!(body, "Too many long-lived clients");
+
+ // release the two held subscriptions
+ drop(sub1);
+ drop(sub2);
+
+ // wait briefly for the slots to be freed
+ tokio::time::sleep(std::time::Duration::from_millis(100)).await;
+
+ // a new subscriber should now be accepted again
+ let sub4 = router
+ .send_graphql_request(query, None, headers.clone())
+ .await;
+ assert!(
+ sub4.status().is_success(),
+ "sub4 should be accepted after the previous slots were freed"
+ );
+ }
+}
diff --git a/e2e/src/supergraph.rs b/e2e/src/supergraph.rs
index 44598274a..cc4d7ca6d 100644
--- a/e2e/src/supergraph.rs
+++ b/e2e/src/supergraph.rs
@@ -21,25 +21,30 @@ mod supergraph_e2e_tests {
let router = TestRouter::builder()
.inline_config(format!(
r#"
- supergraph:
- source: hive
- endpoint: http://{host}/supergraph
- key: dummy_key
- poll_interval: 300ms
- "#,
+ supergraph:
+ source: hive
+ endpoint: http://{host}/supergraph
+ key: dummy_key
+ poll_interval: 500ms
+ "#,
))
.build()
.start()
.await;
- let res = router
- .send_graphql_request("{ __schema { types { name } } }", None, None)
- .await;
- assert!(res.status().is_success(), "Expected 200 OK");
+ wait_until_mock_matched(&mock1)
+ .await
+ .expect("Expected mock1 to be matched");
// wait for caches to populate
- let deadline = std::time::Instant::now() + Duration::from_secs(5);
+ let deadline = std::time::Instant::now() + Duration::from_secs(10);
loop {
+ // we keep making requests to ensure the cache because its flakey
+ let res = router
+ .send_graphql_request("{ __schema { types { name } } }", None, None)
+ .await;
+ assert!(res.status().is_success(), "Expected 200 OK");
+
router
.schema_state()
.normalize_cache
@@ -51,16 +56,18 @@ mod supergraph_e2e_tests {
{
break;
}
+
assert!(
std::time::Instant::now() < deadline,
"timed out waiting for caches to populate: plan={}, normalize={}",
router.schema_state().plan_cache.entry_count(),
router.schema_state().normalize_cache.entry_count()
);
- ntex::time::sleep(Duration::from_millis(100)).await;
+ ntex::time::sleep(Duration::from_millis(50)).await;
}
mock1.remove();
+
let mock2 = server
.mock("GET", "/supergraph")
.expect_at_least(1)
@@ -73,11 +80,8 @@ mod supergraph_e2e_tests {
.await
.expect("Expected mock2 to be matched");
- // wait for the router to finish rebuilding with the new supergraph
- router.wait_for_ready(None).await;
-
- // wait for cache invalidation to be reflected (moka invalidate_all is lazy)
- let deadline = std::time::Instant::now() + Duration::from_secs(5);
+ // wait for cache invalidation to be reflected
+ let deadline = std::time::Instant::now() + Duration::from_secs(10);
loop {
router
.schema_state()
@@ -96,7 +100,7 @@ mod supergraph_e2e_tests {
router.schema_state().plan_cache.entry_count(),
router.schema_state().normalize_cache.entry_count()
);
- ntex::time::sleep(Duration::from_millis(100)).await;
+ ntex::time::sleep(Duration::from_millis(50)).await;
}
}
@@ -139,7 +143,7 @@ mod supergraph_e2e_tests {
source: hive
endpoint: http://{host}/supergraph
key: dummy_key
- poll_interval: 100ms
+ poll_interval: 500ms
"#,
))
.build()
@@ -290,7 +294,7 @@ mod supergraph_e2e_tests {
source: hive
endpoint: http://{host}/supergraph
key: dummy_key
- poll_interval: 200ms
+ poll_interval: 500ms
"#,
))
.build()
diff --git a/e2e/src/testkit/mod.rs b/e2e/src/testkit/mod.rs
index 2e83fbd8f..af2b06ac4 100644
--- a/e2e/src/testkit/mod.rs
+++ b/e2e/src/testkit/mod.rs
@@ -8,7 +8,9 @@ use lazy_static::lazy_static;
use mockito::Mock;
use ntex::{
client::ClientResponse,
+ io::Sealed,
web::{self, test},
+ ws::WsConnection,
};
use reqwest::header::{ACCEPT, CONTENT_TYPE};
use sonic_rs::json;
@@ -23,19 +25,23 @@ use std::{
};
use tempfile::{NamedTempFile, TempPath};
use tokio::{
- net::TcpListener,
sync::{oneshot, Semaphore},
time,
};
use tracing::{info, warn};
use hive_router::{
- background_tasks::BackgroundTasksManager, configure_app_from_config, configure_ntex_app,
- init_rustls_crypto_provider, invoke_shutdown_hooks, plugins::plugins_service::PluginService,
- telemetry::Telemetry, PluginRegistry, RouterPaths, RouterSharedState, SchemaState,
+ add_callback_handler, background_tasks::BackgroundTasksManager, configure_app_from_config,
+ configure_ntex_app, init_rustls_crypto_provider, invoke_shutdown_hooks,
+ pipeline::long_lived_client_limit::LongLivedClientLimitService,
+ plugins::plugins_service::PluginService, telemetry::Telemetry, PluginRegistry, RouterPaths,
+ RouterSharedState, SchemaState,
};
-use hive_router_config::{load_config, parse_yaml_config, HiveRouterConfig};
-use subgraphs::subgraphs_app;
+use hive_router_config::{
+ load_config, parse_yaml_config, subscriptions::CallbackConfig, HiveRouterConfig,
+};
+use hive_router_plan_executor::executors::websocket_client;
+use subgraphs::{subgraphs_app, HTTPStreamingSubscriptionProtocol};
// utilities
@@ -63,6 +69,17 @@ macro_rules! flakey {
// #[macro_export] always hoists to the crate root so we re-export it here module level
pub use flakey;
+/// Binds a TCP listener to an OS-assigned port and returns that port number.
+/// The listener is immediately dropped, so the port is free for the caller to use.
+pub fn get_available_port() -> u16 {
+ let listener =
+ std::net::TcpListener::bind("127.0.0.1:0").expect("failed to bind to get available port");
+ listener
+ .local_addr()
+ .expect("failed to get local address")
+ .port()
+}
+
/// Creates a Some(http::HeaderMap) from a list of key-value pairs, for use in test requests.
#[macro_export]
macro_rules! some_header_map {
@@ -240,6 +257,7 @@ impl ResponseLike {
type OnRequest = dyn Fn(RequestLike) -> Option + Send + Sync;
pub struct TestSubgraphsBuilder {
+ subscriptions_protocol: HTTPStreamingSubscriptionProtocol,
on_request: Option>,
delay: Option,
}
@@ -249,10 +267,18 @@ impl TestSubgraphsBuilder {
Self {
on_request: None,
delay: None,
+ subscriptions_protocol: HTTPStreamingSubscriptionProtocol::default(),
}
}
- #[allow(unused)]
+ pub fn with_http_streaming_subscriptions_protocol(
+ mut self,
+ protocol: HTTPStreamingSubscriptionProtocol,
+ ) -> Self {
+ self.subscriptions_protocol = protocol;
+ self
+ }
+
pub fn with_on_request(
mut self,
on_request: impl Fn(RequestLike) -> Option + Send + Sync + 'static,
@@ -275,6 +301,7 @@ impl TestSubgraphsBuilder {
TestSubgraphs {
on_request: self.on_request,
delay: self.delay,
+ subscriptions_protocol: self.subscriptions_protocol,
handle: None,
_state: PhantomData,
}
@@ -294,6 +321,7 @@ struct TestSubgraphsHandle {
}
pub struct TestSubgraphs {
+ subscriptions_protocol: HTTPStreamingSubscriptionProtocol,
on_request: Option>,
delay: Option,
handle: Option,
@@ -372,12 +400,12 @@ impl TestSubgraphs {
}
pub async fn start(self) -> TestSubgraphs {
- let listener = TcpListener::bind("127.0.0.1:0")
+ let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("failed to bind tcp listener");
let addr = listener.local_addr().expect("failed to get local address");
- let mut app = subgraphs_app();
+ let mut app = subgraphs_app(self.subscriptions_protocol.clone());
let middleware_state = Arc::new(TestSubgraphsMiddlewareState {
request_log: DashMap::new(),
@@ -415,6 +443,7 @@ impl TestSubgraphs {
TestSubgraphs {
on_request: self.on_request,
delay: self.delay,
+ subscriptions_protocol: self.subscriptions_protocol,
handle: Some(TestSubgraphsHandle {
shutdown_tx: Some(shutdown_tx),
addr,
@@ -471,6 +500,8 @@ pub struct TestRouterBuilder {
config: Option,
plugins: Vec PluginRegistry>>,
subgraphs_addr: Option,
+ port: u16,
+ listener: Option,
}
impl TestRouterBuilder {
@@ -481,6 +512,8 @@ impl TestRouterBuilder {
config: None,
plugins: vec![],
subgraphs_addr: None,
+ port: 0,
+ listener: None,
}
}
@@ -510,6 +543,16 @@ impl TestRouterBuilder {
self
}
+ pub fn with_port(mut self, port: u16) -> Self {
+ self.port = port;
+ self
+ }
+
+ pub fn with_listener(mut self, listener: std::net::TcpListener) -> Self {
+ self.listener = Some(listener);
+ self
+ }
+
pub fn skip_wait_for_healthy_on_start(mut self) -> Self {
self.wait_for_healthy_on_start = false;
self
@@ -529,6 +572,7 @@ impl TestRouterBuilder {
pub fn build(self) -> TestRouter {
let mut config = self.config.unwrap_or_default();
+ config.http.port = self.port; // sync with config // TODO: what if testing custom port?
let mut _hold_until_drop: Vec> = vec![];
// change the supergraph to use the test subgraphs address
@@ -563,7 +607,11 @@ impl TestRouterBuilder {
TestRouter {
wait_for_healthy_on_start: self.wait_for_healthy_on_start,
wait_for_ready_on_start: self.wait_for_ready_on_start,
- graphql_path: config.http.graphql_endpoint().to_string(),
+ graphql_path: config.graphql_path().to_string(),
+ websocket_path: config.websocket_path().map(|s| s.to_string()),
+ callback_conf: config.callback_conf().cloned(),
+ port: self.port,
+ listener: self.listener,
config: Some(config),
plugins: self.plugins,
handle: None,
@@ -614,7 +662,8 @@ impl Drop for TestRouterHandle {
tracing::info!(
component = "telemetry",
layer = "provider",
- "shutdown scheduled"
+ layer = "provider",
+ "shutdown completed"
);
let _ = provider.force_flush();
let _ = provider.shutdown();
@@ -650,6 +699,10 @@ pub struct TestRouter {
wait_for_healthy_on_start: bool,
wait_for_ready_on_start: bool,
graphql_path: String,
+ websocket_path: Option,
+ callback_conf: Option,
+ port: u16,
+ listener: Option,
config: Option,
plugins: Vec PluginRegistry>>,
handle: Option,
@@ -694,18 +747,71 @@ impl TestRouter {
let serv_shared_state = shared_state.clone();
let serv_schema_state = schema_state.clone();
- let paths = RouterPaths::new(self.graphql_path.clone());
+ let serv_callback_subs = schema_state.callback_subscriptions.clone();
+ let serv_graphql_path = self.graphql_path.clone();
+ let serv_websocket_path = self.websocket_path.clone();
+
+ // when `listen` is set, the callback route lives on a dedicated server bound to that
+ // address as a background task; otherwise it is mounted on the main server
+ let serv_callback_path = match self.callback_conf {
+ Some(CallbackConfig {
+ listen: Some(listen),
+ ref path,
+ ..
+ }) => {
+ let cb_path = path.to_string();
+ let cb_addr = listen.to_string();
+ let cb_subs = schema_state.callback_subscriptions.clone();
+
+ let server = web::HttpServer::new(async move || {
+ let cb_subs = cb_subs.clone();
+ let cb_path = cb_path.clone();
+ web::App::new()
+ .state(cb_subs)
+ .configure(move |m| add_callback_handler(m, &cb_path))
+ })
+ .bind(&cb_addr)
+ .expect("failed to bind callback server")
+ .run();
+
+ bg_tasks_manager.register_handle(async move {
+ server.await.ok();
+ });
+
+ None
+ }
+ Some(ref cb) => Some(cb.path.to_string()),
+ None => None,
+ };
+
+ let paths = RouterPaths::new(
+ serv_graphql_path,
+ serv_websocket_path,
+ serv_callback_path.clone(),
+ );
paths
.detect_conflicts(&prometheus)
.expect("failed to detect endpoint conflicts");
+ let serv_listener = self.listener.unwrap_or(
+ std::net::TcpListener::bind(format!("127.0.0.1:{}", self.port))
+ .expect("failed to bind tcp listener for test server"),
+ );
+ let serv_port = serv_listener
+ .local_addr()
+ .expect("failed to get local address of test server")
+ .port();
let serv_paths = paths.clone();
let serv_prometheus = prometheus.clone();
- let serv = test::server(move || {
+ let long_lived_limit = LongLivedClientLimitService::new(&shared_state.router_config);
+ let serv = test::server_with(test::config().listener(serv_listener), move || {
let shared_state = serv_shared_state.clone();
let schema_state = serv_schema_state.clone();
let paths = serv_paths.clone();
let prometheus = serv_prometheus.clone();
+ let serv_callback_path = serv_callback_path.clone();
+ let callback_subs = serv_callback_subs.clone();
+ let long_lived_limit = long_lived_limit.clone();
// set the tracing dispatch on the server thread. the guard is
// intentionally leaked: dropping it would restore the no-op default
@@ -719,10 +825,17 @@ impl TestRouter {
async move {
web::App::new()
+ .middleware(long_lived_limit)
.middleware(PluginService)
.state(shared_state)
.state(schema_state)
+ .state(callback_subs)
.configure(|m| configure_ntex_app(m, &paths, prometheus))
+ .configure(|m| {
+ if let Some(ref callback) = serv_callback_path {
+ add_callback_handler(m, callback);
+ }
+ })
}
})
.await;
@@ -730,9 +843,13 @@ impl TestRouter {
let mut hold_until_drop = self._hold_until_drop;
hold_until_drop.push(Box::new(subscription_guard));
let started = TestRouter {
+ port: serv_port,
+ listener: None,
wait_for_healthy_on_start: self.wait_for_healthy_on_start,
wait_for_ready_on_start: self.wait_for_ready_on_start,
graphql_path: self.graphql_path,
+ websocket_path: self.websocket_path,
+ callback_conf: self.callback_conf,
handle: Some(TestRouterHandle {
schema_state,
shared_state,
@@ -775,7 +892,7 @@ impl TestRouter {
/// Waits for the /health endpoint to return 200 OK, with an optional timeout (defaults to 5 seconds).
pub async fn wait_for_healthy(&self, timeout: Option) {
- tokio::time::timeout(timeout.unwrap_or(Duration::from_secs(5)), async {
+ tokio::time::timeout(timeout.unwrap_or(Duration::from_secs(10)), async {
loop {
match self.serv().get("/health").send().await {
Ok(response) => {
@@ -796,7 +913,7 @@ impl TestRouter {
/// Waits for the /readiness endpoint to return 200 OK, with an optional timeout (defaults to 5 seconds).
pub async fn wait_for_ready(&self, timeout: Option) {
- tokio::time::timeout(timeout.unwrap_or(Duration::from_secs(5)), async {
+ tokio::time::timeout(timeout.unwrap_or(Duration::from_secs(10)), async {
loop {
match self.serv().get("/readiness").send().await {
Ok(response) => {
@@ -845,6 +962,19 @@ impl TestRouter {
.await
.expect("Failed to send graphql request")
}
+
+ pub async fn ws(&self) -> WsConnection {
+ let url = self.handle.as_ref().unwrap().serv.url(
+ self.websocket_path
+ .as_deref()
+ .expect("Websocket path not set"),
+ );
+ let ws_url = url.as_str().replace("http://", "ws://");
+ let ws_uri = ws_url.parse::().expect("Failed to parse ws url");
+ websocket_client::connect(&ws_uri)
+ .await
+ .expect("Failed to connect to websocket")
+ }
}
pub trait ClientResponseExt {
@@ -874,7 +1004,7 @@ impl ClientResponseExt for ClientResponse {
pub async fn wait_until_mock_matched(mock: &Mock) -> Result<(), String> {
let now = Instant::now();
- let timeout = Duration::from_secs(5); // always a sane default
+ let timeout = Duration::from_secs(10); // always a sane default
loop {
if mock.matched_async().await {
return Ok(());
diff --git a/e2e/src/testkit/otel.rs b/e2e/src/testkit/otel.rs
index 9903519cb..230d5f21c 100644
--- a/e2e/src/testkit/otel.rs
+++ b/e2e/src/testkit/otel.rs
@@ -699,7 +699,7 @@ impl OtlpCollector {
/// Waits for at least `count` traces to be collected, returning all collected traces.
pub async fn wait_for_traces_count(&self, count: usize) -> Vec {
- tokio::time::timeout(Duration::from_secs(5), async {
+ tokio::time::timeout(Duration::from_secs(10), async {
loop {
let traces = self.traces().await;
@@ -722,7 +722,7 @@ impl OtlpCollector {
count: usize,
hive_kind: &str,
) -> Vec {
- tokio::time::timeout(Duration::from_secs(5), async {
+ tokio::time::timeout(Duration::from_secs(10), async {
loop {
let traces = self.traces().await;
let matching = traces
@@ -747,7 +747,7 @@ impl OtlpCollector {
}
pub async fn wait_for_span_by_hive_kind_one(&self, hive_kind: &str) -> CollectedSpan {
- tokio::time::timeout(Duration::from_secs(5), async {
+ tokio::time::timeout(Duration::from_secs(10), async {
loop {
let traces = self.traces().await;
diff --git a/e2e/src/websocket.rs b/e2e/src/websocket.rs
new file mode 100644
index 000000000..a6590776b
--- /dev/null
+++ b/e2e/src/websocket.rs
@@ -0,0 +1,478 @@
+#[cfg(test)]
+mod websocket_e2e_tests {
+ use futures::StreamExt;
+ use sonic_rs::json;
+ use std::collections::HashMap;
+
+ use crate::testkit::{TestRouter, TestSubgraphs};
+ use hive_router_plan_executor::executors::{
+ graphql_transport_ws::{ConnectionInitPayload, SubscribePayload},
+ websocket_client::WsClient,
+ };
+
+ #[ntex::test]
+ async fn query_over_websocket() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ websocket:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let wsconn = router.ws().await;
+
+ let mut client = WsClient::init(wsconn, None)
+ .await
+ .expect("Failed to init WsClient");
+
+ let execution_request = SubscribePayload {
+ query: r#"
+ query {
+ topProducts {
+ name
+ upc
+ }
+ }
+ "#
+ .into(),
+ ..Default::default()
+ };
+
+ let mut stream = client.subscribe(execution_request).await;
+
+ let response = stream.next().await.expect("Expected a response");
+
+ assert!(response.errors.is_none(), "Expected no errors");
+ assert!(!response.data.is_null(), "Expected data");
+
+ let next = stream.next().await;
+ assert!(next.is_none(), "Expected stream to complete after query");
+ }
+
+ #[ntex::test]
+ async fn subscription_over_websocket() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ websocket:
+ enabled: true
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let wsconn = router.ws().await;
+
+ let mut client = WsClient::init(wsconn, None)
+ .await
+ .expect("Failed to init WsClient");
+
+ let subscribe_payload = SubscribePayload {
+ query: r#"
+ subscription {
+ reviewAdded(step: 1, intervalInMs: 0) {
+ id
+ body
+ }
+ }
+ "#
+ .into(),
+ ..Default::default()
+ };
+
+ let mut stream = client.subscribe(subscribe_payload).await;
+
+ let mut received_count = 0;
+ while let Some(response) = stream.next().await {
+ assert!(response.errors.is_none(), "Expected no errors");
+ assert!(!response.data.is_null(), "Expected data");
+ received_count += 1;
+ }
+
+ assert_eq!(
+ received_count, 11,
+ "Expected to receive 11 subscription events"
+ );
+ }
+
+ #[ntex::test]
+ async fn multiple_subscriptions_in_parallel() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ websocket:
+ enabled: true
+ subscriptions:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let wsconn = router.ws().await;
+
+ let mut client = WsClient::init(wsconn, None)
+ .await
+ .expect("Failed to init WsClient");
+
+ let subscribe_payload1 = SubscribePayload {
+ query: r#"
+ subscription {
+ reviewAdded(step: 1, intervalInMs: 0) {
+ id
+ }
+ }
+ "#
+ .into(),
+ ..Default::default()
+ };
+
+ let mut stream1 = client.subscribe(subscribe_payload1).await;
+
+ let subscribe_payload = SubscribePayload {
+ query: r#"
+ subscription {
+ reviewAdded(step: 2, intervalInMs: 0) {
+ id
+ }
+ }
+ "#
+ .into(),
+ ..Default::default()
+ };
+
+ let mut stream2 = client.subscribe(subscribe_payload).await;
+
+ let mut count1 = 0;
+ let mut count2 = 0;
+
+ loop {
+ tokio::select! {
+ maybe_response = stream1.next() => {
+ match maybe_response {
+ Some(response) => {
+ assert!(response.errors.is_none(), "Expected no errors in stream1");
+ count1 += 1;
+ }
+ None => {
+ if count2 > 0 {
+ break;
+ }
+ }
+ }
+ }
+ maybe_response = stream2.next() => {
+ match maybe_response {
+ Some(response) => {
+ assert!(response.errors.is_none(), "Expected no errors in stream2");
+ count2 += 1;
+ }
+ None => {
+ if count1 > 0 {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if count1 > 0 && count2 > 0 {
+ break;
+ }
+ }
+
+ assert!(
+ count1 > 0,
+ "Expected to receive at least one event from stream1"
+ );
+ assert!(
+ count2 > 0,
+ "Expected to receive at least one event from stream2"
+ );
+ }
+
+ #[ntex::test]
+ async fn header_propagation_from_connection_init_payload() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ headers:
+ all:
+ request:
+ - propagate:
+ named: x-context
+ websocket:
+ enabled: true
+ # default headers.source: connection
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let wsconn = router.ws().await;
+
+ let mut client = WsClient::init(
+ wsconn,
+ Some(ConnectionInitPayload::new(HashMap::from([(
+ "x-context".to_string(),
+ json!("my-init_payload-value"),
+ )]))),
+ )
+ .await
+ .expect("Failed to init WsClient");
+
+ let subscribe_payload = SubscribePayload {
+ query: r#"
+ query {
+ topProducts {
+ name
+ upc
+ }
+ }
+ "#
+ .into(),
+ ..Default::default()
+ };
+
+ let mut stream = client.subscribe(subscribe_payload).await;
+
+ stream.next().await.expect("Expected a response");
+
+ let products_requests = subgraphs
+ .get_requests_log("products")
+ .expect("expected requests sent to products subgraph");
+ let last_products_request = products_requests
+ .last()
+ .expect("expected at least one request to products subgraph");
+ assert_eq!(
+ last_products_request
+ .headers
+ .get("x-context")
+ .expect("expected x-context header to be present"),
+ "my-init_payload-value",
+ )
+ }
+
+ #[ntex::test]
+ async fn header_propagation_from_operation_extensions() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ headers:
+ all:
+ request:
+ - propagate:
+ named: x-context
+ websocket:
+ enabled: true
+ headers:
+ source: operation
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let wsconn = router.ws().await;
+
+ let mut client = WsClient::init(wsconn, None)
+ .await
+ .expect("Failed to init WsClient");
+
+ let subscribe_payload = SubscribePayload {
+ query: r#"
+ query {
+ topProducts {
+ name
+ upc
+ }
+ }
+ "#
+ .into(),
+ extensions: Some(HashMap::from([(
+ "headers".to_string(),
+ json!({"x-context": "my-extensions-value"}),
+ )])),
+ ..Default::default()
+ };
+
+ let mut stream = client.subscribe(subscribe_payload).await;
+
+ stream.next().await.expect("Expected a response");
+
+ let products_requests = subgraphs
+ .get_requests_log("products")
+ .expect("expected requests sent to products subgraph");
+ let last_products_request = products_requests
+ .last()
+ .expect("expected at least one request to products subgraph");
+ assert_eq!(
+ last_products_request
+ .headers
+ .get("x-context")
+ .expect("expected x-context header to be present"),
+ "my-extensions-value",
+ )
+ }
+
+ #[ntex::test]
+ async fn merged_header_propagation_from_both_connection_init_payload_and_operation_extensions()
+ {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ headers:
+ all:
+ request:
+ - propagate:
+ named: x-context
+ websocket:
+ enabled: true
+ headers:
+ source: both
+ persist: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let wsconn = router.ws().await;
+
+ let mut client = WsClient::init(
+ wsconn,
+ Some(ConnectionInitPayload::new(HashMap::from([(
+ "x-context".to_string(),
+ json!("my-init_payload-value"),
+ )]))),
+ )
+ .await
+ .expect("Failed to init WsClient");
+
+ let subscribe_payload = SubscribePayload {
+ query: r#"
+ query {
+ topProducts {
+ name
+ upc
+ }
+ }
+ "#
+ .into(),
+ extensions: Some(HashMap::from([(
+ "headers".to_string(),
+ json!({"x-context": "my-extensions-value"}),
+ )])),
+ ..Default::default()
+ };
+
+ // merging headers
+ let mut stream = client.subscribe(subscribe_payload).await;
+ stream.next().await.expect("Expected a response");
+
+ let subscribe_payload = SubscribePayload {
+ query: r#"
+ query {
+ topProducts {
+ name
+ upc
+ }
+ }
+ "#
+ .into(),
+ ..Default::default()
+ };
+
+ // missing headers in extensions, should've been merged
+ let mut stream = client.subscribe(subscribe_payload).await;
+ stream.next().await.expect("Expected a response");
+
+ let products_requests = subgraphs
+ .get_requests_log("products")
+ .expect("expected requests sent to products subgraph");
+ let last_products_request = products_requests
+ .last()
+ .expect("expected at least one request to products subgraph");
+ assert_eq!(
+ last_products_request
+ .headers
+ .get("x-context")
+ .expect("expected x-context header to be present"),
+ "my-extensions-value",
+ )
+ }
+
+ #[ntex::test]
+ async fn should_not_steal_non_upgrade_get_requests_on_same_graphql_path() {
+ let subgraphs = TestSubgraphs::builder().build().start().await;
+ let router = TestRouter::builder()
+ .with_subgraphs(&subgraphs)
+ .inline_config(
+ r#"
+ supergraph:
+ source: file
+ path: supergraph.graphql
+ websocket:
+ enabled: true
+ "#,
+ )
+ .build()
+ .start()
+ .await;
+
+ let req = router
+ .serv()
+ .get(router.graphql_path()) // same path as the websocket upgrade endpoint
+ .header(http::header::ACCEPT, "application/graphql-response+json")
+ .query(&[("query", "{ __typename }")])
+ .unwrap();
+
+ let res = req.send().await.unwrap();
+
+ assert_eq!(res.status(), http::StatusCode::OK);
+ assert_eq!(
+ res.header(http::header::CONTENT_TYPE)
+ .expect("expected content-type header to be present"),
+ "application/graphql-response+json"
+ );
+ }
+}
diff --git a/e2e/supergraph.graphql b/e2e/supergraph.graphql
index 655a18c1a..0f158458f 100644
--- a/e2e/supergraph.graphql
+++ b/e2e/supergraph.graphql
@@ -2,6 +2,7 @@ schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
query: Query
+ subscription: Subscription
mutation: Mutation
}
@@ -83,6 +84,8 @@ type Product
shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight")
name: String @join__field(graph: PRODUCTS)
reviews: [Review] @join__field(graph: REVIEWS)
+ notes: String @join__field(graph: PRODUCTS)
+ internal: String @join__field(graph: PRODUCTS)
}
type Query
@@ -96,6 +99,13 @@ type Query
topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS)
}
+type Subscription @join__type(graph: REVIEWS) {
+ reviewAdded(step: Int = 1, intervalInMs: Int = 1000): Review
+ @join__field(graph: REVIEWS)
+ reviewAddedForProduct(productUpc: String!, intervalInMs: Int = 1000): Review
+ @join__field(graph: REVIEWS)
+}
+
type Review @join__type(graph: REVIEWS, key: "id") {
id: ID!
body: String
diff --git a/lib/executor/Cargo.toml b/lib/executor/Cargo.toml
index c74589fec..be8f22a45 100644
--- a/lib/executor/Cargo.toml
+++ b/lib/executor/Cargo.toml
@@ -40,6 +40,7 @@ strum = { workspace = true, features = ["derive"] }
ahash = { workspace = true }
ntex = { workspace = true }
hyper-rustls = { workspace = true}
+rustls = { workspace = true }
hyper-util = { version = "0.1.16", features = [
"client",
@@ -53,6 +54,9 @@ zmij = "1.0.21"
indexmap = "2.10.0"
bumpalo = "3.19.0"
sonic-simd = "0.1.2"
+async-stream = "0.3.6"
+futures-util = "0.3.31"
+ulid = "1.2.1"
[dev-dependencies]
subgraphs = { path = "../../bench/subgraphs" }
diff --git a/lib/executor/src/execution/client_request_details.rs b/lib/executor/src/execution/client_request_details.rs
index b6ebd89fb..aba579055 100644
--- a/lib/executor/src/execution/client_request_details.rs
+++ b/lib/executor/src/execution/client_request_details.rs
@@ -1,4 +1,4 @@
-use std::collections::BTreeMap;
+use std::{collections::BTreeMap, sync::Arc};
use bytes::Bytes;
use hive_router_internal::expressions::{lib::ToVrlValue, vrl::core::Value};
@@ -16,7 +16,7 @@ pub struct ClientRequestDetails<'exec> {
pub url: &'exec http::Uri,
pub headers: &'exec NtexHeaderMap,
pub operation: OperationDetails<'exec>,
- pub jwt: JwtRequestDetails,
+ pub jwt: Arc,
}
pub enum JwtRequestDetails {
@@ -65,7 +65,7 @@ impl From<&ClientRequestDetails<'_>> for Value {
]));
// .request.jwt
- let jwt_value = match &details.jwt {
+ let jwt_value = match details.jwt.as_ref() {
JwtRequestDetails::Authenticated {
token,
prefix,
diff --git a/lib/executor/src/execution/plan.rs b/lib/executor/src/execution/plan.rs
index 57fc7f48c..540ff80e8 100644
--- a/lib/executor/src/execution/plan.rs
+++ b/lib/executor/src/execution/plan.rs
@@ -1,28 +1,36 @@
use std::collections::hash_map::Entry;
use std::collections::{BTreeSet, HashMap};
use std::sync::Arc;
+use std::vec;
use ahash::{HashMap as AHashMap, HashMapExt};
use bytes::BufMut;
use futures::TryFutureExt;
-use futures::{future::BoxFuture, stream::FuturesUnordered, FutureExt, StreamExt};
+use futures::{
+ future::BoxFuture,
+ stream::{BoxStream, FuturesUnordered},
+ FutureExt, StreamExt,
+};
use hive_router_internal::telemetry::metrics::graphql_metrics::GraphQLErrorMetricsRecorder;
use hive_router_internal::telemetry::traces::spans::graphql::{
GraphQLOperationSpan, GraphQLSpanOperationIdentity, GraphQLSubgraphOperationSpan,
};
use hive_router_query_planner::ast::operation::SubgraphFetchOperation;
+use hive_router_query_planner::planner::query_plan::QUERY_PLAN_KIND;
use hive_router_query_planner::{
ast::operation::OperationDefinition,
planner::plan_nodes::{
ConditionNode, EntityBatch, EntityBatchAlias, FetchRewrite, FlattenNodePath, PlanNode,
- QueryPlan,
+ QueryPlan, SequenceNode,
},
state::supergraph_state::OperationKind,
};
use http::{HeaderMap, StatusCode};
+use serde::Serialize;
use sonic_rs::ValueRef;
use tracing::Instrument;
+use crate::execution::client_request_details::OperationDetails;
use crate::{
context::ExecutionContext,
execution::{
@@ -64,20 +72,31 @@ use crate::{
pub struct QueryPlanExecutionOpts<'exec> {
pub query_plan: &'exec QueryPlan,
- pub operation_for_plan: &'exec OperationDefinition,
- pub projection_plan: &'exec Vec,
- pub headers_plan: &'exec HeaderRulesPlan,
- pub variable_values: &'exec Option>,
+ pub operation_for_plan: Arc,
+ pub projection_plan: Arc>,
+ pub headers_plan: Arc,
+ pub variable_values: Arc