diff --git a/crates/codex-api/src/docs.rs b/crates/codex-api/src/docs.rs index 552584e3..6aa64d40 100644 --- a/crates/codex-api/src/docs.rs +++ b/crates/codex-api/src/docs.rs @@ -168,11 +168,13 @@ The following paths are exempt from rate limiting: // Series endpoints v1::handlers::list_series, + v1::handlers::list_series_full, v1::handlers::search_series, v1::handlers::list_series_filtered, v1::handlers::list_series_external_index, v1::handlers::list_series_alphabetical_groups, v1::handlers::get_series, + v1::handlers::get_series_full, v1::handlers::patch_series, v1::handlers::get_series_books, v1::handlers::purge_series_deleted_books, @@ -303,6 +305,7 @@ The following paths are exempt from rate limiting: v1::handlers::list_books, v1::handlers::list_books_filtered, v1::handlers::get_book, + v1::handlers::get_book_full, v1::handlers::patch_book, v1::handlers::get_adjacent_books, v1::handlers::get_book_file, @@ -487,6 +490,17 @@ The following paths are exempt from rate limiting: v1::handlers::task_queue::reprocess_series_titles, v1::handlers::task_queue::reprocess_library_series_titles, + // Library jobs: per-library scheduled work, plus the field-group catalog + // its editor is built from. + v1::handlers::library_jobs::list_jobs, + v1::handlers::library_jobs::get_job, + v1::handlers::library_jobs::create_job, + v1::handlers::library_jobs::patch_job, + v1::handlers::library_jobs::delete_job, + v1::handlers::library_jobs::run_job_now, + v1::handlers::library_jobs::dry_run_job, + v1::handlers::library_jobs::list_field_groups, + // Filesystem endpoints v1::handlers::browse_filesystem, v1::handlers::list_drives, @@ -952,8 +966,10 @@ The following paths are exempt from rate limiting: v1::dto::CreateApiKeyResponse, v1::dto::UpdateApiKeyRequest, v1::dto::PaginatedResponse, + v1::dto::PaginatedResponse, v1::dto::PaginatedResponse, v1::dto::PaginatedResponse, + v1::dto::PaginatedResponse, // Metrics DTOs v1::dto::MetricsDto, @@ -1301,7 +1317,13 @@ The following paths are exempt from rate limiting: // Third-Party Compatibility (name = "Komga", description = "Komga-compatible API for third-party apps (Komic, etc.)"), ), - modifiers(&SecurityAddon, &OperationIdPrefixer, &TagGroupsModifier, &NullableRefFlattener), + modifiers( + &SecurityAddon, + &OperationIdPrefixer, + &TagGroupsModifier, + &NullableRefFlattener, + &GenericArgumentReferencer, + ), )] pub struct ApiDoc; @@ -1355,6 +1377,120 @@ impl utoipa::Modify for SecurityAddon { /// is left in place: that is an API design problem (the endpoint should answer /// `204 No Content`) and hiding it here would make the document claim a body /// that the server may not send. +/// Make a generic instantiation reference its type argument instead of +/// expanding it inline. +/// +/// utoipa renders `PaginatedResponse` by inlining `SeriesDto`'s +/// schema into `data.items`, even though `SeriesDto` is registered as its own +/// component. The document is correct either way, but every generator that has +/// to name an inline schema invents a fresh type for it: +/// `swift-openapi-generator` produces +/// `PaginatedResponseSeriesDto.DataPayloadPayload` where `getSeries` produces +/// `SeriesDto`. The two are structurally identical and not interchangeable, so +/// a client needs a hand-written conversion for every paginated endpoint. +/// +/// This walks each `Base_Argument` component and replaces any subschema that is +/// byte-identical to `Argument`'s own component with a `$ref` to it. Requiring +/// an exact match is what makes the rewrite safe: it can only ever collapse a +/// copy of a schema the document already contains under that name, so nothing +/// about the described wire format changes. +/// +/// It is not specific to pagination — `KomgaPage` and any future generic get +/// the same treatment. +struct GenericArgumentReferencer; + +impl utoipa::Modify for GenericArgumentReferencer { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + let Some(components) = openapi.components.as_mut() else { + return; + }; + + let known: std::collections::BTreeMap = components + .schemas + .iter() + .filter_map(|(name, schema)| { + serde_json::to_value(schema).ok().map(|v| (name.clone(), v)) + }) + .collect(); + + for (name, schema) in components.schemas.iter_mut() { + // `Base_Argument` is the shape utoipa emits for a concrete + // instantiation of a generic. + let Some((_, argument)) = name.split_once('_') else { + continue; + }; + if argument == name { + continue; + } + let Some(target) = known.get(argument) else { + continue; + }; + reference_argument(schema, argument, target); + } + } +} + +fn reference_argument( + schema: &mut utoipa::openapi::RefOr, + argument: &str, + target: &serde_json::Value, +) { + use utoipa::openapi::{Ref, RefOr}; + + if matches!(schema, RefOr::T(_)) && serde_json::to_value(&*schema).ok().as_ref() == Some(target) + { + *schema = RefOr::Ref(Ref::from_schema_name(argument)); + return; + } + reference_argument_children(schema, argument, target); +} + +fn reference_argument_children( + schema: &mut utoipa::openapi::RefOr, + argument: &str, + target: &serde_json::Value, +) { + use utoipa::openapi::{RefOr, Schema, schema::AdditionalProperties}; + + let RefOr::T(schema) = schema else { + return; + }; + + match schema { + Schema::Object(object) => { + for (_, property) in object.properties.iter_mut() { + reference_argument(property, argument, target); + } + if let Some(additional) = object.additional_properties.as_deref_mut() + && let AdditionalProperties::RefOr(value) = additional + { + reference_argument(value, argument, target); + } + } + Schema::Array(array) => { + if let utoipa::openapi::schema::ArrayItems::RefOrSchema(items) = &mut array.items { + reference_argument(items, argument, target); + } + } + Schema::OneOf(one_of) => { + for item in one_of.items.iter_mut() { + reference_argument(item, argument, target); + } + } + Schema::AllOf(all_of) => { + for item in all_of.items.iter_mut() { + reference_argument(item, argument, target); + } + } + Schema::AnyOf(any_of) => { + for item in any_of.items.iter_mut() { + reference_argument(item, argument, target); + } + } + _ => {} + } +} + struct NullableRefFlattener; impl utoipa::Modify for NullableRefFlattener { diff --git a/crates/codex-api/src/lib.rs b/crates/codex-api/src/lib.rs index 7372b551..e27b25f4 100644 --- a/crates/codex-api/src/lib.rs +++ b/crates/codex-api/src/lib.rs @@ -6,6 +6,7 @@ pub mod image_limit; pub mod middleware; pub mod observability; pub mod permissions; +pub mod ranged_file; pub mod routes; pub mod web; diff --git a/crates/codex-api/src/ranged_file.rs b/crates/codex-api/src/ranged_file.rs new file mode 100644 index 00000000..a611d39b --- /dev/null +++ b/crates/codex-api/src/ranged_file.rs @@ -0,0 +1,523 @@ +//! Conditional, range-capable responses for files served straight off disk. +//! +//! Two things depend on this that a plain 200 cannot offer. A download that +//! drops at 90% has to resume rather than start again, which needs `Range` plus +//! a validator the client can pin the resumed request to. And a CBZ is a ZIP, +//! whose central directory sits at the end of the file and whose entries are +//! independently addressable, so a client that can issue `bytes=-65536` and +//! then a handful of entry ranges can show page one of a 200 MB volume without +//! fetching the other 199 MB. +//! +//! That second case is why the suffix form is not optional: an implementation +//! that handles only `bytes=a-b` satisfies resume and none of the partial-read +//! case. + +use std::path::Path; + +use axum::body::Body; +use axum::http::{HeaderMap, StatusCode, header}; +use axum::response::Response; +use chrono::{DateTime, Utc}; +use httpdate::fmt_http_date; +use tokio::io::{AsyncReadExt, AsyncSeekExt}; +use tokio_util::io::ReaderStream; + +/// Read buffer for the file streams below. +/// +/// `ReaderStream`'s default is 4 KiB, which turns a 700 KiB page into ~170 +/// chunks and a 40 MiB volume into ~10,000, each one a separate poll through +/// the body machinery. Measured against `GET /books/{id}/pages/{n}`, which +/// answers from a single buffered `Vec`, the 4 KiB default was the whole of a +/// 2-3x gap on identical bytes. 64 KiB closes it and still bounds memory per +/// in-flight response. +const STREAM_CHUNK_BYTES: usize = 64 * 1024; + +use crate::error::ApiError; + +/// What a `Range` header asks for, resolved against a known content length. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RangeRequest { + /// No range header, or one this server declines to satisfy. RFC 9110 lets a + /// server ignore a `Range` it does not wish to honour, so a syntactically + /// invalid header, an unknown unit, and a multi-range request all land here + /// and are answered with the whole representation. + Full, + /// A single satisfiable byte range, inclusive at both ends. + Partial { start: u64, end: u64 }, + /// A range that names no byte of this representation. Answered with 416. + Unsatisfiable, +} + +/// Resolve a `Range` header value against the length of the file being served. +/// +/// `len` is the full representation length. `end` in the returned `Partial` is +/// always clamped to `len - 1`, so callers never have to re-check it. +pub fn parse_range(header: Option<&str>, len: u64) -> RangeRequest { + let Some(header) = header else { + return RangeRequest::Full; + }; + + let Some(spec) = header.trim().strip_prefix("bytes=") else { + // An unrecognised unit is not an error; it is a range this server does + // not implement, and the whole representation is a valid answer to it. + return RangeRequest::Full; + }; + + // Multiple ranges would need a multipart/byteranges body. Nothing in play + // asks for one, and the full response remains a correct answer. + if spec.contains(',') { + return RangeRequest::Full; + } + + let spec = spec.trim(); + let Some((first, last)) = spec.split_once('-') else { + return RangeRequest::Full; + }; + let (first, last) = (first.trim(), last.trim()); + + // An empty file has no byte any range could name. + if len == 0 { + return if first.is_empty() && last.is_empty() { + RangeRequest::Full + } else { + RangeRequest::Unsatisfiable + }; + } + + match (first, last) { + // `bytes=-n`: the last n bytes. A zero-length suffix names nothing. + ("", suffix) => match suffix.parse::() { + Ok(0) => RangeRequest::Unsatisfiable, + Ok(n) => RangeRequest::Partial { + start: len.saturating_sub(n), + end: len - 1, + }, + Err(_) => RangeRequest::Full, + }, + // `bytes=a-`: from a to the end. + (start, "") => match start.parse::() { + Ok(start) if start >= len => RangeRequest::Unsatisfiable, + Ok(start) => RangeRequest::Partial { + start, + end: len - 1, + }, + Err(_) => RangeRequest::Full, + }, + // `bytes=a-b`, with b clamped to the last byte. + (start, end) => match (start.parse::(), end.parse::()) { + // A last-byte-pos below first-byte-pos makes the spec invalid + // rather than unsatisfiable, so it is ignored, not rejected. + (Ok(start), Ok(end)) if end < start => RangeRequest::Full, + (Ok(start), Ok(_)) if start >= len => RangeRequest::Unsatisfiable, + (Ok(start), Ok(end)) => RangeRequest::Partial { + start, + end: end.min(len - 1), + }, + _ => RangeRequest::Full, + }, + } +} + +/// Build a `Content-Disposition` naming `file_name`, encoded per RFC 6266. +/// +/// Both parameters are emitted. `filename*` carries the real name; the quoted +/// `filename` is an **ASCII-only fallback** for clients that do not understand +/// the extended form. +/// +/// The fallback has to be transliterated rather than passed through: a header +/// value may only hold visible ASCII, so putting raw UTF-8 in the quoted +/// parameter produces a header a client reads as latin-1 — which is precisely +/// the mangling the extended parameter exists to avoid. Shared with the +/// Komga-compatible download route so the two cannot drift. +pub fn content_disposition_attachment(file_name: &str) -> String { + let fallback: String = file_name + .chars() + .map(|c| match c { + // Anything outside printable ASCII cannot travel in the quoted + // parameter at all, and a quote or backslash would end it early. + '"' | '\\' => '_', + c if c.is_ascii_graphic() || c == ' ' => c, + _ => '_', + }) + .collect(); + + format!( + "attachment; filename=\"{fallback}\"; filename*=UTF-8''{}", + percent_encode_filename(file_name) + ) +} + +/// Percent-encode a filename for the `filename*` parameter (RFC 5987). +/// +/// Unreserved characters per RFC 3986 pass through; everything else is encoded. +fn percent_encode_filename(file_name: &str) -> String { + let mut result = String::with_capacity(file_name.len() * 3); + for byte in file_name.bytes() { + match byte { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => { + result.push(byte as char); + } + _ => { + result.push('%'); + result.push_str(&format!("{byte:02X}")); + } + } + } + result +} + +/// True when a client-supplied validator names this representation. +/// +/// Handles the weak prefix and missing quotes, matching how the thumbnail and +/// PDF page routes already compare ETags. +fn etag_matches(candidate: &str, etag: &str) -> bool { + let candidate = candidate.trim().trim_start_matches("W/"); + candidate == etag || candidate.trim_matches('"') == etag.trim_matches('"') +} + +/// True when any entry of an `If-None-Match` list names this representation. +fn if_none_match_matches(value: &str, etag: &str) -> bool { + value.trim() == "*" || value.split(',').any(|entry| etag_matches(entry, etag)) +} + +/// Serve `path` as a conditional, range-capable response. +/// +/// Answers 304 to a current `If-None-Match`, 206 to a satisfiable `Range`, 416 +/// to one that names no byte, and 200 otherwise. `Accept-Ranges: bytes` goes out +/// on every one of them, because the capability is a property of the resource +/// rather than of the request that happened to arrive. +/// +/// A 206 seeks and bounds the read, so it never touches more of the file than +/// the range covers. +#[allow(clippy::too_many_arguments)] +pub async fn ranged_file_response( + headers: &HeaderMap, + path: &Path, + len: u64, + etag: &str, + last_modified: DateTime, + content_type: &str, + content_disposition: &str, +) -> Result { + let last_modified_str = fmt_http_date(last_modified.into()); + + let base = |builder: axum::http::response::Builder| { + builder + .header(header::ACCEPT_RANGES, "bytes") + .header(header::ETAG, etag) + .header(header::LAST_MODIFIED, &last_modified_str) + }; + + // A fresh cached copy short-circuits everything below, range or not. + if let Some(value) = headers.get(header::IF_NONE_MATCH) + && let Ok(value) = value.to_str() + && if_none_match_matches(value, etag) + { + return Ok(base(Response::builder()) + .status(StatusCode::NOT_MODIFIED) + .body(Body::empty()) + .expect("304 response is well-formed")); + } + + // `If-Range` makes a conditional range: honour the range only if the client + // is resuming the same representation it started on. A stale validator has + // to yield the whole file, because splicing new bytes into a partially + // downloaded old file is how a resume silently corrupts a download. + let range_header = headers + .get(header::RANGE) + .and_then(|value| value.to_str().ok()); + + let if_range_is_current = match headers.get(header::IF_RANGE) { + None => true, + Some(value) => value.to_str().is_ok_and(|value| etag_matches(value, etag)), + }; + + let requested = if if_range_is_current { + parse_range(range_header, len) + } else { + RangeRequest::Full + }; + + match requested { + RangeRequest::Unsatisfiable => Ok(base(Response::builder()) + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(header::CONTENT_RANGE, format!("bytes */{len}")) + .body(Body::empty()) + .expect("416 response is well-formed")), + + RangeRequest::Partial { start, end } => { + let mut file = tokio::fs::File::open(path) + .await + .map_err(|e| ApiError::Internal(format!("Failed to open file: {e}")))?; + file.seek(std::io::SeekFrom::Start(start)) + .await + .map_err(|e| ApiError::Internal(format!("Failed to seek file: {e}")))?; + + let length = end - start + 1; + let body = Body::from_stream(ReaderStream::with_capacity( + file.take(length), + STREAM_CHUNK_BYTES, + )); + + Ok(base(Response::builder()) + .status(StatusCode::PARTIAL_CONTENT) + .header(header::CONTENT_TYPE, content_type) + .header(header::CONTENT_LENGTH, length) + .header(header::CONTENT_RANGE, format!("bytes {start}-{end}/{len}")) + .header(header::CONTENT_DISPOSITION, content_disposition) + .body(body) + .expect("206 response is well-formed")) + } + + RangeRequest::Full => { + let file = tokio::fs::File::open(path) + .await + .map_err(|e| ApiError::Internal(format!("Failed to open file: {e}")))?; + + Ok(base(Response::builder()) + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, content_type) + .header(header::CONTENT_LENGTH, len) + .header(header::CONTENT_DISPOSITION, content_disposition) + .body(Body::from_stream(ReaderStream::with_capacity( + file, + STREAM_CHUNK_BYTES, + ))) + .expect("200 response is well-formed")) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const LEN: u64 = 1000; + + #[test] + fn absent_range_serves_the_whole_representation() { + assert_eq!(parse_range(None, LEN), RangeRequest::Full); + } + + #[test] + fn closed_ranges_resolve_to_their_bounds() { + for (header, start, end) in [ + ("bytes=0-99", 0, 99), + ("bytes=0-0", 0, 0), + ("bytes=999-999", 999, 999), + ("bytes=500-600", 500, 600), + ] { + assert_eq!( + parse_range(Some(header), LEN), + RangeRequest::Partial { start, end }, + "{header}" + ); + } + } + + #[test] + fn a_last_byte_past_the_end_is_clamped_rather_than_rejected() { + assert_eq!( + parse_range(Some("bytes=990-2000"), LEN), + RangeRequest::Partial { + start: 990, + end: 999 + } + ); + } + + #[test] + fn open_ended_ranges_run_to_the_last_byte() { + for (header, start) in [("bytes=0-", 0), ("bytes=500-", 500), ("bytes=999-", 999)] { + assert_eq!( + parse_range(Some(header), LEN), + RangeRequest::Partial { start, end: 999 }, + "{header}" + ); + } + } + + /// The form a client uses to read a ZIP central directory without + /// downloading the archive. + #[test] + fn suffix_ranges_count_back_from_the_end() { + assert_eq!( + parse_range(Some("bytes=-100"), LEN), + RangeRequest::Partial { + start: 900, + end: 999 + } + ); + assert_eq!( + parse_range(Some("bytes=-1"), LEN), + RangeRequest::Partial { + start: 999, + end: 999 + } + ); + } + + #[test] + fn a_suffix_longer_than_the_file_yields_the_whole_file() { + assert_eq!( + parse_range(Some("bytes=-2000"), LEN), + RangeRequest::Partial { start: 0, end: 999 } + ); + } + + #[test] + fn ranges_beyond_the_end_are_unsatisfiable() { + for header in ["bytes=1000-", "bytes=1000-1200", "bytes=5000-6000"] { + assert_eq!( + parse_range(Some(header), LEN), + RangeRequest::Unsatisfiable, + "{header}" + ); + } + } + + /// A zero-length suffix names no byte, so it is unsatisfiable rather than + /// an empty success. + #[test] + fn a_zero_length_suffix_is_unsatisfiable() { + assert_eq!( + parse_range(Some("bytes=-0"), LEN), + RangeRequest::Unsatisfiable + ); + } + + #[test] + fn an_empty_file_can_satisfy_no_range() { + for header in ["bytes=0-", "bytes=0-0", "bytes=-1"] { + assert_eq!( + parse_range(Some(header), 0), + RangeRequest::Unsatisfiable, + "{header}" + ); + } + assert_eq!(parse_range(None, 0), RangeRequest::Full); + } + + /// RFC 9110: a last-byte-pos below first-byte-pos makes the spec invalid, + /// and an invalid `Range` is ignored rather than rejected. + #[test] + fn an_inverted_range_is_ignored_rather_than_rejected() { + assert_eq!(parse_range(Some("bytes=100-50"), LEN), RangeRequest::Full); + } + + #[test] + fn malformed_and_unsupported_ranges_serve_the_whole_representation() { + for header in [ + "bytes=abc", + "bytes=", + "bytes=-", + "bytes=1-abc", + "items=0-99", + "0-99", + "", + ] { + assert_eq!( + parse_range(Some(header), LEN), + RangeRequest::Full, + "{header}" + ); + } + } + + /// Multiple ranges need a `multipart/byteranges` body. RFC 9110 permits + /// ignoring a range the server does not wish to satisfy, so the whole file + /// is a correct answer. + #[test] + fn multi_range_requests_serve_the_whole_representation() { + assert_eq!( + parse_range(Some("bytes=0-99,200-299"), LEN), + RangeRequest::Full + ); + } + + #[test] + fn ascii_filenames_pass_through_the_encoder_unchanged() { + assert_eq!(percent_encode_filename("test.cbz"), "test.cbz"); + assert_eq!( + percent_encode_filename("my-file_v1.0.epub"), + "my-file_v1.0.epub" + ); + } + + #[test] + fn spaces_and_special_characters_are_encoded() { + assert_eq!(percent_encode_filename("My File.cbz"), "My%20File.cbz"); + assert_eq!(percent_encode_filename("file[1].cbz"), "file%5B1%5D.cbz"); + } + + #[test] + fn non_ascii_filenames_are_encoded_rather_than_dropped() { + let encoded = percent_encode_filename("漫画 Vol 1.cbz"); + assert!(encoded.contains('%')); + assert!(encoded.ends_with(".cbz")); + } + + /// Both parameters go out: an ASCII fallback for old clients, the encoded + /// real name for everything else. + #[test] + fn the_disposition_carries_an_ascii_fallback_and_the_encoded_name() { + let disposition = content_disposition_attachment("漫画.cbz"); + assert_eq!( + disposition, + "attachment; filename=\"__.cbz\"; filename*=UTF-8''%E6%BC%AB%E7%94%BB.cbz" + ); + } + + /// A header value may only hold visible ASCII. A disposition that is not + /// ASCII is one the client reads as latin-1, which is the mangling the + /// extended parameter exists to prevent. + #[test] + fn the_disposition_is_always_ascii() { + for name in [ + "漫画 Vol 1.cbz", + "café.epub", + "naïve\u{7f}.pdf", + "plain.cbz", + ] { + let disposition = content_disposition_attachment(name); + assert!( + disposition.is_ascii(), + "{name} produced a non-ASCII header value: {disposition}" + ); + } + } + + /// A quote or backslash would close the quoted parameter early. + #[test] + fn quotes_cannot_escape_the_quoted_parameter() { + let disposition = content_disposition_attachment("a\"b\\c.cbz"); + assert!( + disposition.starts_with("attachment; filename=\"a_b_c.cbz\""), + "{disposition}" + ); + assert!(disposition.contains("filename*=UTF-8''a%22b%5Cc.cbz")); + } + + #[test] + fn an_ascii_filename_survives_intact_in_both_parameters() { + assert_eq!( + content_disposition_attachment("My Volume 1.cbz"), + "attachment; filename=\"My Volume 1.cbz\"; filename*=UTF-8''My%20Volume%201.cbz" + ); + } + + #[test] + fn etags_match_across_weak_prefixes_and_missing_quotes() { + assert!(etag_matches("\"abc\"", "\"abc\"")); + assert!(etag_matches("W/\"abc\"", "\"abc\"")); + assert!(etag_matches("abc", "\"abc\"")); + assert!(!etag_matches("\"def\"", "\"abc\"")); + } + + #[test] + fn if_none_match_accepts_a_list_and_the_wildcard() { + assert!(if_none_match_matches("*", "\"abc\"")); + assert!(if_none_match_matches("\"xyz\", \"abc\"", "\"abc\"")); + assert!(!if_none_match_matches("\"xyz\", \"def\"", "\"abc\"")); + } +} diff --git a/crates/codex-api/src/routes/komga/handlers/books.rs b/crates/codex-api/src/routes/komga/handlers/books.rs index e6a22708..82751275 100644 --- a/crates/codex-api/src/routes/komga/handlers/books.rs +++ b/crates/codex-api/src/routes/komga/handlers/books.rs @@ -825,14 +825,7 @@ pub async fn download_book_file( let stream = ReaderStream::new(file); let body = Body::from_stream(stream); - // Build Content-Disposition header with UTF-8 encoding (RFC 5987) - // Format: attachment; filename="quoted-filename"; filename*=UTF-8''encoded-filename - let filename_encoded = percent_encode_filename(&book.file_name); - let content_disposition = format!( - "attachment; filename=\"{}\"; filename*=UTF-8''{}", - book.file_name.replace('"', "\\\""), - filename_encoded - ); + let content_disposition = crate::ranged_file::content_disposition_attachment(&book.file_name); // Build response with appropriate headers Ok(Response::builder() @@ -872,29 +865,6 @@ pub(crate) async fn get_series_title( } } -/// Percent-encode a filename for use in Content-Disposition header (RFC 5987) -/// -/// Encodes characters that are not allowed in the filename* parameter: -/// - Unreserved characters (A-Z, a-z, 0-9, -._~) are preserved -/// - All other characters are percent-encoded -fn percent_encode_filename(filename: &str) -> String { - let mut result = String::with_capacity(filename.len() * 3); - for byte in filename.bytes() { - match byte { - // Unreserved characters per RFC 3986 (safe in filename*) - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => { - result.push(byte as char); - } - // Everything else gets percent-encoded - _ => { - result.push('%'); - result.push_str(&format!("{:02X}", byte)); - } - } - } - result -} - #[cfg(test)] mod tests { use super::*; @@ -921,36 +891,6 @@ mod tests { assert_eq!(query.sort, Some("createdDate,desc".to_string())); } - #[test] - fn test_percent_encode_filename_ascii() { - // Simple ASCII filename should be mostly unchanged - assert_eq!(percent_encode_filename("test.cbz"), "test.cbz"); - assert_eq!( - percent_encode_filename("my-file_v1.0.epub"), - "my-file_v1.0.epub" - ); - } - - #[test] - fn test_percent_encode_filename_spaces() { - // Spaces should be encoded - assert_eq!(percent_encode_filename("My File.cbz"), "My%20File.cbz"); - } - - #[test] - fn test_percent_encode_filename_unicode() { - // Japanese characters should be encoded - let encoded = percent_encode_filename("漫画 Vol 1.cbz"); - assert!(encoded.contains("%")); - assert!(encoded.ends_with(".cbz")); - } - - #[test] - fn test_percent_encode_filename_special_chars() { - // Special characters should be encoded - assert_eq!(percent_encode_filename("file[1].cbz"), "file%5B1%5D.cbz"); - } - #[test] fn test_parse_komga_sort_param_simple() { // Simple sort with direction diff --git a/crates/codex-api/src/routes/opds/handlers/pse.rs b/crates/codex-api/src/routes/opds/handlers/pse.rs index b6a6f2ad..c63dfff7 100644 --- a/crates/codex-api/src/routes/opds/handlers/pse.rs +++ b/crates/codex-api/src/routes/opds/handlers/pse.rs @@ -135,7 +135,9 @@ pub async fn book_pages( ("page_number" = i32, Path, description = "Page number (1-indexed)") ), responses( - (status = 200, description = "Page image (also records reading progress)", content_type = "image/jpeg"), + // Delegates to the v1 `get_page_image`, so it answers with the same set + // of image types the archive happens to hold. + (status = 200, description = "Page image (also records reading progress)", content_type = "image/*"), (status = 404, description = "Book or page not found"), (status = 403, description = "Forbidden"), ), diff --git a/crates/codex-api/src/routes/v1/dto/common.rs b/crates/codex-api/src/routes/v1/dto/common.rs index cf5a2d31..88e1a014 100644 --- a/crates/codex-api/src/routes/v1/dto/common.rs +++ b/crates/codex-api/src/routes/v1/dto/common.rs @@ -48,60 +48,6 @@ pub struct CoverUploadForm { // Pagination Parameters // ============================================================================= -/// Pagination parameters for list endpoints -#[derive(Debug, Deserialize, IntoParams)] -#[serde(rename_all = "camelCase")] -#[into_params(rename_all = "camelCase")] -#[allow(dead_code)] // Public API - fields read by serde deserialization -pub struct PaginationParams { - /// Page number (1-indexed, minimum 1) - #[serde(default = "default_page")] - pub page: u64, - - /// Number of items per page (max 100, default 50) - #[serde(default = "default_page_size")] - pub page_size: u64, -} - -impl Default for PaginationParams { - fn default() -> Self { - Self { - page: DEFAULT_PAGE, - page_size: DEFAULT_PAGE_SIZE, - } - } -} - -#[allow(dead_code)] // Public API - used for pagination in list endpoints -impl PaginationParams { - /// Validate and clamp pagination parameters - /// - If page is 0, treats it as page 1 (backward compatibility) - /// - Clamps page_size to max_page_size - pub fn validate(mut self, max_page_size: u64) -> Self { - // Treat page 0 as page 1 for backward compatibility - if self.page == 0 { - self.page = 1; - } - if self.page_size == 0 { - self.page_size = DEFAULT_PAGE_SIZE; - } - if self.page_size > max_page_size { - self.page_size = max_page_size; - } - self - } - - /// Calculate offset for database queries (converts 1-indexed page to 0-indexed offset) - pub fn offset(&self) -> u64 { - self.page.saturating_sub(1) * self.page_size - } - - /// Get limit for database queries - pub fn limit(&self) -> u64 { - self.page_size - } -} - // ============================================================================= // List Pagination Parameters (for POST endpoints with query params) // ============================================================================= @@ -132,10 +78,20 @@ pub struct ListPaginationParams { /// Return full data including metadata, locks, and related entities. /// Default is false for backward compatibility. #[serde(default)] + /// **Deprecated.** Prefer `GET /books/{book_id}/full` and + /// `GET /series/{series_id}/full`. A response whose schema depends on a + /// query parameter cannot be expressed in OpenAPI, so this form is + /// invisible to a generated client. Scheduled for removal in 3.0. + #[deprecated( + since = "2.2.0", + note = "use GET /books/{book_id}/full or GET /series/{series_id}/full; \ +a response shape that depends on a query parameter cannot be described in OpenAPI" + )] pub full: bool, } impl Default for ListPaginationParams { + #[allow(deprecated)] // still constructs the deprecated `full` parameter fn default() -> Self { Self { page: DEFAULT_PAGE, @@ -678,68 +634,6 @@ impl CursorPaginatedResponse { mod tests { use super::*; - #[test] - fn test_pagination_params_defaults() { - let params = PaginationParams::default(); - assert_eq!(params.page, 1); - assert_eq!(params.page_size, 50); - } - - #[test] - fn test_pagination_params_offset_calculation() { - // Page 1 should have offset 0 - let params = PaginationParams { - page: 1, - page_size: 50, - }; - assert_eq!(params.offset(), 0); - - // Page 2 should have offset 50 - let params = PaginationParams { - page: 2, - page_size: 50, - }; - assert_eq!(params.offset(), 50); - - // Page 3 with page_size 20 should have offset 40 - let params = PaginationParams { - page: 3, - page_size: 20, - }; - assert_eq!(params.offset(), 40); - } - - #[test] - fn test_pagination_params_validate_page_zero() { - // Page 0 should be treated as page 1 - let params = PaginationParams { - page: 0, - page_size: 50, - }; - let validated = params.validate(100); - assert_eq!(validated.page, 1); - } - - #[test] - fn test_pagination_params_validate_page_size_zero() { - let params = PaginationParams { - page: 1, - page_size: 0, - }; - let validated = params.validate(100); - assert_eq!(validated.page_size, DEFAULT_PAGE_SIZE); - } - - #[test] - fn test_pagination_params_validate_max_page_size() { - let params = PaginationParams { - page: 1, - page_size: 500, - }; - let validated = params.validate(100); - assert_eq!(validated.page_size, 100); - } - #[test] fn test_pagination_link_builder_first_page() { let builder = PaginationLinkBuilder::new("/api/v1/books", 1, 50, 10); diff --git a/crates/codex-api/src/routes/v1/dto/filter.rs b/crates/codex-api/src/routes/v1/dto/filter.rs index c87e4424..19cfccc4 100644 --- a/crates/codex-api/src/routes/v1/dto/filter.rs +++ b/crates/codex-api/src/routes/v1/dto/filter.rs @@ -22,7 +22,6 @@ pub use codex_models::filter::{ pub struct SeriesListRequest { /// Filter condition (optional - no condition returns all) #[serde(default, skip_serializing_if = "Option::is_none")] - #[schema(value_type = Option)] pub condition: Option, /// Full-text search query (case-insensitive search on series name) @@ -39,7 +38,6 @@ pub struct SeriesListRequest { pub struct BookListRequest { /// Filter condition (optional - no condition returns all) #[serde(default, skip_serializing_if = "Option::is_none")] - #[schema(value_type = Option)] pub condition: Option, /// Full-text search query (case-insensitive search on book title) diff --git a/crates/codex-api/src/routes/v1/dto/series.rs b/crates/codex-api/src/routes/v1/dto/series.rs index 15f93192..b7d61aef 100644 --- a/crates/codex-api/src/routes/v1/dto/series.rs +++ b/crates/codex-api/src/routes/v1/dto/series.rs @@ -189,6 +189,15 @@ pub struct SearchSeriesRequest { /// Return full series data including metadata, locks, genres, tags, etc. #[serde(default)] + /// **Deprecated.** Prefer `GET /books/{book_id}/full` and + /// `GET /series/{series_id}/full`. A response whose schema depends on a + /// query parameter cannot be expressed in OpenAPI, so this form is + /// invisible to a generated client. Scheduled for removal in 3.0. + #[deprecated( + since = "2.2.0", + note = "use GET /books/{book_id}/full or GET /series/{series_id}/full; \ +a response shape that depends on a query parameter cannot be described in OpenAPI" + )] pub full: bool, } diff --git a/crates/codex-api/src/routes/v1/handlers/books.rs b/crates/codex-api/src/routes/v1/handlers/books.rs index 1cec458b..a1dc71a7 100644 --- a/crates/codex-api/src/routes/v1/handlers/books.rs +++ b/crates/codex-api/src/routes/v1/handlers/books.rs @@ -1,13 +1,13 @@ use super::super::dto::{ AdjacentBooksResponse, BookDetailResponse, BookDto, BookFullMetadata, BookListRequest, BookListResponse, BookMetadataDto, BookMetadataLocks, FullBookListResponse, FullBookResponse, - PaginationParams, book::{ AddBookGenreRequest, AddBookTagRequest, BookAuthorDto, BookAwardDto, BookSortParam, BookType, BookTypeDto, SetBookGenresRequest, SetBookTagsRequest, }, common::{ - DEFAULT_PAGE, DEFAULT_PAGE_SIZE, ListPaginationParams, MAX_PAGE_SIZE, PaginationLinkBuilder, + DEFAULT_PAGE, DEFAULT_PAGE_SIZE, ListPaginationParams, MAX_PAGE_SIZE, PaginatedResponse, + PaginationLinkBuilder, }, page::PageDto, series::{GenreDto, GenreListResponse, TagDto, TagListResponse}, @@ -162,6 +162,59 @@ pub struct BookListQuery { /// Return full data including metadata and locks. /// Default is false for backward compatibility. #[serde(default)] + /// **Deprecated.** Prefer `GET /books/{book_id}/full` and + /// `GET /series/{series_id}/full`. A response whose schema depends on a + /// query parameter cannot be expressed in OpenAPI, so this form is + /// invisible to a generated client. Scheduled for removal in 3.0. + #[deprecated( + since = "2.2.0", + note = "use GET /books/{book_id}/full or GET /series/{series_id}/full; \ +a response shape that depends on a query parameter cannot be described in OpenAPI" + )] + pub full: bool, +} + +/// Query parameters for the library-scoped book listings. +/// +/// These routes carry the library in the path, so they deliberately do not +/// advertise `libraryId` or `seriesId`: the handlers extract the wider +/// `BookListQuery` and ignore both. Documenting this narrower set describes +/// exactly the parameters these four routes honour. +/// +/// `parameter_in = Query` is mandatory here and not merely tidy. utoipa infers a +/// parameter's location from the handler's extractor, and this struct is named in +/// the annotation without ever being extracted, so there is nothing to infer from +/// and the derive falls back to `Path`. That fallback is what documented `page` +/// and `pageSize` as path parameters of a path with no such segments, producing +/// an operation no client could construct. +#[derive(Debug, Deserialize, utoipa::IntoParams)] +#[serde(rename_all = "camelCase")] +#[into_params(rename_all = "camelCase", parameter_in = Query)] +pub struct LibraryBookListQuery { + /// Page number (1-indexed, minimum 1) + #[serde(default = "default_page")] + pub page: u64, + + /// Number of items per page (max 100, default 50) + #[serde(default = "default_page_size")] + pub page_size: u64, + + /// Sort parameter (format: "field,direction" e.g. "title,asc") + #[serde(default)] + pub sort: Option, + + /// Return full data including metadata and locks. + /// Default is false for backward compatibility. + #[serde(default)] + /// **Deprecated.** Prefer `GET /books/{book_id}/full` and + /// `GET /series/{series_id}/full`. A response whose schema depends on a + /// query parameter cannot be expressed in OpenAPI, so this form is + /// invisible to a generated client. Scheduled for removal in 3.0. + #[deprecated( + since = "2.2.0", + note = "use GET /books/{book_id}/full or GET /series/{series_id}/full; \ +a response shape that depends on a query parameter cannot be described in OpenAPI" + )] pub full: bool, } @@ -173,6 +226,15 @@ pub struct BookGetQuery { /// Return full data including metadata and locks. /// Default is false for backward compatibility. #[serde(default)] + /// **Deprecated.** Prefer `GET /books/{book_id}/full` and + /// `GET /series/{series_id}/full`. A response whose schema depends on a + /// query parameter cannot be expressed in OpenAPI, so this form is + /// invisible to a generated client. Scheduled for removal in 3.0. + #[deprecated( + since = "2.2.0", + note = "use GET /books/{book_id}/full or GET /series/{series_id}/full; \ +a response shape that depends on a query parameter cannot be described in OpenAPI" + )] pub full: bool, } @@ -763,7 +825,7 @@ pub async fn books_to_full_dtos_batched( path = "/api/v1/books", params(BookListQuery), responses( - (status = 200, description = "Paginated list of books (returns FullBookListResponse when full=true)", body = BookListResponse), + (status = 200, description = "Paginated list of books (returns FullBookListResponse when full=true)", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -772,6 +834,7 @@ pub async fn books_to_full_dtos_batched( ), tag = "Books" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_books( State(state): State>, auth: AuthContext, @@ -883,7 +946,7 @@ pub async fn list_books( params(ListPaginationParams), request_body = BookListRequest, responses( - (status = 200, description = "Paginated list of filtered books (returns FullBookListResponse when full=true)", body = BookListResponse), + (status = 200, description = "Paginated list of filtered books (returns FullBookListResponse when full=true)", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -892,6 +955,7 @@ pub async fn list_books( ), tag = "Books" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_books_filtered( State(state): State>, auth: AuthContext, @@ -1129,6 +1193,62 @@ pub async fn list_books_filtered( } } +/// Get a book with its metadata, genres and tags in one response +/// +/// The same book `GET /api/v1/books/{book_id}` returns, plus the related data a +/// detail screen needs, so it does not have to fan out into separate metadata, +/// genre and tag requests. +/// +/// This exists as its own route because the shape is genuinely different, not +/// merely richer: it carries `metadata`, `genres`, `tags`, `readCount` and +/// `lastCompletedAt`, and it moves `chapter`, `summary` and `volume` inside +/// `metadata`. A response whose schema depends on a query parameter cannot be +/// expressed in OpenAPI, so the deprecated `?full=true` form is undescribable +/// and unusable from a generated client. This route is describable. +#[utoipa::path( + get, + path = "/api/v1/books/{book_id}/full", + params( + ("book_id" = Uuid, Path, description = "Book ID") + ), + responses( + (status = 200, description = "Book with its related data", body = FullBookResponse), + (status = 403, description = "Forbidden"), + (status = 404, description = "Book not found"), + ), + security( + ("jwt_bearer" = []), + ("api_key" = []) + ), + tag = "Books" +)] +pub async fn get_book_full( + State(state): State>, + auth: AuthContext, + Path(book_id): Path, +) -> Result, ApiError> { + require_permission!(auth, Permission::BooksRead)?; + + let book = BookRepository::get_by_id(&state.db, book_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to fetch book: {}", e)))? + .ok_or_else(|| ApiError::NotFound("Book not found".to_string()))?; + + let content_filter = ContentFilter::for_user(&state.db, auth.user_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to load content filter: {}", e)))?; + + if !content_filter.is_book_visible(book.series_id) { + return Err(ApiError::NotFound("Book not found".to_string())); + } + + let mut full_dtos = books_to_full_dtos_batched(&state.db, auth.user_id, vec![book]).await?; + let full_book = full_dtos + .pop() + .ok_or_else(|| ApiError::Internal("Failed to build full book DTO".to_string()))?; + Ok(Json(full_book)) +} + /// Get book by ID #[utoipa::path( get, @@ -1147,6 +1267,7 @@ pub async fn list_books_filtered( ), tag = "Books" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn get_book( State(state): State>, auth: AuthContext, @@ -1532,10 +1653,10 @@ pub async fn get_adjacent_books( path = "/api/v1/libraries/{library_id}/books", params( ("library_id" = Uuid, Path, description = "Library ID"), - PaginationParams, + LibraryBookListQuery, ), responses( - (status = 200, description = "Paginated list of books in library", body = BookListResponse), + (status = 200, description = "Paginated list of books in library", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -1544,6 +1665,7 @@ pub async fn get_adjacent_books( ), tag = "Books" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_library_books( State(state): State>, auth: AuthContext, @@ -1621,7 +1743,7 @@ pub async fn list_library_books( path = "/api/v1/books/in-progress", params(BookListQuery), responses( - (status = 200, description = "Paginated list of in-progress books", body = BookListResponse), + (status = 200, description = "Paginated list of in-progress books", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -1630,6 +1752,7 @@ pub async fn list_library_books( ), tag = "Books" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_in_progress_books( State(state): State>, auth: AuthContext, @@ -1698,10 +1821,10 @@ pub async fn list_in_progress_books( path = "/api/v1/libraries/{library_id}/books/in-progress", params( ("library_id" = Uuid, Path, description = "Library ID"), - PaginationParams, + LibraryBookListQuery, ), responses( - (status = 200, description = "Paginated list of in-progress books in library", body = BookListResponse), + (status = 200, description = "Paginated list of in-progress books in library", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -1771,7 +1894,7 @@ pub async fn list_library_in_progress_books( path = "/api/v1/books/on-deck", params(BookListQuery), responses( - (status = 200, description = "Paginated list of on-deck books", body = BookListResponse), + (status = 200, description = "Paginated list of on-deck books", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -1838,10 +1961,10 @@ pub async fn list_on_deck_books( path = "/api/v1/libraries/{library_id}/books/on-deck", params( ("library_id" = Uuid, Path, description = "Library ID"), - PaginationParams, + LibraryBookListQuery, ), responses( - (status = 200, description = "Paginated list of on-deck books in library", body = BookListResponse), + (status = 200, description = "Paginated list of on-deck books in library", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -1910,7 +2033,7 @@ pub async fn list_library_on_deck_books( path = "/api/v1/books/recently-added", params(BookListQuery), responses( - (status = 200, description = "Paginated list of recently added books", body = BookListResponse), + (status = 200, description = "Paginated list of recently added books", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -1919,6 +2042,7 @@ pub async fn list_library_on_deck_books( ), tag = "Books" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_recently_added_books( State(state): State>, auth: AuthContext, @@ -1986,10 +2110,10 @@ pub async fn list_recently_added_books( path = "/api/v1/libraries/{library_id}/books/recently-added", params( ("library_id" = Uuid, Path, description = "Library ID"), - PaginationParams, + LibraryBookListQuery, ), responses( - (status = 200, description = "Paginated list of recently added books in library", body = BookListResponse), + (status = 200, description = "Paginated list of recently added books in library", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -2162,16 +2286,50 @@ pub async fn list_library_recently_read_books( /// /// Streams the original book file (CBZ, CBR, EPUB, PDF) for download. /// Used by OPDS clients for acquisition links. +/// +/// Range-capable and conditional. `Accept-Ranges: bytes` and a strong `ETag` +/// go out on every response, so a download interrupted partway can resume with +/// `Range` rather than starting again, and a client that understands the +/// container can read part of an archive without fetching all of it — the +/// suffix form `bytes=-65536` reaches a ZIP central directory directly. #[utoipa::path( get, path = "/api/v1/books/{book_id}/file", params( - ("book_id" = Uuid, Path, description = "Book ID") + ("book_id" = Uuid, Path, description = "Book ID"), + // Declared so a generated client can construct a resumable download + // without dropping to a raw request. + ("Range" = Option, Header, + description = "Byte range, e.g. `bytes=0-1023`, `bytes=1024-` or `bytes=-65536`. \ + A single range only; multiple ranges are answered with the whole file."), + ("If-Range" = Option, Header, + description = "Serve the range only if this ETag still matches; otherwise the whole \ + file is returned, so a resume cannot splice bytes from two versions."), + ("If-None-Match" = Option, Header, + description = "Return 304 if this ETag still matches."), ), responses( - (status = 200, description = "Book file", content_type = "application/octet-stream"), + // One entry per arm of the `content_type` match below, including the + // catch-all. Declaring a single type here would be a lie a strict + // generated client turns into a thrown response rather than a decode. + (status = 200, description = "Book file", content( + ("application/zip"), + ("application/x-rar-compressed"), + ("application/epub+zip"), + ("application/pdf"), + ("application/octet-stream"), + )), + (status = 206, description = "The requested byte range", content( + ("application/zip"), + ("application/x-rar-compressed"), + ("application/epub+zip"), + ("application/pdf"), + ("application/octet-stream"), + )), + (status = 304, description = "Not modified (client cache is valid)"), (status = 404, description = "Book not found"), (status = 403, description = "Forbidden"), + (status = 416, description = "The requested range names no byte of the file"), ), security( ("jwt_bearer" = []), @@ -2182,6 +2340,7 @@ pub async fn list_library_recently_read_books( pub async fn get_book_file( State(state): State>, FlexibleAuthContext(auth): FlexibleAuthContext, + headers: axum::http::HeaderMap, Path(book_id): Path, ) -> Result { require_permission!(auth, Permission::BooksRead)?; @@ -2192,7 +2351,8 @@ pub async fn get_book_file( .map_err(|e| ApiError::Internal(format!("Failed to fetch book: {}", e)))? .ok_or_else(|| ApiError::NotFound("Book not found".to_string()))?; - // Check sharing tag access for the book's series + // Check sharing tag access for the book's series. This runs before the file + // is touched, so a 206 is never a way around a check a 200 has to pass. let content_filter = ContentFilter::for_user(&state.db, auth.user_id) .await .map_err(|e| ApiError::Internal(format!("Failed to load content filter: {}", e)))?; @@ -2223,26 +2383,22 @@ pub async fn get_book_file( _ => "application/octet-stream", }; - // Open file for streaming - let file = tokio::fs::File::open(&book.path) - .await - .map_err(|e| ApiError::Internal(format!("Failed to open book file: {}", e)))?; - - // Create a stream from the file - let stream = ReaderStream::new(file); - let body = Body::from_stream(stream); - - // Build response with appropriate headers - Ok(Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, content_type) - .header(header::CONTENT_LENGTH, metadata.len()) - .header( - header::CONTENT_DISPOSITION, - format!("attachment; filename=\"{}\"", book.file_name), - ) - .body(body) - .unwrap()) + // `file_hash` is a non-null column the scanner already computes, so the + // validator costs no I/O, survives a rescan, and survives the file being + // moved on disk. An mtime validator would invalidate every client's cache + // on any rescan that touches timestamps. + let etag = format!("\"{}\"", book.file_hash); + + crate::ranged_file::ranged_file_response( + &headers, + path, + metadata.len(), + &etag, + book.modified_at, + content_type, + &crate::ranged_file::content_disposition_attachment(&book.file_name), + ) + .await } // ============================================================================ diff --git a/crates/codex-api/src/routes/v1/handlers/library_jobs.rs b/crates/codex-api/src/routes/v1/handlers/library_jobs.rs index 3241f0e7..294fc64a 100644 --- a/crates/codex-api/src/routes/v1/handlers/library_jobs.rs +++ b/crates/codex-api/src/routes/v1/handlers/library_jobs.rs @@ -108,6 +108,28 @@ async fn validate_and_normalise_create( // CRUD // ============================================================================= +/// List the scheduled jobs configured for a library +/// +/// Jobs are per-library scheduled work; today the only kind is a metadata +/// refresh. The schedule is a cron expression evaluated in the job's timezone, +/// or the server's if it declares none. +#[utoipa::path( + get, + path = "/api/v1/libraries/{library_id}/jobs", + params( + ("library_id" = Uuid, Path, description = "Library ID"), + ), + responses( + (status = 200, description = "The library's jobs", body = ListLibraryJobsResponse), + (status = 403, description = "Forbidden"), + (status = 404, description = "Library not found"), + ), + security( + ("jwt_bearer" = []), + ("api_key" = []) + ), + tag = "Library Jobs" +)] pub async fn list_jobs( State(state): State>, auth: AuthContext, @@ -125,6 +147,25 @@ pub async fn list_jobs( Ok(Json(ListLibraryJobsResponse { jobs })) } +/// Get one scheduled job +#[utoipa::path( + get, + path = "/api/v1/libraries/{library_id}/jobs/{job_id}", + params( + ("library_id" = Uuid, Path, description = "Library ID"), + ("job_id" = Uuid, Path, description = "Job ID"), + ), + responses( + (status = 200, description = "The job", body = LibraryJobDto), + (status = 403, description = "Forbidden"), + (status = 404, description = "Job not found, or it belongs to another library"), + ), + security( + ("jwt_bearer" = []), + ("api_key" = []) + ), + tag = "Library Jobs" +)] pub async fn get_job( State(state): State>, auth: AuthContext, @@ -141,6 +182,30 @@ pub async fn get_job( Ok(Json(row_to_dto(row)?)) } +/// Create a scheduled job for a library +/// +/// The job's type is taken from the `config` variant rather than named +/// separately, so the two cannot disagree. A blank `name` is generated from the +/// config. +#[utoipa::path( + post, + path = "/api/v1/libraries/{library_id}/jobs", + params( + ("library_id" = Uuid, Path, description = "Library ID"), + ), + request_body = CreateLibraryJobRequest, + responses( + (status = 201, description = "The created job", body = LibraryJobDto), + (status = 400, description = "Invalid cron expression, timezone, or config"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Library not found"), + ), + security( + ("jwt_bearer" = []), + ("api_key" = []) + ), + tag = "Library Jobs" +)] pub async fn create_job( State(state): State>, auth: AuthContext, @@ -201,6 +266,31 @@ pub async fn create_job( Ok((StatusCode::CREATED, Json(row_to_dto(row)?))) } +/// Update a scheduled job +/// +/// Every field is optional. `timezone` is tri-state: absent leaves it, `null` +/// clears it back to the server default, and a value sets it. A `config` whose +/// type differs from the job's is rejected rather than migrating the job. +#[utoipa::path( + patch, + path = "/api/v1/libraries/{library_id}/jobs/{job_id}", + params( + ("library_id" = Uuid, Path, description = "Library ID"), + ("job_id" = Uuid, Path, description = "Job ID"), + ), + request_body = PatchLibraryJobRequest, + responses( + (status = 200, description = "The updated job", body = LibraryJobDto), + (status = 400, description = "Empty name, invalid schedule, or a config of the wrong type"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Job not found, or it belongs to another library"), + ), + security( + ("jwt_bearer" = []), + ("api_key" = []) + ), + tag = "Library Jobs" +)] pub async fn patch_job( State(state): State>, auth: AuthContext, @@ -279,6 +369,25 @@ pub async fn patch_job( Ok(Json(row_to_dto(updated)?)) } +/// Delete a scheduled job +#[utoipa::path( + delete, + path = "/api/v1/libraries/{library_id}/jobs/{job_id}", + params( + ("library_id" = Uuid, Path, description = "Library ID"), + ("job_id" = Uuid, Path, description = "Job ID"), + ), + responses( + (status = 204, description = "Job deleted"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Job not found, or it belongs to another library"), + ), + security( + ("jwt_bearer" = []), + ("api_key" = []) + ), + tag = "Library Jobs" +)] pub async fn delete_job( State(state): State>, auth: AuthContext, @@ -309,6 +418,31 @@ pub async fn delete_job( // Run-now // ============================================================================= +/// Run a scheduled job immediately +/// +/// Enqueues the job's task and returns its id; the run itself is asynchronous +/// and its progress is followed through the task queue. Refuses if a run for +/// this job is already in flight, so a double click cannot queue the work +/// twice. +#[utoipa::path( + post, + path = "/api/v1/libraries/{library_id}/jobs/{job_id}/run-now", + params( + ("library_id" = Uuid, Path, description = "Library ID"), + ("job_id" = Uuid, Path, description = "Job ID"), + ), + responses( + (status = 200, description = "The enqueued task", body = RunNowResponse), + (status = 403, description = "Forbidden"), + (status = 404, description = "Job not found, or it belongs to another library"), + (status = 409, description = "A run for this job is already in flight"), + ), + security( + ("jwt_bearer" = []), + ("api_key" = []) + ), + tag = "Library Jobs" +)] pub async fn run_job_now( State(state): State>, auth: AuthContext, @@ -350,6 +484,32 @@ pub async fn run_job_now( const DRY_RUN_DEFAULT_SAMPLE: u32 = 5; const DRY_RUN_MAX_SAMPLE: u32 = 20; +/// Preview what a job would change, without changing anything +/// +/// Plans the refresh and returns a sample of the per-series field changes it +/// would make. Nothing is written. `configOverride` plans against a config the +/// job does not have yet, which is what lets an editor preview an edit before +/// saving it; it must be of the job's own type. +#[utoipa::path( + post, + path = "/api/v1/libraries/{library_id}/jobs/{job_id}/dry-run", + params( + ("library_id" = Uuid, Path, description = "Library ID"), + ("job_id" = Uuid, Path, description = "Job ID"), + ), + request_body = DryRunRequest, + responses( + (status = 200, description = "The planned changes", body = DryRunResponse), + (status = 400, description = "Override config is of the wrong type, or is invalid"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Job not found, or it belongs to another library"), + ), + security( + ("jwt_bearer" = []), + ("api_key" = []) + ), + tag = "Library Jobs" +)] pub async fn dry_run_job( State(state): State>, auth: AuthContext, @@ -470,6 +630,24 @@ pub async fn dry_run_job( // Field-groups catalog // ============================================================================= +/// List the metadata field groups a refresh job can target +/// +/// A static catalog: the groups and the concrete metadata fields each one +/// covers. It is what a job editor offers as refreshable fields, so a client +/// does not have to hardcode the list. +#[utoipa::path( + get, + path = "/api/v1/library-jobs/metadata-refresh/field-groups", + responses( + (status = 200, description = "The field-group catalog", body = Vec), + (status = 403, description = "Forbidden"), + ), + security( + ("jwt_bearer" = []), + ("api_key" = []) + ), + tag = "Library Jobs" +)] pub async fn list_field_groups(auth: AuthContext) -> Result>, ApiError> { require_permission!(auth, Permission::LibrariesRead)?; let mut out = Vec::with_capacity(FieldGroup::all().len()); diff --git a/crates/codex-api/src/routes/v1/handlers/pages.rs b/crates/codex-api/src/routes/v1/handlers/pages.rs index 55e851a8..7b105e9a 100644 --- a/crates/codex-api/src/routes/v1/handlers/pages.rs +++ b/crates/codex-api/src/routes/v1/handlers/pages.rs @@ -54,7 +54,10 @@ const PLACEHOLDER_SVG: &[u8] = include_bytes!("../../../../../../assets/placehol ("width" = Option, Query, description = "Downscale CBZ/CBR pages to at most this width (px); other formats ignore it") ), responses( - (status = 200, description = "Page image", content_type = "image/jpeg"), + // Whatever `detect_content_type` finds in the archive: jpeg, png, webp, + // gif, bmp or avif. A caller cannot act on the difference, so the + // wildcard describes it exactly and renders as a single binary body. + (status = 200, description = "Page image", content_type = "image/*"), (status = 304, description = "Not modified (client cache is valid)"), (status = 404, description = "Book or page not found"), (status = 403, description = "Forbidden"), @@ -424,7 +427,11 @@ async fn serve_pdf_page_with_streaming( ("book_id" = Uuid, Path, description = "Book ID"), ), responses( - (status = 200, description = "Thumbnail image", content_type = "image/jpeg"), + // `image/jpeg` once generated, `image/svg+xml` for the placeholder that + // is served until the generation task finishes. That difference only + // encodes "not ready yet", which is not a rendering concern, so the + // wildcard spares every caller a switch on it. + (status = 200, description = "Thumbnail image", content_type = "image/*"), (status = 304, description = "Not modified (client cache is valid)"), (status = 404, description = "Book not found"), (status = 403, description = "Forbidden"), diff --git a/crates/codex-api/src/routes/v1/handlers/series.rs b/crates/codex-api/src/routes/v1/handlers/series.rs index 25e6d7f3..b45387fc 100644 --- a/crates/codex-api/src/routes/v1/handlers/series.rs +++ b/crates/codex-api/src/routes/v1/handlers/series.rs @@ -78,6 +78,15 @@ pub struct ListBooksQuery { /// Return full data including metadata and locks. /// Default is false for backward compatibility. #[serde(default)] + /// **Deprecated.** Prefer `GET /books/{book_id}/full` and + /// `GET /series/{series_id}/full`. A response whose schema depends on a + /// query parameter cannot be expressed in OpenAPI, so this form is + /// invisible to a generated client. Scheduled for removal in 3.0. + #[deprecated( + since = "2.2.0", + note = "use GET /books/{book_id}/full or GET /series/{series_id}/full; \ +a response shape that depends on a query parameter cannot be described in OpenAPI" + )] pub full: bool, } @@ -89,6 +98,40 @@ fn default_page_size() -> u64 { DEFAULT_PAGE_SIZE } +/// Query parameters for `GET /api/v1/series/full`. +/// +/// The same filters and pagination as the plain listing, minus `full` itself: +/// the route already says so, and repeating it as a parameter would reintroduce +/// the response-shape-depends-on-a-parameter problem this route exists to avoid. +#[derive(Debug, Deserialize, utoipa::IntoParams)] +#[serde(rename_all = "camelCase")] +#[into_params(rename_all = "camelCase")] +pub struct SeriesFullListQuery { + /// Page number (1-indexed, default 1) + #[serde(default = "default_page")] + pub page: u64, + + /// Number of items per page (max 100, default 50) + #[serde(default = "default_page_size")] + pub page_size: u64, + + /// Sort parameter (format: "field,direction" e.g. "name,asc") + #[serde(default)] + pub sort: Option, + + /// Filter by genres (comma-separated, AND logic - series must have ALL specified genres) + #[serde(default)] + pub genres: Option, + + /// Filter by tags (comma-separated, AND logic - series must have ALL specified tags) + #[serde(default)] + pub tags: Option, + + /// Filter by library ID + #[serde(default)] + pub library_id: Option, +} + /// Query parameters for listing series #[derive(Debug, Deserialize, utoipa::IntoParams)] #[serde(rename_all = "camelCase")] @@ -121,6 +164,15 @@ pub struct SeriesListQuery { /// Return full series data including metadata, locks, genres, tags, alternate titles, /// external ratings, and external links. Default is false for backward compatibility. #[serde(default)] + /// **Deprecated.** Prefer `GET /books/{book_id}/full` and + /// `GET /series/{series_id}/full`. A response whose schema depends on a + /// query parameter cannot be expressed in OpenAPI, so this form is + /// invisible to a generated client. Scheduled for removal in 3.0. + #[deprecated( + since = "2.2.0", + note = "use GET /books/{book_id}/full or GET /series/{series_id}/full; \ +a response shape that depends on a query parameter cannot be described in OpenAPI" + )] pub full: bool, } @@ -813,13 +865,56 @@ async fn series_to_full_dtos_batched( Ok(results) } +/// List series with their metadata, genres, tags and external links +/// +/// The same page `GET /api/v1/series` returns, with each entry carrying the +/// related data inline, so a caller that needs metadata for a whole library +/// pages it once instead of following up per series. +/// +/// This exists as its own route because the shape is genuinely different, not +/// merely richer, and because a response whose schema depends on a query +/// parameter cannot be expressed in OpenAPI — which made the deprecated +/// `GET /api/v1/series?full=true` invisible to every generated client. Accepts +/// the same filters and pagination as the plain listing. +#[utoipa::path( + get, + path = "/api/v1/series/full", + params(SeriesFullListQuery), + responses( + (status = 200, description = "Paginated series with their related data", body = PaginatedResponse), + (status = 403, description = "Forbidden"), + ), + security( + ("jwt_bearer" = []), + ("api_key" = []) + ), + tag = "Series" +)] +#[allow(deprecated)] // sets the deprecated `full` flag on the shared implementation +pub async fn list_series_full( + State(state): State>, + auth: AuthContext, + Query(query): Query, +) -> Result { + let query = SeriesListQuery { + page: query.page, + page_size: query.page_size, + library_id: query.library_id, + genres: query.genres, + tags: query.tags, + sort: query.sort, + full: true, + }; + list_series_impl(state, auth, query, "/api/v1/series/full", false).await +} + /// List series with optional library filter and pagination #[utoipa::path( get, path = "/api/v1/series", params(SeriesListQuery), responses( - (status = 200, description = "Paginated list of series (returns FullSeriesListResponse when full=true)", body = SeriesListResponse), + (status = 200, description = "Paginated list of series (returns FullSeriesListResponse when full=true)", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -832,6 +927,17 @@ pub async fn list_series( State(state): State>, auth: AuthContext, Query(query): Query, +) -> Result { + list_series_impl(state, auth, query, "/api/v1/series", true).await +} + +#[allow(deprecated)] // still serves the deprecated `full` parameter +async fn list_series_impl( + state: Arc, + auth: AuthContext, + query: SeriesListQuery, + base_path: &str, + link_full_param: bool, ) -> Result { require_permission!(auth, Permission::SeriesRead)?; @@ -927,8 +1033,7 @@ pub async fn list_series( } else { total.div_ceil(page_size) }; - let mut link_builder = - PaginationLinkBuilder::new("/api/v1/series", page, page_size, total_pages); + let mut link_builder = PaginationLinkBuilder::new(base_path, page, page_size, total_pages); if let Some(library_id) = query.library_id { link_builder = link_builder.with_param("library_id", &library_id.to_string()); } @@ -941,7 +1046,9 @@ pub async fn list_series( if let Some(ref sort_str) = query.sort { link_builder = link_builder.with_param("sort", sort_str); } - if query.full { + // The dedicated route already means "full"; only the legacy query-parameter + // form needs it echoed back in the pagination links. + if link_full_param && query.full { link_builder = link_builder.with_param("full", "true"); } @@ -982,7 +1089,7 @@ pub async fn list_series( path = "/api/v1/series/external-index", params(ListPaginationParams), responses( - (status = 200, description = "Paginated slim per-series external-index entries", body = SeriesExternalIndexListResponse), + (status = 200, description = "Paginated slim per-series external-index entries", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -1085,9 +1192,77 @@ pub async fn list_series_external_index( pub struct SeriesGetQuery { /// Return full series data including metadata, locks, genres, tags, etc. #[serde(default)] + /// **Deprecated.** Prefer `GET /books/{book_id}/full` and + /// `GET /series/{series_id}/full`. A response whose schema depends on a + /// query parameter cannot be expressed in OpenAPI, so this form is + /// invisible to a generated client. Scheduled for removal in 3.0. + #[deprecated( + since = "2.2.0", + note = "use GET /books/{book_id}/full or GET /series/{series_id}/full; \ +a response shape that depends on a query parameter cannot be described in OpenAPI" + )] pub full: bool, } +/// Get a series with its metadata, genres, tags and external links in one response +/// +/// The same series `GET /api/v1/series/{series_id}` returns, plus the related +/// data a detail screen needs, so it does not have to fan out into separate +/// metadata, genre, tag, external-id, external-link and rating requests. +/// +/// This exists as its own route because the shape is genuinely different, not +/// merely richer: it carries `metadata`, `genres`, `tags`, `alternateTitles`, +/// `externalIds`, `externalLinks`, `externalRatings`, `readCount` and +/// `lastCompletedAt`, and it moves `title`, `titleSort`, `publisher`, `summary` +/// and `year` inside `metadata`. A response whose schema depends on a query +/// parameter cannot be expressed in OpenAPI, so the deprecated `?full=true` +/// form is undescribable and unusable from a generated client. This route is +/// describable. +#[utoipa::path( + get, + path = "/api/v1/series/{series_id}/full", + params( + ("series_id" = Uuid, Path, description = "Series ID") + ), + responses( + (status = 200, description = "Series with its related data", body = FullSeriesResponse), + (status = 403, description = "Forbidden"), + (status = 404, description = "Series not found"), + ), + security( + ("jwt_bearer" = []), + ("api_key" = []) + ), + tag = "Series" +)] +pub async fn get_series_full( + State(state): State>, + auth: AuthContext, + Path(series_id): Path, +) -> Result, ApiError> { + require_permission!(auth, Permission::SeriesRead)?; + + let series = SeriesRepository::get_by_id(&state.db, series_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to fetch series: {}", e)))? + .ok_or_else(|| ApiError::NotFound("Series not found".to_string()))?; + + let content_filter = ContentFilter::for_user(&state.db, auth.user_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to load content filter: {}", e)))?; + + if !content_filter.is_series_visible(series_id) { + return Err(ApiError::NotFound("Series not found".to_string())); + } + + let full_dto = series_to_full_dtos_batched(&state.db, vec![series], Some(auth.user_id)) + .await? + .into_iter() + .next() + .ok_or_else(|| ApiError::Internal("Failed to build full series DTO".to_string()))?; + Ok(Json(full_dto)) +} + /// Get series by ID #[utoipa::path( get, @@ -1106,6 +1281,7 @@ pub struct SeriesGetQuery { ), tag = "Series" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn get_series( State(state): State>, auth: AuthContext, @@ -1302,6 +1478,7 @@ pub async fn patch_series( ), tag = "Series" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn search_series( State(state): State>, auth: AuthContext, @@ -1389,7 +1566,7 @@ pub async fn search_series( params(ListPaginationParams), request_body = SeriesListRequest, responses( - (status = 200, description = "Paginated list of filtered series (returns FullSeriesListResponse when full=true)", body = SeriesListResponse), + (status = 200, description = "Paginated list of filtered series (returns FullSeriesListResponse when full=true)", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -1398,6 +1575,7 @@ pub async fn search_series( ), tag = "Series" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_series_filtered( State(state): State>, auth: AuthContext, @@ -1736,6 +1914,7 @@ pub async fn list_series_alphabetical_groups( ), tag = "Series" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn get_series_books( State(state): State>, auth: AuthContext, @@ -2030,7 +2209,10 @@ pub async fn set_series_cover_source( ("series_id" = Uuid, Path, description = "Series ID"), ), responses( - (status = 200, description = "Thumbnail image", content_type = "image/jpeg"), + // `image/jpeg` once generated, `image/svg+xml` for the placeholder this + // route returns while it queues the generation task. Every cover in a + // freshly scanned library takes the second branch. + (status = 200, description = "Thumbnail image", content_type = "image/*"), (status = 304, description = "Not modified (client cache is valid)"), (status = 404, description = "Series not found"), (status = 403, description = "Forbidden"), @@ -2241,6 +2423,15 @@ pub struct InProgressSeriesQuery { /// Return full series data including metadata, locks, genres, tags, etc. #[serde(default)] + /// **Deprecated.** Prefer `GET /books/{book_id}/full` and + /// `GET /series/{series_id}/full`. A response whose schema depends on a + /// query parameter cannot be expressed in OpenAPI, so this form is + /// invisible to a generated client. Scheduled for removal in 3.0. + #[deprecated( + since = "2.2.0", + note = "use GET /books/{book_id}/full or GET /series/{series_id}/full; \ +a response shape that depends on a query parameter cannot be described in OpenAPI" + )] pub full: bool, } @@ -2259,6 +2450,7 @@ pub struct InProgressSeriesQuery { ), tag = "Series" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_in_progress_series( State(state): State>, auth: AuthContext, @@ -2314,6 +2506,15 @@ pub struct RecentSeriesQuery { /// Return full series data including metadata, locks, genres, tags, etc. #[serde(default)] + /// **Deprecated.** Prefer `GET /books/{book_id}/full` and + /// `GET /series/{series_id}/full`. A response whose schema depends on a + /// query parameter cannot be expressed in OpenAPI, so this form is + /// invisible to a generated client. Scheduled for removal in 3.0. + #[deprecated( + since = "2.2.0", + note = "use GET /books/{book_id}/full or GET /series/{series_id}/full; \ +a response shape that depends on a query parameter cannot be described in OpenAPI" + )] pub full: bool, } @@ -2336,6 +2537,7 @@ fn default_recent_limit() -> u64 { ), tag = "Series" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_recently_added_series( State(state): State>, auth: AuthContext, @@ -2393,6 +2595,7 @@ pub async fn list_recently_added_series( ), tag = "Series" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_library_recently_added_series( State(state): State>, auth: AuthContext, @@ -2448,6 +2651,7 @@ pub async fn list_library_recently_added_series( ), tag = "Series" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_recently_updated_series( State(state): State>, auth: AuthContext, @@ -2504,6 +2708,7 @@ pub async fn list_recently_updated_series( ), tag = "Series" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_library_recently_updated_series( State(state): State>, auth: AuthContext, @@ -2552,7 +2757,7 @@ pub async fn list_library_recently_updated_series( SeriesListQuery ), responses( - (status = 200, description = "Paginated list of series in library (returns FullSeriesListResponse when full=true)", body = SeriesListResponse), + (status = 200, description = "Paginated list of series in library (returns FullSeriesListResponse when full=true)", body = PaginatedResponse), (status = 403, description = "Forbidden"), ), security( @@ -2561,6 +2766,7 @@ pub async fn list_library_recently_updated_series( ), tag = "Series" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_library_series( State(state): State>, auth: AuthContext, @@ -2652,6 +2858,15 @@ pub async fn list_library_series( pub struct LibraryInProgressSeriesQuery { /// Return full series data including metadata, locks, genres, tags, etc. #[serde(default)] + /// **Deprecated.** Prefer `GET /books/{book_id}/full` and + /// `GET /series/{series_id}/full`. A response whose schema depends on a + /// query parameter cannot be expressed in OpenAPI, so this form is + /// invisible to a generated client. Scheduled for removal in 3.0. + #[deprecated( + since = "2.2.0", + note = "use GET /books/{book_id}/full or GET /series/{series_id}/full; \ +a response shape that depends on a query parameter cannot be described in OpenAPI" + )] pub full: bool, } @@ -2673,6 +2888,7 @@ pub struct LibraryInProgressSeriesQuery { ), tag = "Series" )] +#[allow(deprecated)] // still serves the deprecated `full` parameter pub async fn list_library_in_progress_series( State(state): State>, auth: AuthContext, diff --git a/crates/codex-api/src/routes/v1/handlers/series_exports.rs b/crates/codex-api/src/routes/v1/handlers/series_exports.rs index 8906da3a..3e81672f 100644 --- a/crates/codex-api/src/routes/v1/handlers/series_exports.rs +++ b/crates/codex-api/src/routes/v1/handlers/series_exports.rs @@ -220,7 +220,14 @@ pub async fn get_export( path = "/api/v1/user/exports/series/{id}/download", params(("id" = Uuid, Path, description = "Export ID")), responses( - (status = 200, description = "Export file", content_type = "application/octet-stream"), + // One entry per arm of the `content_type` match below. The export is + // text, never opaque bytes, and the format is the caller's own choice + // at creation time. + (status = 200, description = "Export file", content( + ("text/csv"), + ("text/markdown"), + ("application/json"), + )), (status = 404, description = "Export not found or file missing"), (status = 409, description = "Export not yet completed"), ), diff --git a/crates/codex-api/src/routes/v1/routes/books.rs b/crates/codex-api/src/routes/v1/routes/books.rs index 842f6afe..4f147896 100644 --- a/crates/codex-api/src/routes/v1/routes/books.rs +++ b/crates/codex-api/src/routes/v1/routes/books.rs @@ -32,6 +32,8 @@ pub fn routes(_state: Arc) -> Router> { .route("/books/list", post(handlers::list_books_filtered)) .route("/books/{book_id}", get(handlers::get_book)) .route("/books/{book_id}", patch(handlers::patch_book)) + // Describable alternative to the deprecated `?full=true`. + .route("/books/{book_id}/full", get(handlers::get_book_full)) .route( "/books/{book_id}/adjacent", get(handlers::get_adjacent_books), diff --git a/crates/codex-api/src/routes/v1/routes/series.rs b/crates/codex-api/src/routes/v1/routes/series.rs index 2ac88f3c..168fd05c 100644 --- a/crates/codex-api/src/routes/v1/routes/series.rs +++ b/crates/codex-api/src/routes/v1/routes/series.rs @@ -37,8 +37,12 @@ pub fn routes(_state: Arc) -> Router> { "/series/external-index", get(handlers::list_series_external_index), ) + // Describable alternative to the deprecated `?full=true` listing. + .route("/series/full", get(handlers::list_series_full)) .route("/series/{series_id}", get(handlers::get_series)) .route("/series/{series_id}", patch(handlers::patch_series)) + // Describable alternative to the deprecated `?full=true`. + .route("/series/{series_id}/full", get(handlers::get_series_full)) .route( "/series/{series_id}/read-history", get(handlers::get_series_read_history).delete(handlers::clear_series_read_history), diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 732dc6d6..821fe09e 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -2796,8 +2796,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -2809,7 +2810,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3386,8 +3387,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -3399,7 +3401,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3467,8 +3469,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -3490,7 +3493,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3580,8 +3583,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -3593,7 +3597,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3683,8 +3687,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -3696,7 +3701,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3891,8 +3896,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -4739,7 +4745,7 @@ "Books" ], "summary": "Download book file", - "description": "Streams the original book file (CBZ, CBR, EPUB, PDF) for download.\nUsed by OPDS clients for acquisition links.", + "description": "Streams the original book file (CBZ, CBR, EPUB, PDF) for download.\nUsed by OPDS clients for acquisition links.\n\nRange-capable and conditional. `Accept-Ranges: bytes` and a strong `ETag`\ngo out on every response, so a download interrupted partway can resume with\n`Range` rather than starting again, and a client that understands the\ncontainer can read part of an archive without fetching all of it — the\nsuffix form `bytes=-65536` reaches a ZIP central directory directly.", "operationId": "get_book_file", "parameters": [ { @@ -4751,15 +4757,119 @@ "type": "string", "format": "uuid" } + }, + { + "name": "Range", + "in": "header", + "description": "Byte range, e.g. `bytes=0-1023`, `bytes=1024-` or `bytes=-65536`. A single range only; multiple ranges are answered with the whole file.", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "If-Range", + "in": "header", + "description": "Serve the range only if this ETag still matches; otherwise the whole file is returned, so a resume cannot splice bytes from two versions.", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "If-None-Match", + "in": "header", + "description": "Return 304 if this ETag still matches.", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } } ], "responses": { "200": { "description": "Book file", "content": { + "application/zip": {}, + "application/x-rar-compressed": {}, + "application/epub+zip": {}, + "application/pdf": {}, + "application/octet-stream": {} + } + }, + "206": { + "description": "The requested byte range", + "content": { + "application/zip": {}, + "application/x-rar-compressed": {}, + "application/epub+zip": {}, + "application/pdf": {}, "application/octet-stream": {} } }, + "304": { + "description": "Not modified (client cache is valid)" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Book not found" + }, + "416": { + "description": "The requested range names no byte of the file" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/books/{book_id}/full": { + "get": { + "tags": [ + "Books" + ], + "summary": "Get a book with its metadata, genres and tags in one response", + "description": "The same book `GET /api/v1/books/{book_id}` returns, plus the related data a\ndetail screen needs, so it does not have to fan out into separate metadata,\ngenre and tag requests.\n\nThis exists as its own route because the shape is genuinely different, not\nmerely richer: it carries `metadata`, `genres`, `tags`, `readCount` and\n`lastCompletedAt`, and it moves `chapter`, `summary` and `volume` inside\n`metadata`. A response whose schema depends on a query parameter cannot be\nexpressed in OpenAPI, so the deprecated `?full=true` form is undescribable\nand unusable from a generated client. This route is describable.", + "operationId": "get_book_full", + "parameters": [ + { + "name": "book_id", + "in": "path", + "description": "Book ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Book with its related data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FullBookResponse" + } + } + } + }, "403": { "description": "Forbidden" }, @@ -5091,7 +5201,7 @@ "200": { "description": "Page image", "content": { - "image/jpeg": {} + "image/*": {} } }, "304": { @@ -5592,7 +5702,7 @@ "200": { "description": "Thumbnail image", "content": { - "image/jpeg": {} + "image/*": {} } }, "304": { @@ -7389,9 +7499,9 @@ }, { "name": "page", - "in": "path", + "in": "query", "description": "Page number (1-indexed, minimum 1)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", @@ -7400,23 +7510,212 @@ }, { "name": "pageSize", + "in": "query", + "description": "Number of items per page (max 100, default 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sort parameter (format: \"field,direction\" e.g. \"title,asc\")", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", + "required": false, + "deprecated": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Paginated list of books in library", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_BookDto" + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/libraries/{library_id}/books/in-progress": { + "get": { + "tags": [ + "Books" + ], + "summary": "List books with reading progress in a specific library (in-progress books)", + "operationId": "list_library_in_progress_books", + "parameters": [ + { + "name": "library_id", "in": "path", + "description": "Library ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "page", + "in": "query", + "description": "Page number (1-indexed, minimum 1)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "pageSize", + "in": "query", "description": "Number of items per page (max 100, default 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sort parameter (format: \"field,direction\" e.g. \"title,asc\")", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", + "required": false, + "deprecated": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Paginated list of in-progress books in library", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_BookDto" + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/libraries/{library_id}/books/on-deck": { + "get": { + "tags": [ + "Books" + ], + "summary": "List on-deck books in a specific library", + "operationId": "list_library_on_deck_books", + "parameters": [ + { + "name": "library_id", + "in": "path", + "description": "Library ID", "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "page", + "in": "query", + "description": "Page number (1-indexed, minimum 1)", + "required": false, "schema": { "type": "integer", "format": "int64", "minimum": 0 } + }, + { + "name": "pageSize", + "in": "query", + "description": "Number of items per page (max 100, default 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sort parameter (format: \"field,direction\" e.g. \"title,asc\")", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", + "required": false, + "deprecated": true, + "schema": { + "type": "boolean" + } } ], "responses": { "200": { - "description": "Paginated list of books in library", + "description": "Paginated list of on-deck books in library", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -7435,13 +7734,13 @@ ] } }, - "/api/v1/libraries/{library_id}/books/in-progress": { + "/api/v1/libraries/{library_id}/books/recently-added": { "get": { "tags": [ "Books" ], - "summary": "List books with reading progress in a specific library (in-progress books)", - "operationId": "list_library_in_progress_books", + "summary": "List recently added books in a specific library", + "operationId": "list_library_recently_added_books", "parameters": [ { "name": "library_id", @@ -7455,9 +7754,9 @@ }, { "name": "page", - "in": "path", + "in": "query", "description": "Page number (1-indexed, minimum 1)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", @@ -7466,23 +7765,42 @@ }, { "name": "pageSize", - "in": "path", + "in": "query", "description": "Number of items per page (max 100, default 50)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", "minimum": 0 } + }, + { + "name": "sort", + "in": "query", + "description": "Sort parameter (format: \"field,direction\" e.g. \"title,asc\")", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", + "required": false, + "deprecated": true, + "schema": { + "type": "boolean" + } } ], "responses": { "200": { - "description": "Paginated list of in-progress books in library", + "description": "Paginated list of recently added books in library", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -7501,13 +7819,13 @@ ] } }, - "/api/v1/libraries/{library_id}/books/on-deck": { + "/api/v1/libraries/{library_id}/books/recently-read": { "get": { "tags": [ "Books" ], - "summary": "List on-deck books in a specific library", - "operationId": "list_library_on_deck_books", + "summary": "List recently read books in a specific library", + "operationId": "list_library_recently_read_books", "parameters": [ { "name": "library_id", @@ -7520,41 +7838,91 @@ } }, { - "name": "page", - "in": "path", - "description": "Page number (1-indexed, minimum 1)", - "required": true, + "name": "limit", + "in": "query", + "description": "Maximum number of books to return (default: 50)", + "required": false, "schema": { "type": "integer", "format": "int64", "minimum": 0 } + } + ], + "responses": { + "200": { + "description": "List of recently read books in library", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BookDto" + } + } + } + } }, + "403": { + "description": "Forbidden" + } + }, + "security": [ { - "name": "pageSize", + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/libraries/{library_id}/books/thumbnails/generate": { + "post": { + "tags": [ + "Thumbnails" + ], + "summary": "Generate thumbnails for all books in a library", + "description": "Queues a fan-out task that enqueues individual thumbnail generation tasks for each book in the library.\n\n# Permission Required\n- `tasks:write`", + "operationId": "generate_library_book_thumbnails", + "parameters": [ + { + "name": "library_id", "in": "path", - "description": "Number of items per page (max 100, default 50)", + "description": "Library ID", "required": true, "schema": { - "type": "integer", - "format": "int64", - "minimum": 0 + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForceRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Paginated list of on-deck books in library", + "description": "Thumbnail generation task queued", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/CreateTaskResponse" } } } }, "403": { - "description": "Forbidden" + "description": "Permission denied" + }, + "404": { + "description": "Library not found" } }, "security": [ @@ -7567,13 +7935,60 @@ ] } }, - "/api/v1/libraries/{library_id}/books/recently-added": { + "/api/v1/libraries/{library_id}/jobs": { "get": { "tags": [ - "Books" + "Library Jobs" ], - "summary": "List recently added books in a specific library", - "operationId": "list_library_recently_added_books", + "summary": "List the scheduled jobs configured for a library", + "description": "Jobs are per-library scheduled work; today the only kind is a metadata\nrefresh. The schedule is a cron expression evaluated in the job's timezone,\nor the server's if it declares none.", + "operationId": "list_jobs", + "parameters": [ + { + "name": "library_id", + "in": "path", + "description": "Library ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The library's jobs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListLibraryJobsResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Library not found" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "Library Jobs" + ], + "summary": "Create a scheduled job for a library", + "description": "The job's type is taken from the `config` variant rather than named\nseparately, so the two cannot disagree. A blank `name` is generated from the\nconfig.", + "operationId": "create_job", "parameters": [ { "name": "library_id", @@ -7584,43 +7999,94 @@ "type": "string", "format": "uuid" } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateLibraryJobRequest" + } + } }, + "required": true + }, + "responses": { + "201": { + "description": "The created job", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LibraryJobDto" + } + } + } + }, + "400": { + "description": "Invalid cron expression, timezone, or config" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Library not found" + } + }, + "security": [ { - "name": "page", + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/libraries/{library_id}/jobs/{job_id}": { + "get": { + "tags": [ + "Library Jobs" + ], + "summary": "Get one scheduled job", + "operationId": "get_job", + "parameters": [ + { + "name": "library_id", "in": "path", - "description": "Page number (1-indexed, minimum 1)", + "description": "Library ID", "required": true, "schema": { - "type": "integer", - "format": "int64", - "minimum": 0 + "type": "string", + "format": "uuid" } }, { - "name": "pageSize", + "name": "job_id", "in": "path", - "description": "Number of items per page (max 100, default 50)", + "description": "Job ID", "required": true, "schema": { - "type": "integer", - "format": "int64", - "minimum": 0 + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "Paginated list of recently added books in library", + "description": "The job", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/LibraryJobDto" } } } }, "403": { "description": "Forbidden" + }, + "404": { + "description": "Job not found, or it belongs to another library" } }, "security": [ @@ -7631,15 +8097,13 @@ "api_key": [] } ] - } - }, - "/api/v1/libraries/{library_id}/books/recently-read": { - "get": { + }, + "delete": { "tags": [ - "Books" + "Library Jobs" ], - "summary": "List recently read books in a specific library", - "operationId": "list_library_recently_read_books", + "summary": "Delete a scheduled job", + "operationId": "delete_job", "parameters": [ { "name": "library_id", @@ -7652,33 +8116,94 @@ } }, { - "name": "limit", - "in": "query", - "description": "Maximum number of books to return (default: 50)", - "required": false, + "name": "job_id", + "in": "path", + "description": "Job ID", + "required": true, "schema": { - "type": "integer", - "format": "int64", - "minimum": 0 + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Job deleted" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Job not found, or it belongs to another library" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + }, + "patch": { + "tags": [ + "Library Jobs" + ], + "summary": "Update a scheduled job", + "description": "Every field is optional. `timezone` is tri-state: absent leaves it, `null`\nclears it back to the server default, and a value sets it. A `config` whose\ntype differs from the job's is rejected rather than migrating the job.", + "operationId": "patch_job", + "parameters": [ + { + "name": "library_id", + "in": "path", + "description": "Library ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "job_id", + "in": "path", + "description": "Job ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchLibraryJobRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "List of recently read books in library", + "description": "The updated job", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BookDto" - } + "$ref": "#/components/schemas/LibraryJobDto" } } } }, + "400": { + "description": "Empty name, invalid schedule, or a config of the wrong type" + }, "403": { "description": "Forbidden" + }, + "404": { + "description": "Job not found, or it belongs to another library" } }, "security": [ @@ -7691,14 +8216,14 @@ ] } }, - "/api/v1/libraries/{library_id}/books/thumbnails/generate": { + "/api/v1/libraries/{library_id}/jobs/{job_id}/dry-run": { "post": { "tags": [ - "Thumbnails" + "Library Jobs" ], - "summary": "Generate thumbnails for all books in a library", - "description": "Queues a fan-out task that enqueues individual thumbnail generation tasks for each book in the library.\n\n# Permission Required\n- `tasks:write`", - "operationId": "generate_library_book_thumbnails", + "summary": "Preview what a job would change, without changing anything", + "description": "Plans the refresh and returns a sample of the per-series field changes it\nwould make. Nothing is written. `configOverride` plans against a config the\njob does not have yet, which is what lets an editor preview an edit before\nsaving it; it must be of the job's own type.", + "operationId": "dry_run_job", "parameters": [ { "name": "library_id", @@ -7709,13 +8234,23 @@ "type": "string", "format": "uuid" } + }, + { + "name": "job_id", + "in": "path", + "description": "Job ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForceRequest" + "$ref": "#/components/schemas/DryRunRequest" } } }, @@ -7723,20 +8258,84 @@ }, "responses": { "200": { - "description": "Thumbnail generation task queued", + "description": "The planned changes", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateTaskResponse" + "$ref": "#/components/schemas/DryRunResponse" } } } }, + "400": { + "description": "Override config is of the wrong type, or is invalid" + }, "403": { - "description": "Permission denied" + "description": "Forbidden" }, "404": { - "description": "Library not found" + "description": "Job not found, or it belongs to another library" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/libraries/{library_id}/jobs/{job_id}/run-now": { + "post": { + "tags": [ + "Library Jobs" + ], + "summary": "Run a scheduled job immediately", + "description": "Enqueues the job's task and returns its id; the run itself is asynchronous\nand its progress is followed through the task queue. Refuses if a run for\nthis job is already in flight, so a double click cannot queue the work\ntwice.", + "operationId": "run_job_now", + "parameters": [ + { + "name": "library_id", + "in": "path", + "description": "Library ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "job_id", + "in": "path", + "description": "Job ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The enqueued task", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunNowResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Job not found, or it belongs to another library" + }, + "409": { + "description": "A run for this job is already in flight" } }, "security": [ @@ -8042,8 +8641,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, alternate titles,\nexternal ratings, and external links. Default is false for backward compatibility.", + "description": "Return full series data including metadata, locks, genres, tags, alternate titles,\nexternal ratings, and external links. Default is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -8055,7 +8655,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" } } } @@ -8095,8 +8695,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -8175,8 +8776,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -8255,8 +8857,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -8406,6 +9009,42 @@ ] } }, + "/api/v1/library-jobs/metadata-refresh/field-groups": { + "get": { + "tags": [ + "Library Jobs" + ], + "summary": "List the metadata field groups a refresh job can target", + "description": "A static catalog: the groups and the concrete metadata fields each one\ncovers. It is what a job editor offers as refreshable fields, so a client\ndoes not have to hardcode the list.", + "operationId": "list_field_groups", + "responses": { + "200": { + "description": "The field-group catalog", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldGroupDto" + } + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, "/api/v1/metrics/inventory": { "get": { "tags": [ @@ -10553,8 +11192,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, alternate titles,\nexternal ratings, and external links. Default is false for backward compatibility.", + "description": "Return full series data including metadata, locks, genres, tags, alternate titles,\nexternal ratings, and external links. Default is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -10566,7 +11206,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" } } } @@ -11293,8 +11933,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -11306,7 +11947,113 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesExternalIndexDto" + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/series/full": { + "get": { + "tags": [ + "Series" + ], + "summary": "List series with their metadata, genres, tags and external links", + "description": "The same page `GET /api/v1/series` returns, with each entry carrying the\nrelated data inline, so a caller that needs metadata for a whole library\npages it once instead of following up per series.\n\nThis exists as its own route because the shape is genuinely different, not\nmerely richer, and because a response whose schema depends on a query\nparameter cannot be expressed in OpenAPI — which made the deprecated\n`GET /api/v1/series?full=true` invisible to every generated client. Accepts\nthe same filters and pagination as the plain listing.", + "operationId": "list_series_full", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "Page number (1-indexed, default 1)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "pageSize", + "in": "query", + "description": "Number of items per page (max 100, default 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sort parameter (format: \"field,direction\" e.g. \"name,asc\")", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "genres", + "in": "query", + "description": "Filter by genres (comma-separated, AND logic - series must have ALL specified genres)", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "tags", + "in": "query", + "description": "Filter by tags (comma-separated, AND logic - series must have ALL specified tags)", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "libraryId", + "in": "query", + "description": "Filter by library ID", + "required": false, + "schema": { + "type": [ + "string", + "null" + ], + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Paginated series with their related data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_FullSeriesResponse" } } } @@ -11349,8 +12096,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -11433,8 +12181,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -11456,7 +12205,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" } } } @@ -11608,8 +12357,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -11678,8 +12428,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -12182,8 +12933,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -12779,8 +13531,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -13718,6 +14471,54 @@ ] } }, + "/api/v1/series/{series_id}/full": { + "get": { + "tags": [ + "Series" + ], + "summary": "Get a series with its metadata, genres, tags and external links in one response", + "description": "The same series `GET /api/v1/series/{series_id}` returns, plus the related\ndata a detail screen needs, so it does not have to fan out into separate\nmetadata, genre, tag, external-id, external-link and rating requests.\n\nThis exists as its own route because the shape is genuinely different, not\nmerely richer: it carries `metadata`, `genres`, `tags`, `alternateTitles`,\n`externalIds`, `externalLinks`, `externalRatings`, `readCount` and\n`lastCompletedAt`, and it moves `title`, `titleSort`, `publisher`, `summary`\nand `year` inside `metadata`. A response whose schema depends on a query\nparameter cannot be expressed in OpenAPI, so the deprecated `?full=true`\nform is undescribable and unusable from a generated client. This route is\ndescribable.", + "operationId": "get_series_full", + "parameters": [ + { + "name": "series_id", + "in": "path", + "description": "Series ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Series with its related data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FullSeriesResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Series not found" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, "/api/v1/series/{series_id}/genres": { "get": { "tags": [ @@ -15179,7 +15980,7 @@ "200": { "description": "Thumbnail image", "content": { - "image/jpeg": {} + "image/*": {} } }, "304": { @@ -16457,7 +17258,9 @@ "200": { "description": "Export file", "content": { - "application/octet-stream": {} + "text/csv": {}, + "text/markdown": {}, + "application/json": {} } }, "404": { @@ -18299,7 +19102,7 @@ "200": { "description": "Page image (also records reading progress)", "content": { - "image/jpeg": {} + "image/*": {} } }, "403": { @@ -23766,10 +24569,7 @@ "description": "Request body for POST /books/list\n\nPagination parameters (page, pageSize, sort) are passed as query parameters,\nnot in the request body. This enables proper HATEOAS links.", "properties": { "condition": { - "type": [ - "object", - "null" - ], + "$ref": "#/components/schemas/BookCondition", "description": "Filter condition (optional - no condition returns all)" }, "fullTextSearch": { @@ -31141,100 +31941,7 @@ "content": { "type": "array", "items": { - "type": "object", - "description": "Komga book DTO\n\nBased on actual Komic traffic analysis. This is the main book representation.", - "required": [ - "id", - "seriesId", - "seriesTitle", - "libraryId", - "name", - "url", - "number", - "created", - "lastModified", - "fileLastModified", - "sizeBytes", - "size", - "media", - "metadata" - ], - "properties": { - "created": { - "type": "string", - "description": "Created timestamp (ISO 8601)" - }, - "deleted": { - "type": "boolean", - "description": "Whether book is deleted (soft delete)" - }, - "fileHash": { - "type": "string", - "description": "File hash" - }, - "fileLastModified": { - "type": "string", - "description": "File last modified timestamp (ISO 8601)" - }, - "id": { - "type": "string", - "description": "Book unique identifier (UUID as string)" - }, - "lastModified": { - "type": "string", - "description": "Last modified timestamp (ISO 8601)" - }, - "libraryId": { - "type": "string", - "description": "Library ID" - }, - "media": { - "$ref": "#/components/schemas/KomgaMediaDto", - "description": "Media information" - }, - "metadata": { - "$ref": "#/components/schemas/KomgaBookMetadataDto", - "description": "Book metadata" - }, - "name": { - "type": "string", - "description": "Book filename/name" - }, - "number": { - "type": "integer", - "format": "int32", - "description": "Book number in series" - }, - "oneshot": { - "type": "boolean", - "description": "Whether this is a oneshot" - }, - "readProgress": { - "$ref": "#/components/schemas/KomgaReadProgressDto", - "description": "User's read progress (null if not started)" - }, - "seriesId": { - "type": "string", - "description": "Series ID" - }, - "seriesTitle": { - "type": "string", - "description": "Series title (required by Komic for display)" - }, - "size": { - "type": "string", - "description": "Human-readable file size (e.g., \"869.9 MiB\")" - }, - "sizeBytes": { - "type": "integer", - "format": "int64", - "description": "File size in bytes" - }, - "url": { - "type": "string", - "description": "File URL/path" - } - } + "$ref": "#/components/schemas/KomgaBookDto" }, "description": "The content items for this page" }, @@ -31305,50 +32012,7 @@ "content": { "type": "array", "items": { - "type": "object", - "description": "Minimal collection DTO (stub)\n\nKomga collections are user-created groupings of series.\nCodex doesn't support this feature, so we return empty results.", - "required": [ - "id", - "name", - "ordered", - "seriesIds", - "createdDate", - "lastModifiedDate", - "filtered" - ], - "properties": { - "createdDate": { - "type": "string", - "description": "Created timestamp (ISO 8601)" - }, - "filtered": { - "type": "boolean", - "description": "Whether this collection is filtered from the user's view" - }, - "id": { - "type": "string", - "description": "Collection unique identifier" - }, - "lastModifiedDate": { - "type": "string", - "description": "Last modified timestamp (ISO 8601)" - }, - "name": { - "type": "string", - "description": "Collection name" - }, - "ordered": { - "type": "boolean", - "description": "Whether the collection is ordered" - }, - "seriesIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Series IDs in the collection" - } - } + "$ref": "#/components/schemas/KomgaCollectionDto" }, "description": "The content items for this page" }, @@ -31419,55 +32083,7 @@ "content": { "type": "array", "items": { - "type": "object", - "description": "Minimal read list DTO (stub)\n\nKomga read lists are user-created lists of books to read.\nCodex doesn't support this feature, so we return empty results.", - "required": [ - "id", - "name", - "summary", - "ordered", - "bookIds", - "createdDate", - "lastModifiedDate", - "filtered" - ], - "properties": { - "bookIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Book IDs in the read list" - }, - "createdDate": { - "type": "string", - "description": "Created timestamp (ISO 8601)" - }, - "filtered": { - "type": "boolean", - "description": "Whether this read list is filtered from the user's view" - }, - "id": { - "type": "string", - "description": "Read list unique identifier" - }, - "lastModifiedDate": { - "type": "string", - "description": "Last modified timestamp (ISO 8601)" - }, - "name": { - "type": "string", - "description": "Read list name" - }, - "ordered": { - "type": "boolean", - "description": "Whether the read list is ordered" - }, - "summary": { - "type": "string", - "description": "Read list summary/description" - } - } + "$ref": "#/components/schemas/KomgaReadListDto" }, "description": "The content items for this page" }, @@ -31538,89 +32154,7 @@ "content": { "type": "array", "items": { - "type": "object", - "description": "Komga series DTO\n\nBased on actual Komic traffic analysis.", - "required": [ - "id", - "libraryId", - "name", - "url", - "created", - "lastModified", - "fileLastModified", - "booksCount", - "booksReadCount", - "booksUnreadCount", - "booksInProgressCount", - "metadata", - "booksMetadata" - ], - "properties": { - "booksCount": { - "type": "integer", - "format": "int32", - "description": "Total books count" - }, - "booksInProgressCount": { - "type": "integer", - "format": "int32", - "description": "In-progress books count" - }, - "booksMetadata": { - "$ref": "#/components/schemas/KomgaBooksMetadataAggregationDto", - "description": "Aggregated books metadata" - }, - "booksReadCount": { - "type": "integer", - "format": "int32", - "description": "Read books count" - }, - "booksUnreadCount": { - "type": "integer", - "format": "int32", - "description": "Unread books count" - }, - "created": { - "type": "string", - "description": "Created timestamp (ISO 8601)" - }, - "deleted": { - "type": "boolean", - "description": "Whether series is deleted (soft delete)" - }, - "fileLastModified": { - "type": "string", - "description": "File last modified timestamp (ISO 8601)" - }, - "id": { - "type": "string", - "description": "Series unique identifier (UUID as string)" - }, - "lastModified": { - "type": "string", - "description": "Last modified timestamp (ISO 8601)" - }, - "libraryId": { - "type": "string", - "description": "Library ID" - }, - "metadata": { - "$ref": "#/components/schemas/KomgaSeriesMetadataDto", - "description": "Series metadata" - }, - "name": { - "type": "string", - "description": "Series name" - }, - "oneshot": { - "type": "boolean", - "description": "Whether this is a oneshot (single book)" - }, - "url": { - "type": "string", - "description": "File URL/path" - } - } + "$ref": "#/components/schemas/KomgaSeriesDto" }, "description": "The content items for this page" }, @@ -34261,7 +34795,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/BookDto" + "$ref": "#/components/schemas/FullBookResponse" }, "description": "The data items for this page" }, @@ -34314,80 +34848,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "API key data transfer object", - "required": [ - "id", - "userId", - "name", - "keyPrefix", - "permissions", - "isActive", - "createdAt", - "updatedAt" - ], - "properties": { - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the key was created", - "example": "2024-01-01T00:00:00Z" - }, - "expiresAt": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When the key expires (if set)", - "example": "2025-12-31T23:59:59Z" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Unique API key identifier", - "example": "550e8400-e29b-41d4-a716-446655440000" - }, - "isActive": { - "type": "boolean", - "description": "Whether the key is currently active", - "example": true - }, - "keyPrefix": { - "type": "string", - "description": "Prefix of the key for identification", - "example": "cdx_a1b2c3" - }, - "lastUsedAt": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When the key was last used", - "example": "2024-01-15T10:30:00Z" - }, - "name": { - "type": "string", - "description": "Human-readable name for the key", - "example": "Mobile App Key" - }, - "permissions": { - "description": "Permissions granted to this key" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "When the key was last updated", - "example": "2024-01-15T10:30:00Z" - }, - "userId": { - "type": "string", - "format": "uuid", - "description": "Owner user ID", - "example": "550e8400-e29b-41d4-a716-446655440001" - } - } + "$ref": "#/components/schemas/ApiKeyDto" }, "description": "The data items for this page" }, @@ -34440,187 +34901,60 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Book data transfer object", - "required": [ - "id", - "libraryId", - "libraryName", - "seriesId", - "seriesName", - "title", - "path", - "fileFormat", - "fileSize", - "fileHash", - "pageCount", - "createdAt", - "updatedAt", - "deleted", - "analyzed" - ], - "properties": { - "analysisError": { - "type": [ - "string", - "null" - ], - "description": "Error message if book analysis failed", - "example": "Failed to parse CBZ: invalid archive" - }, - "analyzed": { - "type": "boolean", - "description": "Whether the book has been analyzed (page dimensions available)", - "example": true - }, - "chapter": { - "type": [ - "number", - "null" - ], - "format": "float", - "description": "Chapter number from book metadata (may be fractional)", - "example": 42.5 - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the book was added to the library", - "example": "2024-01-01T00:00:00Z" - }, - "deleted": { - "type": "boolean", - "description": "Whether the book has been soft-deleted", - "example": false - }, - "fileFormat": { - "type": "string", - "description": "File format (cbz, cbr, epub, pdf)", - "example": "cbz" - }, - "fileHash": { - "type": "string", - "description": "File hash for deduplication", - "example": "a1b2c3d4e5f6g7h8i9j0" - }, - "fileSize": { - "type": "integer", - "format": "int64", - "description": "File size in bytes", - "example": 52428800 - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Book unique identifier", - "example": "550e8400-e29b-41d4-a716-446655440001" - }, - "koreaderHash": { - "type": [ - "string", - "null" - ], - "description": "KOReader-compatible partial MD5 hash for sync" - }, - "libraryId": { - "type": "string", - "format": "uuid", - "description": "Library this book belongs to", - "example": "550e8400-e29b-41d4-a716-446655440000" - }, - "libraryName": { - "type": "string", - "description": "Name of the library", - "example": "Comics" - }, - "number": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "description": "Book number within the series", - "example": 1 - }, - "pageCount": { - "type": "integer", - "format": "int32", - "description": "Number of pages in the book", - "example": 32 - }, - "path": { - "type": "string", - "description": "Filesystem path to the book file", - "example": "/media/comics/Batman/Batman - Year One 001.cbz" - }, - "readProgress": { - "$ref": "#/components/schemas/ReadProgressResponse", - "description": "User's read progress for this book" - }, - "readingDirection": { - "type": [ - "string", - "null" - ], - "description": "Effective reading direction (from series metadata, or library default if not set)\nValues: ltr, rtl, ttb or webtoon", - "example": "ltr" - }, - "seriesId": { - "type": "string", - "format": "uuid", - "description": "Series this book belongs to", - "example": "550e8400-e29b-41d4-a716-446655440002" - }, - "seriesName": { - "type": "string", - "description": "Name of the series", - "example": "Batman: Year One" - }, - "summary": { - "type": [ - "string", - "null" - ], - "description": "Book summary/description from book_metadata (ComicInfo `` or\nEPUB description). Surfaced on the list response so cards can show it on\nhover without fetching the full detail payload.", - "example": "Bruce Wayne returns to Gotham to begin his war on crime." - }, - "title": { - "type": "string", - "description": "Book title", - "example": "Batman: Year One #1" - }, - "titleSort": { - "type": [ - "string", - "null" - ], - "description": "Title used for sorting (title_sort field)", - "example": "batman year one 001" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "When the book was last updated", - "example": "2024-01-15T10:30:00Z" - }, - "volume": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "description": "Volume number from book metadata", - "example": 1 - }, - "wantToRead": { - "type": [ - "boolean", - "null" - ], - "description": "Whether the requesting user has this book in their want-to-read queue.\n\n`None` when not computed for this response; populated on book list and\ndetail endpoints that have a user context.", - "example": false - } - } + "$ref": "#/components/schemas/BookDto" + }, + "description": "The data items for this page" + }, + "links": { + "$ref": "#/components/schemas/PaginationLinks", + "description": "HATEOAS navigation links" + }, + "page": { + "type": "integer", + "format": "int64", + "description": "Current page number (1-indexed)", + "example": 1, + "minimum": 0 + }, + "pageSize": { + "type": "integer", + "format": "int64", + "description": "Number of items per page", + "example": 50, + "minimum": 0 + }, + "total": { + "type": "integer", + "format": "int64", + "description": "Total number of items across all pages", + "example": 150, + "minimum": 0 + }, + "totalPages": { + "type": "integer", + "format": "int64", + "description": "Total number of pages", + "example": 3, + "minimum": 0 + } + } + }, + "PaginatedResponse_FullSeriesResponse": { + "type": "object", + "description": "Generic paginated response wrapper with HATEOAS links", + "required": [ + "data", + "page", + "pageSize", + "total", + "totalPages", + "links" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FullSeriesResponse" }, "description": "The data items for this page" }, @@ -34673,42 +35007,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Genre data transfer object", - "required": [ - "id", - "name", - "createdAt" - ], - "properties": { - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the genre was created", - "example": "2024-01-01T00:00:00Z" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Genre ID", - "example": "550e8400-e29b-41d4-a716-446655440010" - }, - "name": { - "type": "string", - "description": "Genre name", - "example": "Action" - }, - "seriesCount": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Number of series with this genre", - "example": 42, - "minimum": 0 - } - } + "$ref": "#/components/schemas/GenreDto" }, "description": "The data items for this page" }, @@ -34761,149 +35060,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Library data transfer object", - "required": [ - "id", - "name", - "path", - "isActive", - "seriesStrategy", - "bookStrategy", - "numberStrategy", - "createdAt", - "updatedAt", - "defaultReadingDirection" - ], - "properties": { - "allowedFormats": { - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - }, - "description": "Allowed file formats (e.g., [\"CBZ\", \"CBR\", \"EPUB\"])", - "example": [ - "CBZ", - "CBR", - "PDF" - ] - }, - "autoMatchConditions": { - "description": "Auto-match conditions (JSON object with mode and rules)\nControls when auto-matching runs for this library" - }, - "bookConfig": { - "description": "Book strategy-specific configuration (JSON)" - }, - "bookCount": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Total number of books in this library", - "example": 1250 - }, - "bookStrategy": { - "$ref": "#/components/schemas/BookStrategy", - "description": "Book naming strategy (filename, metadata_first, smart, series_name)" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the library was created", - "example": "2024-01-01T00:00:00Z" - }, - "defaultReadingDirection": { - "type": "string", - "description": "Default reading direction for books in this library (ltr, rtl, ttb or webtoon)", - "example": "ltr" - }, - "description": { - "type": [ - "string", - "null" - ], - "description": "Optional description", - "example": "My comic book collection" - }, - "excludedPatterns": { - "type": [ - "string", - "null" - ], - "description": "Excluded path patterns (newline-separated, e.g., \".DS_Store\\nThumbs.db\")", - "example": ".DS_Store\nThumbs.db" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Library unique identifier", - "example": "550e8400-e29b-41d4-a716-446655440000" - }, - "isActive": { - "type": "boolean", - "description": "Whether the library is active", - "example": true - }, - "lastScannedAt": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When the library was last scanned", - "example": "2024-01-15T10:30:00Z" - }, - "name": { - "type": "string", - "description": "Library name", - "example": "Comics" - }, - "numberConfig": { - "description": "Number strategy-specific configuration (JSON)" - }, - "numberStrategy": { - "$ref": "#/components/schemas/NumberStrategy", - "description": "Book number strategy (file_order, metadata, filename, smart)" - }, - "path": { - "type": "string", - "description": "Filesystem path to the library root", - "example": "/media/comics" - }, - "scanningConfig": { - "$ref": "#/components/schemas/ScanningConfigDto", - "description": "Scanning configuration for scheduled scans" - }, - "seriesConfig": { - "description": "Strategy-specific configuration (JSON)" - }, - "seriesCount": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Total number of series in this library", - "example": 85 - }, - "seriesStrategy": { - "$ref": "#/components/schemas/SeriesStrategy", - "description": "Series detection strategy (series_volume, series_volume_chapter, flat, etc.)" - }, - "titlePreprocessingRules": { - "description": "Title preprocessing rules (JSON array of regex rules)\nApplied during scan to clean series titles before metadata search" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "When the library was last updated", - "example": "2024-01-15T10:30:00Z" - } - } + "$ref": "#/components/schemas/LibraryDto" }, "description": "The data items for this page" }, @@ -34956,136 +35113,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "A single release announcement. Sources write these; the inbox reads them.", - "required": [ - "id", - "seriesId", - "seriesTitle", - "sourceId", - "externalReleaseId", - "payloadUrl", - "confidence", - "state", - "observedAt", - "createdAt" - ], - "properties": { - "chapters": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/ReleaseSpanDto" - }, - "description": "Full chapter coverage as a normalized span list. Decimals supported\n(`c12.5` → `[{start: 12.5, end: 12.5}]`). `null` when the upstream\ntitle carried no chapter info." - }, - "confidence": { - "type": "number", - "format": "double" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "externalReleaseId": { - "type": "string", - "description": "Plugin-stable identity for the release (used for dedup).", - "example": "nyaa:1234567" - }, - "formatHints": { - "description": "Sparse `{ \"jxl\": true, \"container\": \"cbz\", ... }`." - }, - "groupOrUploader": { - "type": [ - "string", - "null" - ], - "description": "Group/scanlator/uploader attribution." - }, - "id": { - "type": "string", - "format": "uuid", - "example": "550e8400-e29b-41d4-a716-446655440a00" - }, - "infoHash": { - "type": [ - "string", - "null" - ], - "description": "Torrent info_hash, if applicable." - }, - "language": { - "type": [ - "string", - "null" - ] - }, - "mediaUrl": { - "type": [ - "string", - "null" - ], - "description": "Optional second URL for direct fetch (`.torrent`, `magnet:`, DDL\nlink). Travels paired with [`Self::media_url_kind`]." - }, - "mediaUrlKind": { - "type": [ - "string", - "null" - ], - "description": "Classifies what `media_url` points at: `torrent` | `magnet` |\n`direct` | `other`. The frontend uses this to pick a kind-specific\nicon next to the standard external-link icon." - }, - "metadata": { - "description": "Source-specific extras (free-form)." - }, - "observedAt": { - "type": "string", - "format": "date-time", - "description": "When Codex detected this release." - }, - "payloadUrl": { - "type": "string", - "description": "Where to acquire the release. Conventionally a human-readable\nlanding page (Nyaa view page, MangaUpdates release page)." - }, - "releasedAt": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Upstream publish date from the source feed. `null` when unavailable." - }, - "seriesId": { - "type": "string", - "format": "uuid", - "example": "550e8400-e29b-41d4-a716-446655440002" - }, - "seriesTitle": { - "type": "string", - "description": "Series title at the time of the response. Joined from the `series`\ntable so the inbox UI can render a human-readable label without a\nfollow-up fetch. Falls back to the empty string only if the series\nrow was hard-deleted between the join and the read.", - "example": "Chainsaw Man" - }, - "sourceId": { - "type": "string", - "format": "uuid", - "example": "550e8400-e29b-41d4-a716-446655440b00" - }, - "state": { - "type": "string", - "description": "`announced` | `dismissed` | `marked_acquired` | `hidden`." - }, - "volumes": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/ReleaseSpanDto" - }, - "description": "Full volume coverage as a normalized span list. `null` semantics\nmirror [`Self::chapters`]." - } - } + "$ref": "#/components/schemas/ReleaseLedgerEntryDto" }, "description": "The data items for this page" }, @@ -35138,192 +35166,60 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Series data transfer object", - "required": [ - "id", - "libraryId", - "libraryName", - "title", - "bookCount", - "tracked", - "createdAt", - "updatedAt" - ], - "properties": { - "bookCount": { - "type": "integer", - "format": "int64", - "description": "Total number of books in this series", - "example": 4 - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the series was created", - "example": "2024-01-01T00:00:00Z" - }, - "hasCustomCover": { - "type": [ - "boolean", - "null" - ], - "description": "Whether the series has a custom cover uploaded", - "example": false - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Series unique identifier", - "example": "550e8400-e29b-41d4-a716-446655440002" - }, - "libraryId": { - "type": "string", - "format": "uuid", - "description": "Library unique identifier", - "example": "550e8400-e29b-41d4-a716-446655440000" - }, - "libraryName": { - "type": "string", - "description": "Name of the library this series belongs to", - "example": "Comics" - }, - "localMaxChapter": { - "type": [ - "number", - "null" - ], - "format": "float", - "description": "Highest `book_metadata.chapter` across the books in this series.\n\n`None` when no book in the series has `chapter` populated. When\nnon-null and `metadata.totalChapterCount` is also known, the UI renders\n`/ ch`.", - "example": 137.5 - }, - "localMaxVolume": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "description": "Highest `book_metadata.volume` across the books in this series.\n\n`None` when no book in the series has `volume` populated. When\nnon-null and `metadata.totalVolumeCount` is also known, the UI renders\n`/ vol` instead of the legacy\n`/ vol`.", - "example": 14 - }, - "path": { - "type": [ - "string", - "null" - ], - "description": "Filesystem path to the series directory", - "example": "/media/comics/Batman - Year One" - }, - "publisher": { - "type": [ - "string", - "null" - ], - "description": "Publisher name", - "example": "DC Comics" - }, - "selectedCoverSource": { - "type": [ - "string", - "null" - ], - "description": "Selected cover source (e.g., \"first_book\", \"custom\")", - "example": "first_book" - }, - "summary": { - "type": [ - "string", - "null" - ], - "description": "Summary/description from series_metadata", - "example": "The definitive origin story of Batman, following Bruce Wayne's first year as a vigilante." - }, - "title": { - "type": "string", - "description": "Series title from series_metadata", - "example": "Batman: Year One" - }, - "titleSort": { - "type": [ - "string", - "null" - ], - "description": "Sort title from series_metadata (for ordering)", - "example": "batman year one" - }, - "tracked": { - "type": "boolean", - "description": "Whether release tracking is enabled for this series.\n\nMirrors `series_tracking.tracked`. `false` when no tracking row exists.\nExposed so list views can surface a tracking indicator without an extra\nper-card request.", - "example": false - }, - "unreadCount": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Number of unread books in this series (user-specific)", - "example": 2 - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "When the series was last updated", - "example": "2024-01-15T10:30:00Z" - }, - "upstreamChapterGap": { - "type": [ - "number", - "null" - ], - "format": "float", - "description": "Difference between the upstream original-language chapter count\n(`series_metadata.total_chapter_count`, supplied by metadata\nproviders like MangaBaka or AniList) and the highest locally-owned\nchapter (`local_max_chapter`).\n\nAlways `None` unless the series is tracked AND `track_chapters` is\nenabled AND the provider count is populated AND the rounded-to-1-\ndecimal gap is positive. **This is an informational signal, not a\nrelease announcement**; the MangaUpdates plugin owns the\ntranslation-release feed.", - "example": 3.0 - }, - "upstreamGapProvider": { - "type": [ - "string", - "null" - ], - "description": "Display name of the metadata provider that supplied the upstream\ncounts (e.g., \"MangaBaka\", \"AniList\"). Set whenever at least one of\n`upstream_chapter_gap` / `upstream_volume_gap` is populated. Used by\nthe gap badge tooltip.", - "example": "MangaBaka" - }, - "upstreamVolumeGap": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "description": "Difference between the upstream original-language volume count\n(`series_metadata.total_volume_count`) and the highest locally-owned\nvolume (`local_max_volume`). Same suppression rules as\n`upstream_chapter_gap`, gated on `track_volumes`.", - "example": 1 - }, - "volumesOwned": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Number of books in this series classified as a complete volume\n(`volume IS NOT NULL AND chapter IS NULL`).\n\nDistinct from `bookCount`: a chapter inside a volume (`v15 c126`)\ncounts as a chapter, not a volume. `None` when no books exist;\n`Some(0)` when books exist but none are complete volumes.", - "example": 14 - }, - "wantToRead": { - "type": [ - "boolean", - "null" - ], - "description": "Whether the requesting user has this series in their want-to-read queue.\n\n`None` when not computed for this response (e.g. list endpoints that\ndon't enrich it); populated on the series detail endpoint.", - "example": false - }, - "year": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "description": "Release year", - "example": 1987 - } - } + "$ref": "#/components/schemas/SeriesDto" + }, + "description": "The data items for this page" + }, + "links": { + "$ref": "#/components/schemas/PaginationLinks", + "description": "HATEOAS navigation links" + }, + "page": { + "type": "integer", + "format": "int64", + "description": "Current page number (1-indexed)", + "example": 1, + "minimum": 0 + }, + "pageSize": { + "type": "integer", + "format": "int64", + "description": "Number of items per page", + "example": 50, + "minimum": 0 + }, + "total": { + "type": "integer", + "format": "int64", + "description": "Total number of items across all pages", + "example": 150, + "minimum": 0 + }, + "totalPages": { + "type": "integer", + "format": "int64", + "description": "Total number of pages", + "example": 3, + "minimum": 0 + } + } + }, + "PaginatedResponse_SeriesExternalIndexDto": { + "type": "object", + "description": "Generic paginated response wrapper with HATEOAS links", + "required": [ + "data", + "page", + "pageSize", + "total", + "totalPages", + "links" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesExternalIndexDto" }, "description": "The data items for this page" }, @@ -35376,63 +35272,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Sharing tag data transfer object", - "required": [ - "id", - "name", - "seriesCount", - "userCount", - "createdAt", - "updatedAt" - ], - "properties": { - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Creation timestamp", - "example": "2024-01-01T00:00:00Z" - }, - "description": { - "type": [ - "string", - "null" - ], - "description": "Optional description", - "example": "Content appropriate for children" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Unique sharing tag identifier", - "example": "550e8400-e29b-41d4-a716-446655440000" - }, - "name": { - "type": "string", - "description": "Display name of the sharing tag", - "example": "Kids Content" - }, - "seriesCount": { - "type": "integer", - "format": "int64", - "description": "Number of series tagged with this sharing tag", - "example": 42, - "minimum": 0 - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "Last update timestamp", - "example": "2024-01-15T10:30:00Z" - }, - "userCount": { - "type": "integer", - "format": "int64", - "description": "Number of users with grants for this sharing tag", - "example": 5, - "minimum": 0 - } - } + "$ref": "#/components/schemas/SharingTagDto" }, "description": "The data items for this page" }, @@ -35485,42 +35325,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Tag data transfer object", - "required": [ - "id", - "name", - "createdAt" - ], - "properties": { - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the tag was created", - "example": "2024-01-01T00:00:00Z" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Tag ID", - "example": "550e8400-e29b-41d4-a716-446655440020" - }, - "name": { - "type": "string", - "description": "Tag name", - "example": "Completed" - }, - "seriesCount": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Number of series with this tag", - "example": 15, - "minimum": 0 - } - } + "$ref": "#/components/schemas/TagDto" }, "description": "The data items for this page" }, @@ -35573,73 +35378,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "User data transfer object", - "required": [ - "id", - "username", - "email", - "role", - "permissions", - "isActive", - "createdAt", - "updatedAt" - ], - "properties": { - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Account creation timestamp", - "example": "2024-01-01T00:00:00Z" - }, - "email": { - "type": "string", - "description": "User email address", - "example": "john.doe@example.com" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Unique user identifier", - "example": "550e8400-e29b-41d4-a716-446655440000" - }, - "isActive": { - "type": "boolean", - "description": "Whether the account is active", - "example": true - }, - "lastLoginAt": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Timestamp of last login", - "example": "2024-01-15T10:30:00Z" - }, - "permissions": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Custom permissions that extend the role's base permissions" - }, - "role": { - "$ref": "#/components/schemas/UserRole", - "description": "User role (reader, maintainer, admin)" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "Last account update timestamp", - "example": "2024-01-15T10:30:00Z" - }, - "username": { - "type": "string", - "description": "Username for login", - "example": "johndoe" - } - } + "$ref": "#/components/schemas/UserDto" }, "description": "The data items for this page" }, @@ -40522,7 +40261,8 @@ "properties": { "full": { "type": "boolean", - "description": "Return full series data including metadata, locks, genres, tags, etc." + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", + "deprecated": true }, "libraryId": { "type": [ @@ -41835,10 +41575,7 @@ "description": "Request body for POST /series/list\n\nPagination parameters (page, pageSize, sort) are passed as query parameters,\nnot in the request body. This enables proper HATEOAS links.", "properties": { "condition": { - "type": [ - "object", - "null" - ], + "$ref": "#/components/schemas/SeriesCondition", "description": "Filter condition (optional - no condition returns all)" }, "fullTextSearch": { diff --git a/docs/package-lock.json b/docs/package-lock.json index 4c5858d1..dd0a5f5f 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8,68 +8,85 @@ "name": "docs", "version": "2.1.0", "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/preset-classic": "3.9.2", + "@docusaurus/core": "3.10.2", + "@docusaurus/faster": "^3.10.2", + "@docusaurus/preset-classic": "3.10.2", "@easyops-cn/docusaurus-search-local": "^0.55.0", "@mdx-js/react": "^3.1.1", "clsx": "^2.1.1", - "docusaurus-plugin-openapi-docs": "^4.7.1", - "docusaurus-theme-openapi-docs": "^4.7.1", + "docusaurus-plugin-openapi-docs": "^5.2.0", + "docusaurus-plugin-sass": "^0.2.6", + "docusaurus-theme-openapi-docs": "^5.2.0", "prism-react-renderer": "^2.4.1", "react": "^19.2.4", "react-dom": "^19.2.4" }, "devDependencies": { - "@docusaurus/module-type-aliases": "3.9.2", - "@docusaurus/tsconfig": "3.9.2", - "@docusaurus/types": "3.9.2", + "@docusaurus/module-type-aliases": "3.10.2", + "@docusaurus/tsconfig": "3.10.2", + "@docusaurus/types": "3.10.2", "typescript": "~5.9.3" }, "engines": { "node": ">=20.0" } }, + "node_modules/@11ty/gray-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@11ty/gray-matter/-/gray-matter-1.0.0.tgz", + "integrity": "sha512-7mJJl+wf1AByoT0PknQiQfOPnVNT4fevGrUBVWO4HXsnYn1aQPyRyrELYrNUFleUBM++KzMKN6QaxHPk0t/6/g==", + "license": "MIT", + "dependencies": { + "js-yaml": "^4.1.0", + "kind-of": "^6.0.3", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=11" + } + }, "node_modules/@algolia/abtesting": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.0.tgz", - "integrity": "sha512-cZfj+1Z1dgrk3YPtNQNt0H9Rr67P8b4M79JjUKGS0d7/EbFbGxGgSu6zby5f22KXo3LT0LZa4O2c6VVbupJuDg==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.23.0.tgz", + "integrity": "sha512-j45MBISstltys9QyQ4xf6quRiN1g7vMuwQL9VM4dx8YuRZvCQ173b9royZAx6iAbRX3IB1VnG1z//NuwyQ8jpQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0", - "@algolia/requester-browser-xhr": "5.48.0", - "@algolia/requester-fetch": "5.48.0", - "@algolia/requester-node-http": "5.48.0" + "@algolia/client-common": "5.57.0", + "@algolia/requester-browser-xhr": "5.57.0", + "@algolia/requester-fetch": "5.57.0", + "@algolia/requester-node-http": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/autocomplete-core": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", - "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.9.tgz", + "integrity": "sha512-4U2JKLMWlDu0CotYyUkWakDxr8AIav3QtIUXXRpfavYN29aVWfzlwJp9T0rPKEf/dO2QCPAUc0Kq1Tj1GJxo2A==", "license": "MIT", "dependencies": { - "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", - "@algolia/autocomplete-shared": "1.19.2" + "@algolia/autocomplete-plugin-algolia-insights": "1.19.9", + "@algolia/autocomplete-shared": "1.19.9" } }, "node_modules/@algolia/autocomplete-plugin-algolia-insights": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", - "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.9.tgz", + "integrity": "sha512-6mExC6X7762s2SV3eJy3QOkB8bdMmnUhQ2agvGVDuzwoGyr3PquGSY/0vPQXCfiAiCaXUz1rXn+lwghgSi0l0w==", "license": "MIT", "dependencies": { - "@algolia/autocomplete-shared": "1.19.2" + "@algolia/autocomplete-shared": "1.19.9" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, "node_modules/@algolia/autocomplete-shared": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", - "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.9.tgz", + "integrity": "sha512-YosP9Uoek6y/Ur1r1qeogk4biMe/hzkyNcgMCciw0//3XpCM7VlYLSHnyt/vOnEOGhCCc0+3v+unEiH6zz+Z1A==", "license": "MIT", "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", @@ -77,99 +94,99 @@ } }, "node_modules/@algolia/client-abtesting": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.0.tgz", - "integrity": "sha512-n17WSJ7vazmM6yDkWBAjY12J8ERkW9toOqNgQ1GEZu/Kc4dJDJod1iy+QP5T/UlR3WICgZDi/7a/VX5TY5LAPQ==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.57.0.tgz", + "integrity": "sha512-JVFFujiZUCguk5tz3LZr4fTQxqpIrj4/Jw3SI7kMljSqtfLxYn/s/TWH0J2s4iNfsDpxPhgFGMotCpmDI4kZ8w==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0", - "@algolia/requester-browser-xhr": "5.48.0", - "@algolia/requester-fetch": "5.48.0", - "@algolia/requester-node-http": "5.48.0" + "@algolia/client-common": "5.57.0", + "@algolia/requester-browser-xhr": "5.57.0", + "@algolia/requester-fetch": "5.57.0", + "@algolia/requester-node-http": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.0.tgz", - "integrity": "sha512-v5bMZMEqW9U2l40/tTAaRyn4AKrYLio7KcRuHmLaJtxuJAhvZiE7Y62XIsF070juz4MN3eyvfQmI+y5+OVbZuA==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.57.0.tgz", + "integrity": "sha512-6KqECK4ED3JJQEoDrQWnGPQzElA828xAD4qK5ceawNNyP/LcSvzAoLHjFkoTPksZ/kxj6VUtCRH+IHZesLltng==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0", - "@algolia/requester-browser-xhr": "5.48.0", - "@algolia/requester-fetch": "5.48.0", - "@algolia/requester-node-http": "5.48.0" + "@algolia/client-common": "5.57.0", + "@algolia/requester-browser-xhr": "5.57.0", + "@algolia/requester-fetch": "5.57.0", + "@algolia/requester-node-http": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.0.tgz", - "integrity": "sha512-7H3DgRyi7UByScc0wz7EMrhgNl7fKPDjKX9OcWixLwCj7yrRXDSIzwunykuYUUO7V7HD4s319e15FlJ9CQIIFQ==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.57.0.tgz", + "integrity": "sha512-uqpGF3oXYsoCbQq5d7BzNrNTfIfuvJyGP1CKvSW27T9boUg7KOwyxsAw1AX0a3jSW2HrYEJ/NN+Z4MiGivbpeQ==", "license": "MIT", "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-insights": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.0.tgz", - "integrity": "sha512-tXmkB6qrIGAXrtRYHQNpfW0ekru/qymV02bjT0w5QGaGw0W91yT+53WB6dTtRRsIrgS30Al6efBvyaEosjZ5uw==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.57.0.tgz", + "integrity": "sha512-u5NboJVJXDEFplvNnqqX4CxkXPYysjJRj47hOSh9329H8kG5gFLKJBIiS5utMQ+GZm8xQl3Te7NInDk6elEADQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0", - "@algolia/requester-browser-xhr": "5.48.0", - "@algolia/requester-fetch": "5.48.0", - "@algolia/requester-node-http": "5.48.0" + "@algolia/client-common": "5.57.0", + "@algolia/requester-browser-xhr": "5.57.0", + "@algolia/requester-fetch": "5.57.0", + "@algolia/requester-node-http": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.0.tgz", - "integrity": "sha512-4tXEsrdtcBZbDF73u14Kb3otN+xUdTVGop1tBjict+Rc/FhsJQVIwJIcTrOJqmvhtBfc56Bu65FiVOnpAZCxcw==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.57.0.tgz", + "integrity": "sha512-uzc0b2LmHAK9/QID4xeo35OG84AkZl4YewkCqawqAOGLjT2eZpM/OZx45ESygMHG30Ws+ZTSdluPtMJcUnrbWQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0", - "@algolia/requester-browser-xhr": "5.48.0", - "@algolia/requester-fetch": "5.48.0", - "@algolia/requester-node-http": "5.48.0" + "@algolia/client-common": "5.57.0", + "@algolia/requester-browser-xhr": "5.57.0", + "@algolia/requester-fetch": "5.57.0", + "@algolia/requester-node-http": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.0.tgz", - "integrity": "sha512-unzSUwWFpsDrO8935RhMAlyK0Ttua/5XveVIwzfjs5w+GVBsHgIkbOe8VbBJccMU/z1LCwvu1AY3kffuSLAR5Q==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.57.0.tgz", + "integrity": "sha512-dIAhnM6ue/ssa5PjgNfu4g8A4yTojl9ZOUzZU3wIaIKRerL2R/3Emuf9n/D6ICXXP167KC6XCeC7nliSw7cuSw==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0", - "@algolia/requester-browser-xhr": "5.48.0", - "@algolia/requester-fetch": "5.48.0", - "@algolia/requester-node-http": "5.48.0" + "@algolia/client-common": "5.57.0", + "@algolia/requester-browser-xhr": "5.57.0", + "@algolia/requester-fetch": "5.57.0", + "@algolia/requester-node-http": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.0.tgz", - "integrity": "sha512-RB9bKgYTVUiOcEb5bOcZ169jiiVW811dCsJoLT19DcbbFmU4QaK0ghSTssij35QBQ3SCOitXOUrHcGgNVwS7sQ==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.57.0.tgz", + "integrity": "sha512-2TTPTTKSJmCptvhCm4Xf3bBYMqZni+Pgc2hVdqc4l9wsBpSJNVTVIKpnd10OubUgkGcmppVDj1XQqYaf6EnPSQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0", - "@algolia/requester-browser-xhr": "5.48.0", - "@algolia/requester-fetch": "5.48.0", - "@algolia/requester-node-http": "5.48.0" + "@algolia/client-common": "5.57.0", + "@algolia/requester-browser-xhr": "5.57.0", + "@algolia/requester-fetch": "5.57.0", + "@algolia/requester-node-http": "5.57.0" }, "engines": { "node": ">= 14.0.0" @@ -182,110 +199,140 @@ "license": "MIT" }, "node_modules/@algolia/ingestion": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.0.tgz", - "integrity": "sha512-rhoSoPu+TDzDpvpk3cY/pYgbeWXr23DxnAIH/AkN0dUC+GCnVIeNSQkLaJ+CL4NZ51cjLIjksrzb4KC5Xu+ktw==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.57.0.tgz", + "integrity": "sha512-W4JseHKt+pzOxlFV+T3MWEG0h4Z2Se5zjoXUD0ewlw8aOWMG/yjRdopUdLQsXULepB/My2tDuZjkwk2sMfsUrQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0", - "@algolia/requester-browser-xhr": "5.48.0", - "@algolia/requester-fetch": "5.48.0", - "@algolia/requester-node-http": "5.48.0" + "@algolia/client-common": "5.57.0", + "@algolia/requester-browser-xhr": "5.57.0", + "@algolia/requester-fetch": "5.57.0", + "@algolia/requester-node-http": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.0.tgz", - "integrity": "sha512-aSe6jKvWt+8VdjOaq2ERtsXp9+qMXNJ3mTyTc1VMhNfgPl7ArOhRMRSQ8QBnY8ZL4yV5Xpezb7lAg8pdGrrulg==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.57.0.tgz", + "integrity": "sha512-BrxJVE0/eLinEPICCD7BKN/2xnt0nkjge70u8zzE2ISP3fuB3tjLgcwpanUycvlHBFLI4gK0l5ol54p6IYuR/Q==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0", - "@algolia/requester-browser-xhr": "5.48.0", - "@algolia/requester-fetch": "5.48.0", - "@algolia/requester-node-http": "5.48.0" + "@algolia/client-common": "5.57.0", + "@algolia/requester-browser-xhr": "5.57.0", + "@algolia/requester-fetch": "5.57.0", + "@algolia/requester-node-http": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.0.tgz", - "integrity": "sha512-p9tfI1bimAaZrdiVExL/dDyGUZ8gyiSHsktP1ZWGzt5hXpM3nhv4tSjyHtXjEKtA0UvsaHKwSfFE8aAAm1eIQA==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.57.0.tgz", + "integrity": "sha512-Gc29jkeiLKlVfHvyrIgyUHHE+aYTdXEeLfK42rjr5/1TTVsYwUJz0XkvoIBIqfMjcDg6gXeHb1jTUZ0H+SYIlQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0", - "@algolia/requester-browser-xhr": "5.48.0", - "@algolia/requester-fetch": "5.48.0", - "@algolia/requester-node-http": "5.48.0" + "@algolia/client-common": "5.57.0", + "@algolia/requester-browser-xhr": "5.57.0", + "@algolia/requester-fetch": "5.57.0", + "@algolia/requester-node-http": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.0.tgz", - "integrity": "sha512-XshyfpsQB7BLnHseMinp3fVHOGlTv6uEHOzNK/3XrEF9mjxoZAcdVfY1OCXObfwRWX5qXZOq8FnrndFd44iVsQ==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.57.0.tgz", + "integrity": "sha512-PIPnPN7MP3fp2VAi01BVXhCWmD366ZB2Hkq5TlYKtThd4KxUtMmaaNDpFgVCTXtSIqWVZLJntOHRvxg/sIPd8Q==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0" + "@algolia/client-common": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.0.tgz", - "integrity": "sha512-Q4XNSVQU89bKNAPuvzSYqTH9AcbOOiIo6AeYMQTxgSJ2+uvT78CLPMG89RIIloYuAtSfE07s40OLV50++l1Bbw==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.57.0.tgz", + "integrity": "sha512-AX3RlOudXMdTwtwUqdAf5hAVLvXfOZZH1FZh6ALDdrhVLT0TtAIe48N6nYcWcTnwoTxK/wDIQqZ19IMjK8zJAA==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0" + "@algolia/client-common": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.0.tgz", - "integrity": "sha512-ZgxV2+5qt3NLeUYBTsi6PLyHcENQWC0iFppFZekHSEDA2wcLdTUjnaJzimTEULHIvJuLRCkUs4JABdhuJktEag==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.57.0.tgz", + "integrity": "sha512-cWZc1dKb7wy9/wPpwMtL1y89gK2G7y2A47Coa7zwf1ydtIeJm4+S+XxoQ2b/ZRiQnrC1YHavLjYUPDCdnT7Khg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.0" + "@algolia/client-common": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", - "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-15.5.2.tgz", + "integrity": "sha512-B+C9Ok0DF/rjANIUHgwcV5/d4C72MB7f2IbKFL8jDGcGq2qn3yr893s8vAn2kbhmyaelAin0JK8EKF9P1+y7aQ==", "license": "MIT", "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.0" + "js-yaml": "^5.2.2", + "undici": "^6.28.0" }, "engines": { - "node": ">= 16" + "node": ">=20" }, - "funding": { - "url": "https://github.com/sponsors/philsturgeon" + "peerDependencies": { + "@types/json-schema": "^7.0.15" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.3.0.tgz", + "integrity": "sha512-muutsYr+e2+d3rTgUGslq5rxbBlUy3cJ61IsHag2QNDQV+7zXWjkUpmALIajhrlLlrgRUiymj6U3zUr/TMK84Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/undici": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "license": "MIT", + "engines": { + "node": ">=18.17" } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -294,29 +341,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -342,13 +389,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -358,25 +405,25 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -395,17 +442,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "engines": { @@ -425,12 +472,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, @@ -451,9 +498,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", - "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", @@ -467,49 +514,49 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -519,35 +566,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -557,14 +604,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -574,79 +621,79 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -656,13 +703,13 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", - "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -672,12 +719,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -687,12 +734,28 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -702,14 +765,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -719,13 +782,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", - "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/traverse": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -759,12 +822,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", - "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -774,12 +837,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -789,12 +852,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -804,12 +867,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -835,12 +898,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -850,14 +913,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", - "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.29.0" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -867,14 +930,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", - "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -884,12 +947,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -899,12 +962,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -914,13 +977,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -930,13 +993,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", - "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -946,17 +1009,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -966,13 +1029,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -982,13 +1045,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -998,13 +1061,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", - "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1014,12 +1077,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1029,13 +1092,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1045,12 +1108,12 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1060,13 +1123,13 @@ } }, "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", - "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1076,12 +1139,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", - "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1091,12 +1154,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1106,13 +1169,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1122,14 +1185,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1139,12 +1202,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", - "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1154,12 +1217,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1169,12 +1232,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1184,12 +1247,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1199,13 +1262,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1215,13 +1278,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1231,15 +1294,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz", + "integrity": "sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.8" }, "engines": { "node": ">=6.9.0" @@ -1249,13 +1312,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1265,13 +1328,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1281,12 +1344,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1296,12 +1359,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1311,12 +1374,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1326,16 +1389,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.6" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1345,13 +1408,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1361,12 +1424,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1376,13 +1439,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1392,12 +1455,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1407,13 +1470,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1423,14 +1486,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1440,12 +1503,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1455,12 +1518,12 @@ } }, "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", - "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.29.7.tgz", + "integrity": "sha512-J0wGhKan+rIiE2OhfhRptySLrJ6SjQYM6b6N1FMlhyhCcw1Mig8vQjWchyB+bgHGDvaWo6Diu6CLRMra2uMtmg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1470,12 +1533,12 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", + "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1485,16 +1548,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", - "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-syntax-jsx": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1504,12 +1567,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", - "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz", + "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", "license": "MIT", "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.27.1" + "@babel/plugin-transform-react-jsx": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1519,13 +1582,13 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", - "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", + "integrity": "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1535,12 +1598,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", - "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz", + "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1550,13 +1613,13 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", - "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1566,12 +1629,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1581,13 +1644,13 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", - "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", @@ -1610,12 +1673,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1625,13 +1688,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz", + "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1641,12 +1704,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1656,12 +1719,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1671,12 +1734,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1686,16 +1749,16 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", - "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1705,12 +1768,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1720,13 +1783,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", - "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1736,13 +1799,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1752,13 +1815,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", - "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1768,75 +1831,76 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", - "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.28.6", - "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.29.0", - "@babel/plugin-transform-async-to-generator": "^7.28.6", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.6", - "@babel/plugin-transform-class-properties": "^7.28.6", - "@babel/plugin-transform-class-static-block": "^7.28.6", - "@babel/plugin-transform-classes": "^7.28.6", - "@babel/plugin-transform-computed-properties": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-dotall-regex": "^7.28.6", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.6", - "@babel/plugin-transform-exponentiation-operator": "^7.28.6", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.28.6", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@babel/plugin-transform-modules-systemjs": "^7.29.0", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", - "@babel/plugin-transform-numeric-separator": "^7.28.6", - "@babel/plugin-transform-object-rest-spread": "^7.28.6", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.28.6", - "@babel/plugin-transform-optional-chaining": "^7.28.6", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.28.6", - "@babel/plugin-transform-private-property-in-object": "^7.28.6", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.29.0", - "@babel/plugin-transform-regexp-modifiers": "^7.28.6", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.28.6", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.28.6", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.15", "babel-plugin-polyfill-corejs3": "^0.14.0", @@ -1852,12 +1916,12 @@ } }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz", - "integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", "core-js-compat": "^3.48.0" }, "peerDependencies": { @@ -1888,17 +1952,17 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", - "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz", + "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-transform-react-display-name": "^7.28.0", - "@babel/plugin-transform-react-jsx": "^7.27.1", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-react-display-name": "^7.29.7", + "@babel/plugin-transform-react-jsx": "^7.29.7", + "@babel/plugin-transform-react-jsx-development": "^7.29.7", + "@babel/plugin-transform-react-pure-annotations": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1908,16 +1972,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", - "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1935,44 +1999,32 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.0.tgz", - "integrity": "sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==", - "license": "MIT", - "dependencies": { - "core-js-pure": "^3.48.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -1980,13 +2032,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2236,9 +2288,9 @@ } }, "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -2657,9 +2709,9 @@ } }, "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -3104,9 +3156,9 @@ } }, "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -3328,9 +3380,9 @@ } }, "node_modules/@docsearch/core": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.5.4.tgz", - "integrity": "sha512-DbkfZbJyYAPFJtF71eAFOTQSy5z5c/hdSN0UrErORKDwXKLTJBR0c+5WxE5l+IKZx4xIaEa8RkrL7T28DTCOYw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.7.0.tgz", + "integrity": "sha512-p/9xVKmPDj3FPvMfPf5naVO3Ej8SCbcUugGvx1+8GgkuBNbqxqN2Irx3WLBv8VY0jH7XpRwKWdlmjXLZsmTLsg==", "license": "MIT", "peerDependencies": { "@types/react": ">= 16.8.0 < 20.0.0", @@ -3350,20 +3402,20 @@ } }, "node_modules/@docsearch/css": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.5.4.tgz", - "integrity": "sha512-gzO4DJwyM9c4YEPHwaLV1nUCDC2N6yoh0QJj44dce2rcfN71mB+jpu3+F+Y/KMDF1EKV0C3m54leSWsraE94xg==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.7.0.tgz", + "integrity": "sha512-Sk5xkdRFeE7PeWjG9l4AfTwdvMfr9wHiwNNCpHXT4v4SNyNMKdHGvEILc31BgaVFGDDNbv5u/a73tofRiwbEZw==", "license": "MIT" }, "node_modules/@docsearch/react": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.5.4.tgz", - "integrity": "sha512-iBNFfvWoUFRUJmGQ/r+0AEp2OJgJMoYIKRiRcTDON0hObBRSLlrv2ktb7w3nc1MeNm1JIpbPA99i59TiIR49fA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.7.0.tgz", + "integrity": "sha512-x6oedjJ8O8/pIDBsMo5Orca3/6cQCz616/CwthVe68l43mqnj2lrJ9kFQITBqy8hMsS3nWeBWFoVO5dJ1DCFKA==", "license": "MIT", "dependencies": { "@algolia/autocomplete-core": "1.19.2", - "@docsearch/core": "4.5.4", - "@docsearch/css": "4.5.4" + "@docsearch/core": "4.7.0", + "@docsearch/css": "4.7.0" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 20.0.0", @@ -3386,10 +3438,42 @@ } } }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-core": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", + "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", + "@algolia/autocomplete-shared": "1.19.2" + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", + "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.2" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-shared": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", + "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, "node_modules/@docusaurus/babel": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.9.2.tgz", - "integrity": "sha512-GEANdi/SgER+L7Japs25YiGil/AUDnFFHaCGPBbundxoWtCkA2lmy7/tFmgED4y1htAy6Oi4wkJEQdGssnw9MA==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.10.2.tgz", + "integrity": "sha512-aJ1hpGyvfkte3dDAfNbWM4biW4yWZBVz7TIGLZP+v+tWOBgxX3e0N5ZIXHIvmfNNXTI77pcHUx3KmtOk05Ze3Q==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.9", @@ -3400,10 +3484,9 @@ "@babel/preset-react": "^7.25.9", "@babel/preset-typescript": "^7.25.9", "@babel/runtime": "^7.25.9", - "@babel/runtime-corejs3": "^7.25.9", "@babel/traverse": "^7.25.9", - "@docusaurus/logger": "3.9.2", - "@docusaurus/utils": "3.9.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/utils": "3.10.2", "babel-plugin-dynamic-import-node": "^2.3.3", "fs-extra": "^11.1.1", "tslib": "^2.6.0" @@ -3413,17 +3496,17 @@ } }, "node_modules/@docusaurus/bundler": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.9.2.tgz", - "integrity": "sha512-ZOVi6GYgTcsZcUzjblpzk3wH1Fya2VNpd5jtHoCCFcJlMQ1EYXZetfAnRHLcyiFeBABaI1ltTYbOBtH/gahGVA==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.10.2.tgz", + "integrity": "sha512-i0ZNcy0f0WhaOlYVgzLsWhIoEXO9kS3HRoKPtgE6vQtZUq7arKZaYdNBudr3mqCmd+TyOkwtwfHgs1ENj07r5g==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.9", - "@docusaurus/babel": "3.9.2", - "@docusaurus/cssnano-preset": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", + "@docusaurus/babel": "3.10.2", + "@docusaurus/cssnano-preset": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", "babel-loader": "^9.2.1", "clean-css": "^5.3.3", "copy-webpack-plugin": "^11.0.0", @@ -3441,7 +3524,7 @@ "tslib": "^2.6.0", "url-loader": "^4.1.1", "webpack": "^5.95.0", - "webpackbar": "^6.0.1" + "webpackbar": "^7.0.0" }, "engines": { "node": ">=20.0" @@ -3456,18 +3539,18 @@ } }, "node_modules/@docusaurus/core": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.9.2.tgz", - "integrity": "sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw==", - "license": "MIT", - "dependencies": { - "@docusaurus/babel": "3.9.2", - "@docusaurus/bundler": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/mdx-loader": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.10.2.tgz", + "integrity": "sha512-EYByj6nk+aD9KeVxV6Hmo2/nAAT79P21Y82ycTBOBtrmqilloIbIEhgL2/8Xpt2Jz/pgNqHAwyusOGwmbKeJmA==", + "license": "MIT", + "dependencies": { + "@docusaurus/babel": "3.10.2", + "@docusaurus/bundler": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/mdx-loader": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "boxen": "^6.2.1", "chalk": "^4.1.2", "chokidar": "^3.5.3", @@ -3475,11 +3558,11 @@ "combine-promises": "^1.1.0", "commander": "^5.1.0", "core-js": "^3.31.1", - "detect-port": "^1.5.1", + "detect-port": "^2.1.0", "escape-html": "^1.0.3", "eta": "^2.2.0", "eval": "^0.1.8", - "execa": "5.1.1", + "execa": "^5.1.1", "fs-extra": "^11.1.1", "html-tags": "^3.3.1", "html-webpack-plugin": "^5.6.0", @@ -3490,12 +3573,12 @@ "prompts": "^2.4.2", "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", + "react-loadable-ssr-addon-v5-slorber": "^1.0.3", "react-router": "^5.3.4", "react-router-config": "^5.1.1", "react-router-dom": "^5.3.4", "semver": "^7.5.4", - "serve-handler": "^6.1.6", + "serve-handler": "^6.1.7", "tinypool": "^1.0.2", "tslib": "^2.6.0", "update-notifier": "^6.0.2", @@ -3511,15 +3594,21 @@ "node": ">=20.0" }, "peerDependencies": { + "@docusaurus/faster": "*", "@mdx-js/react": "^3.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@docusaurus/faster": { + "optional": true + } } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.9.2.tgz", - "integrity": "sha512-8gBKup94aGttRduABsj7bpPFTX7kbwu+xh3K9NMCF5K4bWBqTFYW+REKHF6iBVDHRJ4grZdIPbvkiHd/XNKRMQ==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.10.2.tgz", + "integrity": "sha512-4gCnHRbJLTloiwfvFAa92tgb2gI4KYhvjfQVYnEaiMO/EgvWfCo1LwytHXen+1oZAN0VAlS0JAPxp3MsvKDa3A==", "license": "MIT", "dependencies": { "cssnano-preset-advanced": "^6.1.2", @@ -3531,10 +3620,34 @@ "node": ">=20.0" } }, + "node_modules/@docusaurus/faster": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/faster/-/faster-3.10.2.tgz", + "integrity": "sha512-p/5E5/RyHv+QWusJMPN5i3OMJTqTgkhuwzVbB1AReDWTUHXQCmf5mlTFzGiDrWeQWIDOKsuOPn1jJh0s9LUOHA==", + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.10.2", + "@rspack/core": "^1.7.10", + "@swc/core": "^1.15.40", + "@swc/html": "^1.15.40", + "browserslist": "^4.24.2", + "lightningcss": "^1.27.0", + "semver": "^7.5.4", + "swc-loader": "^0.2.6", + "tslib": "^2.6.0", + "webpack": "^5.95.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/types": "*" + } + }, "node_modules/@docusaurus/logger": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.9.2.tgz", - "integrity": "sha512-/SVCc57ByARzGSU60c50rMyQlBuMIJCjcsJlkphxY6B0GV4UH3tcA1994N8fFfbJ9kX3jIBe/xg3XP5qBtGDbA==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.10.2.tgz", + "integrity": "sha512-gSEwqtPfCAnC3ZSJY6xL7tcIfgg0vFD39jbv93eakuweyvO2864xR0K+kmKwBhkTCtWRNjuGGnb5rdmkD/ndqw==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", @@ -3545,14 +3658,14 @@ } }, "node_modules/@docusaurus/mdx-loader": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.9.2.tgz", - "integrity": "sha512-wiYoGwF9gdd6rev62xDU8AAM8JuLI/hlwOtCzMmYcspEkzecKrP8J8X+KpYnTlACBUUtXNJpSoCwFWJhLRevzQ==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.10.2.tgz", + "integrity": "sha512-9Fd4V/SFjfrVQ0JH5EN0+iPWyFunvTeQE3gfyFeetqPaXMP0OylIjOw16dCuXG4NZJrYdBqwzjh18/h3gRi47w==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "@mdx-js/mdx": "^3.0.0", "@slorber/remark-comment": "^1.0.0", "escape-html": "^1.0.3", @@ -3584,12 +3697,12 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.9.2.tgz", - "integrity": "sha512-8qVe2QA9hVLzvnxP46ysuofJUIc/yYQ82tvA/rBTrnpXtCjNSFLxEZfd5U8cYZuJIVlkPxamsIgwd5tGZXfvew==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.10.2.tgz", + "integrity": "sha512-h/I5e4jaAhDHW4vaLENi1i2hnOEnXY1t9R+nnRTbgUl7ymVRzN/HF7dDfj8rKYGj8gfIge+Ef+iYRAMtbGvsrQ==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.9.2", + "@docusaurus/types": "3.10.2", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -3603,20 +3716,21 @@ } }, "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.9.2.tgz", - "integrity": "sha512-3I2HXy3L1QcjLJLGAoTvoBnpOwa6DPUa3Q0dMK19UTY9mhPkKQg/DYhAGTiBUKcTR0f08iw7kLPqOhIgdV3eVQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/mdx-loader": "3.9.2", - "@docusaurus/theme-common": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.10.2.tgz", + "integrity": "sha512-0cbEnNKf0InmLkhj/+nVRmqEnWEoOE8Mh+2x1qOXI0qYpCnphq4RXknVJ8BvybKRXqYVvbmdMfiJSup+k4tm5w==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/mdx-loader": "3.10.2", + "@docusaurus/theme-common": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "cheerio": "1.0.0-rc.12", + "combine-promises": "^1.1.0", "feed": "^4.2.2", "fs-extra": "^11.1.1", "lodash": "^4.17.21", @@ -3637,20 +3751,20 @@ } }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", - "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/mdx-loader": "3.9.2", - "@docusaurus/module-type-aliases": "3.9.2", - "@docusaurus/theme-common": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.2.tgz", + "integrity": "sha512-Sqwl4FPoZBDrlY8I2VU2H8O0M91CHp9T8ToMSkTZmjvHCif+1laqfXi6sTk8IfyVS/trN5yNjcWd1bFsGB6W5Q==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/mdx-loader": "3.10.2", + "@docusaurus/module-type-aliases": "3.10.2", + "@docusaurus/theme-common": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "@types/react-router-config": "^5.0.7", "combine-promises": "^1.1.0", "fs-extra": "^11.1.1", @@ -3670,16 +3784,16 @@ } }, "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.9.2.tgz", - "integrity": "sha512-s4849w/p4noXUrGpPUF0BPqIAfdAe76BLaRGAGKZ1gTDNiGxGcpsLcwJ9OTi1/V8A+AzvsmI9pkjie2zjIQZKA==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.10.2.tgz", + "integrity": "sha512-h5R12sZ/vV9EPiVjvIl9YFCOwkpwXes7dQMYt3EvP6Pphu4amHxxTqWxf08Fl5DR8h+oZMbWpFTNw5vKEYfvzQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/mdx-loader": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", + "@docusaurus/core": "3.10.2", + "@docusaurus/mdx-loader": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "fs-extra": "^11.1.1", "tslib": "^2.6.0", "webpack": "^5.88.1" @@ -3693,15 +3807,15 @@ } }, "node_modules/@docusaurus/plugin-css-cascade-layers": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.9.2.tgz", - "integrity": "sha512-w1s3+Ss+eOQbscGM4cfIFBlVg/QKxyYgj26k5AnakuHkKxH6004ZtuLe5awMBotIYF2bbGDoDhpgQ4r/kcj4rQ==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.10.2.tgz", + "integrity": "sha512-UkdvQby5OQUKWrw3lLnSTJXQ6VETaUVTuPQX9AABtmFm5h+ifEBx1OQ+LN726Q4byuwBf2ElHkf4qU4hTxdvRg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", + "@docusaurus/core": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "tslib": "^2.6.0" }, "engines": { @@ -3709,14 +3823,14 @@ } }, "node_modules/@docusaurus/plugin-debug": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.9.2.tgz", - "integrity": "sha512-j7a5hWuAFxyQAkilZwhsQ/b3T7FfHZ+0dub6j/GxKNFJp2h9qk/P1Bp7vrGASnvA9KNQBBL1ZXTe7jlh4VdPdA==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.10.2.tgz", + "integrity": "sha512-8vbZNOSCpnsT57EY6CgN7sgRVmx3KTYwO8Uvo2pbxOyb8tbqAwtT9SslqaQ41HbA1v1hpn5RP7u5s2KvRwAFpQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", + "@docusaurus/core": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", "fs-extra": "^11.1.1", "react-json-view-lite": "^2.3.0", "tslib": "^2.6.0" @@ -3730,14 +3844,14 @@ } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.9.2.tgz", - "integrity": "sha512-mAwwQJ1Us9jL/lVjXtErXto4p4/iaLlweC54yDUK1a97WfkC6Z2k5/769JsFgwOwOP+n5mUQGACXOEQ0XDuVUw==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.10.2.tgz", + "integrity": "sha512-kMHMBK9j4VAtgd5owwrRLRIi0EjkrpXlX7ePj1+y68XfVZV9I1T4S+koPDm+Hfw2TtnyHvh0uNrDvjz+DjQGVA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", + "@docusaurus/core": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "tslib": "^2.6.0" }, "engines": { @@ -3749,15 +3863,14 @@ } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.9.2.tgz", - "integrity": "sha512-YJ4lDCphabBtw19ooSlc1MnxtYGpjFV9rEdzjLsUnBCeis2djUyCozZaFhCg6NGEwOn7HDDyMh0yzcdRpnuIvA==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.10.2.tgz", + "integrity": "sha512-Vt90nNFhtAChRe9+it1hcHFgFvETdSnOkL5Bma+p6E/yU2tAYrvvyk+gv+LJGM2ZUkyKuKXLRsZ2Lb0bO7+Vog==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "@types/gtag.js": "^0.0.12", + "@docusaurus/core": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "tslib": "^2.6.0" }, "engines": { @@ -3769,14 +3882,14 @@ } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.9.2.tgz", - "integrity": "sha512-LJtIrkZN/tuHD8NqDAW1Tnw0ekOwRTfobWPsdO15YxcicBo2ykKF0/D6n0vVBfd3srwr9Z6rzrIWYrMzBGrvNw==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.10.2.tgz", + "integrity": "sha512-MLCffCldysi/R0nzJQP7ZWd0xAoGNnSTiVOo6TTR6mKVGFhE+/XArGe67ZcaZv1uytgQXoXs92VJrgVDrz80rQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", + "@docusaurus/core": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "tslib": "^2.6.0" }, "engines": { @@ -3788,17 +3901,17 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.9.2.tgz", - "integrity": "sha512-WLh7ymgDXjG8oPoM/T4/zUP7KcSuFYRZAUTl8vR6VzYkfc18GBM4xLhcT+AKOwun6kBivYKUJf+vlqYJkm+RHw==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.10.2.tgz", + "integrity": "sha512-PODkwg5XetLML3hU/3xpCKJUZ9cqExLaBnD/Fzzwj2VHogLeqnDisLIujae87zuze7T4mCm2A6KEqZkyiz07EQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", + "@docusaurus/core": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "fs-extra": "^11.1.1", "sitemap": "^7.1.1", "tslib": "^2.6.0" @@ -3812,15 +3925,15 @@ } }, "node_modules/@docusaurus/plugin-svgr": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.9.2.tgz", - "integrity": "sha512-n+1DE+5b3Lnf27TgVU5jM1d4x5tUh2oW5LTsBxJX4PsAPV0JGcmI6p3yLYtEY0LRVEIJh+8RsdQmRE66wSV8mw==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.10.2.tgz", + "integrity": "sha512-JgfT3jWM0TJ8Uw0cEcqxHpybngQY1vlBYpuuNO+gEh5iPh5Ar+vxq/u9CFrYsWeXy48BN7Db76Pzp2edNXUQ8A==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", + "@docusaurus/core": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "@svgr/core": "8.1.0", "@svgr/webpack": "^8.1.0", "tslib": "^2.6.0", @@ -3835,26 +3948,26 @@ } }, "node_modules/@docusaurus/preset-classic": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.9.2.tgz", - "integrity": "sha512-IgyYO2Gvaigi21LuDIe+nvmN/dfGXAiMcV/murFqcpjnZc7jxFAxW+9LEjdPt61uZLxG4ByW/oUmX/DDK9t/8w==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/plugin-content-blog": "3.9.2", - "@docusaurus/plugin-content-docs": "3.9.2", - "@docusaurus/plugin-content-pages": "3.9.2", - "@docusaurus/plugin-css-cascade-layers": "3.9.2", - "@docusaurus/plugin-debug": "3.9.2", - "@docusaurus/plugin-google-analytics": "3.9.2", - "@docusaurus/plugin-google-gtag": "3.9.2", - "@docusaurus/plugin-google-tag-manager": "3.9.2", - "@docusaurus/plugin-sitemap": "3.9.2", - "@docusaurus/plugin-svgr": "3.9.2", - "@docusaurus/theme-classic": "3.9.2", - "@docusaurus/theme-common": "3.9.2", - "@docusaurus/theme-search-algolia": "3.9.2", - "@docusaurus/types": "3.9.2" + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.10.2.tgz", + "integrity": "sha512-a4B3VczmDl99zK0EufDQYomdJ186WDingjmDXxhN2PNPS9Ty/Y2M5CLFX1KQMRKqRTLiRDKfutzG5IY1FC/ceg==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/plugin-content-blog": "3.10.2", + "@docusaurus/plugin-content-docs": "3.10.2", + "@docusaurus/plugin-content-pages": "3.10.2", + "@docusaurus/plugin-css-cascade-layers": "3.10.2", + "@docusaurus/plugin-debug": "3.10.2", + "@docusaurus/plugin-google-analytics": "3.10.2", + "@docusaurus/plugin-google-gtag": "3.10.2", + "@docusaurus/plugin-google-tag-manager": "3.10.2", + "@docusaurus/plugin-sitemap": "3.10.2", + "@docusaurus/plugin-svgr": "3.10.2", + "@docusaurus/theme-classic": "3.10.2", + "@docusaurus/theme-common": "3.10.2", + "@docusaurus/theme-search-algolia": "3.10.2", + "@docusaurus/types": "3.10.2" }, "engines": { "node": ">=20.0" @@ -3865,26 +3978,27 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.9.2.tgz", - "integrity": "sha512-IGUsArG5hhekXd7RDb11v94ycpJpFdJPkLnt10fFQWOVxAtq5/D7hT6lzc2fhyQKaaCE62qVajOMKL7OiAFAIA==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/mdx-loader": "3.9.2", - "@docusaurus/module-type-aliases": "3.9.2", - "@docusaurus/plugin-content-blog": "3.9.2", - "@docusaurus/plugin-content-docs": "3.9.2", - "@docusaurus/plugin-content-pages": "3.9.2", - "@docusaurus/theme-common": "3.9.2", - "@docusaurus/theme-translations": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.10.2.tgz", + "integrity": "sha512-JqTSLQmqmA9uKWZsD5iwBGJ4JyKB4/yTw6PsSXVPRJG/6GAm/u+add9Iip+hvwP12/AnPNztrdxsI14NJW4KeA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/mdx-loader": "3.10.2", + "@docusaurus/module-type-aliases": "3.10.2", + "@docusaurus/plugin-content-blog": "3.10.2", + "@docusaurus/plugin-content-docs": "3.10.2", + "@docusaurus/plugin-content-pages": "3.10.2", + "@docusaurus/theme-common": "3.10.2", + "@docusaurus/theme-translations": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "copy-text-to-clipboard": "^3.2.0", "infima": "0.2.0-alpha.45", "lodash": "^4.17.21", "nprogress": "^0.2.0", @@ -3905,15 +4019,15 @@ } }, "node_modules/@docusaurus/theme-common": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", - "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.10.2.tgz", + "integrity": "sha512-R9b/vMpK1yye6hNZTA6x/ivRv+at6GhxnXcxkpzCGzO1R1RwiquqiFg2wMFh6aqlJTpWRFKpFD2TzCDQcyOU0A==", "license": "MIT", "dependencies": { - "@docusaurus/mdx-loader": "3.9.2", - "@docusaurus/module-type-aliases": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", + "@docusaurus/mdx-loader": "3.10.2", + "@docusaurus/module-type-aliases": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -3933,19 +4047,20 @@ } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz", - "integrity": "sha512-GBDSFNwjnh5/LdkxCKQHkgO2pIMX1447BxYUBG2wBiajS21uj64a+gH/qlbQjDLxmGrbrllBrtJkUHxIsiwRnw==", - "license": "MIT", - "dependencies": { - "@docsearch/react": "^3.9.0 || ^4.1.0", - "@docusaurus/core": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/plugin-content-docs": "3.9.2", - "@docusaurus/theme-common": "3.9.2", - "@docusaurus/theme-translations": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.10.2.tgz", + "integrity": "sha512-1msxllyhi/5m77JukXtp5UFnUAriwZIC1oJ7MTnpQpCwLTbclJi5BK5n28CTZuSXpQN2ewbbnqRgAhMM6c6ihg==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "^1.19.2", + "@docsearch/react": "^3.9.0 || ^4.3.2", + "@docusaurus/core": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/plugin-content-docs": "3.10.2", + "@docusaurus/theme-common": "3.10.2", + "@docusaurus/theme-translations": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", "algoliasearch": "^5.37.0", "algoliasearch-helper": "^3.26.0", "clsx": "^2.0.0", @@ -3964,9 +4079,9 @@ } }, "node_modules/@docusaurus/theme-translations": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.9.2.tgz", - "integrity": "sha512-vIryvpP18ON9T9rjgMRFLr2xJVDpw1rtagEGf8Ccce4CkTrvM/fRB8N2nyWYOW5u3DdjkwKw5fBa+3tbn9P4PA==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.10.2.tgz", + "integrity": "sha512-iv20wrxnyXkY89LM3TzRlzGlt5fIGO5UnaR6UL1ZVfB9RRFjxQFQ6awDrwAc6Km8Y5gD8pInuwYPF+6/TiCxXA==", "license": "MIT", "dependencies": { "fs-extra": "^11.1.1", @@ -3977,16 +4092,16 @@ } }, "node_modules/@docusaurus/tsconfig": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.9.2.tgz", - "integrity": "sha512-j6/Fp4Rlpxsc632cnRnl5HpOWeb6ZKssDj6/XzzAzVGXXfm9Eptx3rxCC+fDzySn9fHTS+CWJjPineCR1bB5WQ==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.10.2.tgz", + "integrity": "sha512-5GiB7h/nFsMFPO9mCqcRNE1yA5TSXXNCshNIgHPL6fCPOjcTDixs6qjQBu8ddkgPcicwCvOA7n3jeK2rGdJk6g==", "dev": true, "license": "MIT" }, "node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.10.2.tgz", + "integrity": "sha512-B6rvfwIFSapUqUJjMriZswX13K8l5Z7AcmVE6uTEJpYddQieSTR12DsGaFtcZAIDsQd4p+0WTl0Vc6jmZK0Trw==", "license": "MIT", "dependencies": { "@mdx-js/mdx": "^3.0.0", @@ -4020,21 +4135,21 @@ } }, "node_modules/@docusaurus/utils": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.9.2.tgz", - "integrity": "sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.10.2.tgz", + "integrity": "sha512-xx0W3eav2uW1NRIpuHJWNwLTC15xPNjU4Uxi9NSnd3swYC96BE3vFiT93SD8s24kmAAWNwgZwfZ2fghGZ01Lcw==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils-common": "3.9.2", + "@11ty/gray-matter": "^1.0.0", + "@docusaurus/logger": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils-common": "3.10.2", "escape-string-regexp": "^4.0.0", - "execa": "5.1.1", + "execa": "^5.1.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", "github-slugger": "^1.5.0", "globby": "^11.1.0", - "gray-matter": "^4.0.3", "jiti": "^1.20.0", "js-yaml": "^4.1.0", "lodash": "^4.17.21", @@ -4052,12 +4167,12 @@ } }, "node_modules/@docusaurus/utils-common": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.9.2.tgz", - "integrity": "sha512-I53UC1QctruA6SWLvbjbhCpAw7+X7PePoe5pYcwTOEXD/PxeP8LnECAhTHHwWCblyUX5bMi4QLRkxvyZ+IT8Aw==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.10.2.tgz", + "integrity": "sha512-x3Dz6jv6iQKBNjBmVTu8p57abMp/VNTUgKBMgRVXJc5444orBTsArv0+cdfrXTiz/VMmHfDRVkPbL7GH2B7T7w==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.9.2", + "@docusaurus/types": "3.10.2", "tslib": "^2.6.0" }, "engines": { @@ -4065,14 +4180,14 @@ } }, "node_modules/@docusaurus/utils-validation": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.9.2.tgz", - "integrity": "sha512-l7yk3X5VnNmATbwijJkexdhulNsQaNDwoagiwujXoxFbWLcxHQqNQ+c/IAlzrfMMOfa/8xSBZ7KEKDesE/2J7A==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.10.2.tgz", + "integrity": "sha512-sn8unbDfUL585NtR3cwHefPicOyaHvPaX7VD0aOg/siIxUBoKyKKaGEqzJZDS64mM43TnxurkYDtmB1wsJlZsw==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", "fs-extra": "^11.2.0", "joi": "^17.9.2", "js-yaml": "^4.1.0", @@ -4356,12 +4471,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, "node_modules/@jsonjoy.com/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", @@ -4831,6 +4940,59 @@ "react": ">=16" } }, + "node_modules/@module-federation/error-codes": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", + "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", + "license": "MIT" + }, + "node_modules/@module-federation/runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", + "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/runtime-core": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-core": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", + "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-tools": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", + "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/webpack-bundler-runtime": "0.22.0" + } + }, + "node_modules/@module-federation/sdk": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", + "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", + "license": "MIT" + }, + "node_modules/@module-federation/webpack-bundler-runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", + "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -5648,9 +5810,9 @@ "license": "MIT" }, "node_modules/@redocly/ajv": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.3.tgz", - "integrity": "sha512-NQsbJbB/GV7JVO88ebFkMndrnuGp/dTm5/2NISeg+JGcLzTfGBJZ01+V5zD8nKBOpi/dLLNFT+Ql6IcUk8ehng==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.3.tgz", + "integrity": "sha512-l42u0of3hY98sN2A+M4qTX1O/KrpgGH32Hu9kP2GtHyD5Dfqq86PKFLe5dwaD8DEnNmlOlll2BAmeEtf0DaySg==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -5664,65 +5826,338 @@ } }, "node_modules/@redocly/config": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", - "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", - "license": "MIT" + "version": "0.53.1", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.53.1.tgz", + "integrity": "sha512-aOldTWynKS3ZM6u8WkL72d94QBobITv9X6UECiZykgVjdLiAqV737bPVLGKJYPWf0Ryf3zSHgUYALjCj66BkdA==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "2.7.2" + } }, "node_modules/@redocly/openapi-core": { - "version": "1.34.6", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.6.tgz", - "integrity": "sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==", + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-2.47.0.tgz", + "integrity": "sha512-lx4fEeSvWhKagYJGJdEgXWKEBtUthwi0ivcqzScrWZhhM9Vl5REJ/sjFeirSoKIUycrbEAWdin8D8xhOFQgv8g==", "license": "MIT", "dependencies": { - "@redocly/ajv": "^8.11.2", - "@redocly/config": "^0.22.0", + "@redocly/ajv": "^8.18.3", + "@redocly/config": "^0.53.1", + "ajv": "npm:@redocly/ajv@^8.18.3", + "ajv-formats": "^3.0.1", "colorette": "^1.2.0", - "https-proxy-agent": "^7.0.5", + "graphql": "^16.14.1", "js-levenshtein": "^1.1.6", - "js-yaml": "^4.1.0", - "minimatch": "^5.0.1", + "js-yaml": "^5.2.2", + "picomatch": "^4.0.4", "pluralize": "^8.0.0", "yaml-ast-parser": "0.0.43" }, "engines": { - "node": ">=18.17.0", - "npm": ">=9.5.0" + "node": ">=22.12.0 || >=20.19.0 <21.0.0", + "npm": ">=10" } }, - "node_modules/@reduxjs/toolkit": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", - "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "node_modules/@redocly/openapi-core/node_modules/ajv": { + "name": "@redocly/ajv", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.3.tgz", + "integrity": "sha512-l42u0of3hY98sN2A+M4qTX1O/KrpgGH32Hu9kP2GtHyD5Dfqq86PKFLe5dwaD8DEnNmlOlll2BAmeEtf0DaySg==", "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/openapi-core/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" }, "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + "ajv": "^8.0.0" }, "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { + "ajv": { "optional": true } } }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "license": "BSD-3-Clause", + "node_modules/@redocly/openapi-core/node_modules/js-yaml": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.3.0.tgz", + "integrity": "sha512-muutsYr+e2+d3rTgUGslq5rxbBlUy3cJ61IsHag2QNDQV+7zXWjkUpmALIajhrlLlrgRUiymj6U3zUr/TMK84Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", "dependencies": { - "@hapi/hoek": "^9.0.0" + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/@redocly/openapi-core/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rspack/binding": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.12.tgz", + "integrity": "sha512-f4HHuLbvuld8Ba4iB/4ibse5XrKxFrgmM3S4P2AOKnPlekAFlBjmltCuaTL/W2ggYvILaVY+YcFXrEH1rrKeQA==", + "license": "MIT", + "optionalDependencies": { + "@rspack/binding-darwin-arm64": "1.7.12", + "@rspack/binding-darwin-x64": "1.7.12", + "@rspack/binding-linux-arm64-gnu": "1.7.12", + "@rspack/binding-linux-arm64-musl": "1.7.12", + "@rspack/binding-linux-x64-gnu": "1.7.12", + "@rspack/binding-linux-x64-musl": "1.7.12", + "@rspack/binding-wasm32-wasi": "1.7.12", + "@rspack/binding-win32-arm64-msvc": "1.7.12", + "@rspack/binding-win32-ia32-msvc": "1.7.12", + "@rspack/binding-win32-x64-msvc": "1.7.12" + } + }, + "node_modules/@rspack/binding-darwin-arm64": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.12.tgz", + "integrity": "sha512-rbFprJaJiqrmfy8SHth8EsoRS0wg4bXcucwj9NiMzpGFq14Opw8c04iQ6H9BECYzgmN0PKZ9rh41LdVvhdZe4A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-darwin-x64": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.12.tgz", + "integrity": "sha512-jnOp+/UXOJa9xqUb8KXH03sysoO2e4Ij6tw6MqDdmdj8n/A8PQENRPUbW9AwXpPtVDJPus9r4fi7b3+6e4B8Hg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-linux-arm64-gnu": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.12.tgz", + "integrity": "sha512-C8owWG+yvo7X0oVLIXetkoJhIFBP1LYNcAQqtgLmJnQLQDklGuP83dKC+zISGQWpjawHfZ1ER96vLgoTrxKZdw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-arm64-musl": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.12.tgz", + "integrity": "sha512-i51WWI64aRpsfSki6rN0aepPqXkVfS+vZM7+4bWDcmnhUmdMvhIPcYg0QRk3DtyJnu33jqNLM0WHY78k00NyfA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-gnu": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.12.tgz", + "integrity": "sha512-MSos0FuPEefqo9V92ULd5hggKG29EkSNg1zDcypy0OkpsKh5pfjVxTLYFXgTcVyFoUQQbdG8zFBzYbwmJ8V4ew==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-musl": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.12.tgz", + "integrity": "sha512-JcAMVKXOnjfpC3coWjCFPWD3Yl8RBw6a+IXQQ8mfRlHaHMIiOv8IfZqx15XRxMUn49CtP7Z0Na8iiAg2aKrcfw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-wasm32-wasi": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.12.tgz", + "integrity": "sha512-n+ZqP6ZMc0nhOgvadg5VhEs9ojtbES80AcWeFnmGkbzIszvGSO63GKNiRkXtjJ9KFuRzytbbmsCqkUVH+Tywxg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "1.0.7" + } + }, + "node_modules/@rspack/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@rspack/binding-win32-arm64-msvc": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.12.tgz", + "integrity": "sha512-8+h5fYDXYdmugbdfZ+D1y8IQ3rv2EhSfyGP7vBe+bjNyaMa4jWrpucmZbtxojUL1AzaeuHbvMdj9UO/gelk/+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.12.tgz", + "integrity": "sha512-cDMGwTRSa2p9fNBVe1wTRkF2AEXZ9ARWW36QeC5CkLaI0Ezz8lvhF2+CSOPnhaQ1O1qtn0L0SF+lFnrY+I7xGQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-x64-msvc": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.12.tgz", + "integrity": "sha512-wIqFvlgFqrgUyj/6S/FJcvShnkZOmIeXTfqvheLY67MGq8qd8jb1YimQVKAIrmWB3yuJKUFACI3Ag1UBtEedEA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/core": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.12.tgz", + "integrity": "sha512-6CwFIHlhRmXfZoMj3v9MZ1SMTPBn+cHVXeMIeaGp5sufqinKsISbsqHu6ZMJu2wDSmZLdmQJX6zLxkhcAUlhkQ==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime-tools": "0.22.0", + "@rspack/binding": "1.7.12", + "@rspack/lite-tapable": "1.1.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.1" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@rspack/lite-tapable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz", + "integrity": "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==", + "license": "MIT" + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" } }, "node_modules/@sideway/formula": { @@ -5738,9 +6173,9 @@ "license": "BSD-3-Clause" }, "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", "license": "MIT" }, "node_modules/@sindresorhus/is": { @@ -5906,133 +6341,634 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@svgr/babel-preset": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", - "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", - "license": "MIT", - "dependencies": { - "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", - "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", - "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", - "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", - "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", - "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", - "@svgr/babel-plugin-transform-svg-component": "8.0.0" - }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@swc/core": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.1.tgz", + "integrity": "sha512-nUaeu91O5QZKrQdaDCHd402ogUIoNOOjpkZNq0UomWK0G6gDaGmLhvddF1/3BXf5O8aLyo6ZPY/aMDWvaJQ/hg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.28" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.16.1", + "@swc/core-darwin-x64": "1.16.1", + "@swc/core-linux-arm-gnueabihf": "1.16.1", + "@swc/core-linux-arm64-gnu": "1.16.1", + "@swc/core-linux-arm64-musl": "1.16.1", + "@swc/core-linux-ppc64-gnu": "1.16.1", + "@swc/core-linux-s390x-gnu": "1.16.1", + "@swc/core-linux-x64-gnu": "1.16.1", + "@swc/core-linux-x64-musl": "1.16.1", + "@swc/core-win32-arm64-msvc": "1.16.1", + "@swc/core-win32-ia32-msvc": "1.16.1", + "@swc/core-win32-x64-msvc": "1.16.1" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.1.tgz", + "integrity": "sha512-zlJblJ8ncErD43lKdxjbUaUskJQf+LxiPXYcWXD8/8ZMV+7uuAT+CwjciLXpyZBd5Pq/S726bMpeeAwSeL1hhg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.1.tgz", + "integrity": "sha512-IN0BmPWb0YAh/17mmlWB/HDBtTw2MfuW4hulf/tQAgTQBRH17l+z499bNJLK6LizSjqs0P7V+jU38Zj+vJC1DA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.1.tgz", + "integrity": "sha512-EYgrx2YOCQ2Twz2S793kqNjPkpvYVUPzzR95bIb7by+VQcyaai4lZZ2iz/tZvcFVKSNcN3/JTKwx+aBn2ZL52A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.1.tgz", + "integrity": "sha512-moyKm0YZlHdHohzm1YwgAyesqnE853rO0REMfJLFAova51wF9BNi+3ZW2PeS7Vqvn6HeJuepLpAHbBdZctxpHA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.1.tgz", + "integrity": "sha512-kKGBO9wdapiSzuf5ZzZ2fYtlu1BNSYtIIUxvH1ir/gcelTOREEHGDCLTDFx/2Knf878nU11A40z7LxwasEFxqA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.1.tgz", + "integrity": "sha512-nZ6qahtLxC3PM54cWOQZHxt4lTCF/3J4LIoWWzz6v7A+rLs8Dx54anYQf7mH3eIi8KlNpgKci/ie8ZSqFN8O7A==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.1.tgz", + "integrity": "sha512-4ji5PNzhYq193Z4/4xUaSoNJza6iCkDJSzhetrbB6KOYxsr+kxtQr8ePWhMJUiMt6JUWtXaZ1PYT8FhtED+nGA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.1.tgz", + "integrity": "sha512-VJQxqrisHV+B394IgrOu8YsIIXZgffnf5tO+yc9Z/hoUpuZEvuQTjWwlnpZdpyD+0nx6LTD1/3k646JYm43yJA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.1.tgz", + "integrity": "sha512-r9oV1mwxxsIGcLV1IQ/tw76MW3doatKze1QFWuC+a7QqJUkhY/bKTSVk6NpKKUGm2LDsE33Va8VqSClfA7vSiQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.1.tgz", + "integrity": "sha512-6huNRessoBLxWEqBm5zJXyCQ27TO7anvkdiuQ5MDO4CJni0nOXEqKtV9RllQ2TdyENKKsUMXVnIfW2hIXx/R5Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.1.tgz", + "integrity": "sha512-OVKJFUzphrGmsh+BGtcZDesx0YryV7/Yvy5XGgTqnrZfjnyfcr5uaqYQugCckdIlupc5Vs3XtDjRAj12z4ZPlw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.1.tgz", + "integrity": "sha512-Bt+VIhWYCGk4urklnkkteLUOeLv1VxigwTCeB/xC6rBZxY6IIKdDwCJf6on3E3SUGsIqmQS6QqtuJQc1VxF4Aw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/html": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html/-/html-1.16.1.tgz", + "integrity": "sha512-OYkogBdrxP4kziTlIrFoXgpGfjgoqVjoQKbPleHw4XEXrSgbHeQkkQXYOSZGLl6Q1d1JFsFHuL6Tars/IN9EWQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@swc/html-darwin-arm64": "1.16.1", + "@swc/html-darwin-x64": "1.16.1", + "@swc/html-linux-arm-gnueabihf": "1.16.1", + "@swc/html-linux-arm64-gnu": "1.16.1", + "@swc/html-linux-arm64-musl": "1.16.1", + "@swc/html-linux-ppc64-gnu": "1.16.1", + "@swc/html-linux-s390x-gnu": "1.16.1", + "@swc/html-linux-x64-gnu": "1.16.1", + "@swc/html-linux-x64-musl": "1.16.1", + "@swc/html-win32-arm64-msvc": "1.16.1", + "@swc/html-win32-ia32-msvc": "1.16.1", + "@swc/html-win32-x64-msvc": "1.16.1" + } + }, + "node_modules/@swc/html-darwin-arm64": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.16.1.tgz", + "integrity": "sha512-kFs0Rk9pMb/FiX7BaRhSKsw4J4VoBR0J/iO1h5WtegcXz5kuR3L2VHhde0zSJt6irZU+NgiAw5ULLJJo/+yDCQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-darwin-x64": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html-darwin-x64/-/html-darwin-x64-1.16.1.tgz", + "integrity": "sha512-oItqtVJ2WnHVxI2TqcQJ/VM1LNHwJhhf0a2syioT0dhYiR6b6jidMFRmlgTTTSBxqxwiSHhAeDYyx7HHAn7b1g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-arm-gnueabihf": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html-linux-arm-gnueabihf/-/html-linux-arm-gnueabihf-1.16.1.tgz", + "integrity": "sha512-5+VAuNhby3fuciNmLe5R5ypZemE5Qq3DJUM0cp9JPb7oVJVM/KqXbxedhI70J5chqFSrfAR7/7OWDxxL8CBNvg==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-arm64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.16.1.tgz", + "integrity": "sha512-27mr1L1WR1bsC9t3NkK6JWK5TMIC9fbz0QlM1yzaqRwn9Hs7S48ZkJAk5TC0lNxsU/BLrEr4kMLaDAJ8aVkjig==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-arm64-musl": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-musl/-/html-linux-arm64-musl-1.16.1.tgz", + "integrity": "sha512-jKLc+AiR64y8RdMpBbpccgmMfVvcPOhrgMyewCLgyB3qS0wXa107KLzdFSOFdsCJiOGdzuoHygImmptek7vgBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-ppc64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html-linux-ppc64-gnu/-/html-linux-ppc64-gnu-1.16.1.tgz", + "integrity": "sha512-BGWhTWe8ef2Z76GE8MTxNpyDiHAqnjtQ2LFi4qb4XuoCoBSCxYC0+7kw72D/98+gSCvjWqGBFY9kz6QiSalS4g==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-s390x-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html-linux-s390x-gnu/-/html-linux-s390x-gnu-1.16.1.tgz", + "integrity": "sha512-8VYsjv33X1afDdXfVVV9m3lkvjAnMUNHWSSmqf9+LIXwg6GfNynf88U8w6MYS6809+9EtuBBxhePF3u9J0EaVw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-x64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.16.1.tgz", + "integrity": "sha512-VUr08k9KncdAJGWO8dSNzY0JdZPKo1+7PjH/eNo5p1Fv9AwL4lzzh2uRvmYn9Ill2qfazPY+0Gse8BCWPL3KIw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=10" } }, - "node_modules/@svgr/core": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", - "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.3", - "@svgr/babel-preset": "8.1.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^8.1.3", - "snake-case": "^3.0.4" - }, + "node_modules/@swc/html-linux-x64-musl": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html-linux-x64-musl/-/html-linux-x64-musl-1.16.1.tgz", + "integrity": "sha512-1i7cIhBoRlglaAOdOBv4rKH5N7MZygH4z7R5QloBuQVlQ/5lkL00yKU5ZdCa0tR5/ri6Q/JuR5PeuYNg63P67g==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "node": ">=10" } }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", - "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.21.3", - "entities": "^4.4.0" - }, + "node_modules/@swc/html-win32-arm64-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html-win32-arm64-msvc/-/html-win32-arm64-msvc-1.16.1.tgz", + "integrity": "sha512-zOKTv8W1y2ir5+uylfS/s0D64DxU0PtH0G1bjCxOKsQVDURwZmZ1Ehdyinv+CCM/kNDlgLJz2ssaynHiYlmQaQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "node": ">=10" } }, - "node_modules/@svgr/plugin-jsx": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", - "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.3", - "@svgr/babel-preset": "8.1.0", - "@svgr/hast-util-to-babel-ast": "8.0.0", - "svg-parser": "^2.0.4" - }, + "node_modules/@swc/html-win32-ia32-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html-win32-ia32-msvc/-/html-win32-ia32-msvc-1.16.1.tgz", + "integrity": "sha512-A1o1FujsjwaJbi8riZt5nxsJI//95elt9O0ryt43zlyVKB/WdGdPu2fPS4jkDhQov9baqcQkHLv210aFl9rzbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@svgr/core": "*" + "node": ">=10" } }, - "node_modules/@svgr/plugin-svgo": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", - "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", - "license": "MIT", - "dependencies": { - "cosmiconfig": "^8.1.3", - "deepmerge": "^4.3.1", - "svgo": "^3.0.2" - }, + "node_modules/@swc/html-win32-x64-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/html-win32-x64-msvc/-/html-win32-x64-msvc-1.16.1.tgz", + "integrity": "sha512-b14EbrfRC7Qmy1nGHEBk2qlsgoIQAtCV93a09ibHVZ5IH9JBy8o+tCcsn9KcLdhGmYM+nLP3zJ90czixPZGshg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@svgr/core": "*" + "node": ">=10" } }, - "node_modules/@svgr/webpack": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", - "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", - "license": "MIT", + "node_modules/@swc/types": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz", + "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==", + "license": "Apache-2.0", "dependencies": { - "@babel/core": "^7.21.3", - "@babel/plugin-transform-react-constant-elements": "^7.21.3", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "@babel/preset-typescript": "^7.21.0", - "@svgr/core": "8.1.0", - "@svgr/plugin-jsx": "8.1.0", - "@svgr/plugin-svgo": "8.1.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "@swc/counter": "^0.1.3" } }, "node_modules/@szmarczak/http-timer": { @@ -6047,15 +6983,6 @@ "node": ">=14.16" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -6172,12 +7099,6 @@ "@types/send": "*" } }, - "node_modules/@types/gtag.js": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", - "integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==", - "license": "MIT" - }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -6697,21 +7618,12 @@ } }, "node_modules/address": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", - "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/address/-/address-2.0.3.tgz", + "integrity": "sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==", "license": "MIT", "engines": { - "node": ">= 14" + "node": ">= 16.0.0" } }, "node_modules/aggregate-error": { @@ -6787,34 +7699,34 @@ } }, "node_modules/algoliasearch": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.0.tgz", - "integrity": "sha512-aD8EQC6KEman6/S79FtPdQmB7D4af/etcRL/KwiKFKgAE62iU8c5PeEQvpvIcBPurC3O/4Lj78nOl7ZcoazqSw==", - "license": "MIT", - "dependencies": { - "@algolia/abtesting": "1.14.0", - "@algolia/client-abtesting": "5.48.0", - "@algolia/client-analytics": "5.48.0", - "@algolia/client-common": "5.48.0", - "@algolia/client-insights": "5.48.0", - "@algolia/client-personalization": "5.48.0", - "@algolia/client-query-suggestions": "5.48.0", - "@algolia/client-search": "5.48.0", - "@algolia/ingestion": "1.48.0", - "@algolia/monitoring": "1.48.0", - "@algolia/recommend": "5.48.0", - "@algolia/requester-browser-xhr": "5.48.0", - "@algolia/requester-fetch": "5.48.0", - "@algolia/requester-node-http": "5.48.0" + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.57.0.tgz", + "integrity": "sha512-HpND7MBGctOAkd1GoQoDZCGoCpqNTS5NG1LuhElFet3RdLJkwnyTYZXZhXwtpAQPrI36fqQ3eT6KQrdKDTKu3A==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.23.0", + "@algolia/client-abtesting": "5.57.0", + "@algolia/client-analytics": "5.57.0", + "@algolia/client-common": "5.57.0", + "@algolia/client-insights": "5.57.0", + "@algolia/client-personalization": "5.57.0", + "@algolia/client-query-suggestions": "5.57.0", + "@algolia/client-search": "5.57.0", + "@algolia/ingestion": "1.57.0", + "@algolia/monitoring": "1.57.0", + "@algolia/recommend": "5.57.0", + "@algolia/requester-browser-xhr": "5.57.0", + "@algolia/requester-fetch": "5.57.0", + "@algolia/requester-node-http": "5.57.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/algoliasearch-helper": { - "version": "3.27.0", - "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.27.0.tgz", - "integrity": "sha512-eNYchRerbsvk2doHOMfdS1/B6Tm70oGtu8mzQlrNzbCeQ8p1MjCW8t/BL6iZ5PD+cL5NNMgTMyMnmiXZ1sgmNw==", + "version": "3.29.3", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.29.3.tgz", + "integrity": "sha512-gVOMbbPVrCO3Xs+B+BLAIGFqIQow6qHMTYCEeBqLQB9m4ZmKUqMwkEumlal2iyyezHEd4Y0FvFTLbCRutCutIQ==", "license": "MIT", "dependencies": { "@algolia/events": "^4.0.1" @@ -6861,33 +7773,6 @@ "node": ">=8" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-html-community": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", @@ -6924,6 +7809,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -7000,9 +7894,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.24", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", - "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", "funding": [ { "type": "opencollective", @@ -7019,8 +7913,8 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001766", + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -7062,13 +7956,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", - "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { @@ -7098,12 +7992,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", - "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.6" + "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -7146,12 +8040,15 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.11.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.17.tgz", + "integrity": "sha512-KAUDn1OSS0fmPlGO+NOUMRcOQ/b/shUBH3OgkG73mPgdf+JD/BQ6fHboGxNOxnUmlwcq+lLq3dTkayRPuSfXwg==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/batch": { @@ -7280,12 +8177,13 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { @@ -7301,9 +8199,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "funding": [ { "type": "opencollective", @@ -7320,11 +8218,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -7424,14 +8322,14 @@ } }, "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==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -7520,9 +8418,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", "funding": [ { "type": "opencollective", @@ -7889,9 +8787,9 @@ "license": "MIT" }, "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", "license": "MIT" }, "node_modules/colorette": { @@ -8213,24 +9111,16 @@ } }, "node_modules/core-js-compat": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", - "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", + "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.1" + "browserslist": "^4.28.7" + }, + "engines": { + "node": ">=6.4.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-pure": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.48.0.tgz", - "integrity": "sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==", - "hasInstallScript": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -8341,9 +9231,9 @@ } }, "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -8354,9 +9244,9 @@ } }, "node_modules/css-declaration-sorter": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.1.tgz", - "integrity": "sha512-gz6x+KkgNCjxq3Var03pRYLhyNfwhkKF1g/yoLgDNtFvVu0/fOLV9C8fFEZRjACp/XQLumjAYo7JVjzH3wLbxA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.4.0.tgz", + "integrity": "sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==", "license": "ISC", "engines": { "node": "^14 || ^16 || >=18" @@ -8415,9 +9305,9 @@ } }, "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -8570,9 +9460,9 @@ } }, "node_modules/cssdb": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.7.1.tgz", - "integrity": "sha512-+F6LKx48RrdGOtE4DT5jz7Uo+VeyKXpK797FAevIkzjV8bMHz6xTO5F7gNDcRCHmPgD5jj2g6QCsY9zmVrh38A==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.10.0.tgz", + "integrity": "sha512-+JWEEjjoqkPY7iGHps3SaCT2w67Zpaj9zHvhCJ2iPBavKHwgtOOD2YEbZSCU8VO2uVGpKdYjVz+j6bhkNSjZ0g==", "funding": [ { "type": "opencollective", @@ -8927,7 +9817,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -8951,20 +9840,19 @@ } }, "node_modules/detect-port": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", - "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-2.1.0.tgz", + "integrity": "sha512-epZuWb/6Q62L+nDHJc/hQAqf8pylsqgk3BpZXVBx1CDnr3nkrVNn73Uu1rXcFzkNcc+hkP3whuOg7JZYaQB65Q==", "license": "MIT", "dependencies": { - "address": "^1.0.1", - "debug": "4" + "address": "^2.0.1" }, "bin": { - "detect": "bin/detect-port.js", - "detect-port": "bin/detect-port.js" + "detect": "dist/commonjs/bin/detect-port.js", + "detect-port": "dist/commonjs/bin/detect-port.js" }, "engines": { - "node": ">= 4.0.0" + "node": ">= 16.0.0" } }, "node_modules/devlop": { @@ -9005,22 +9893,22 @@ } }, "node_modules/docusaurus-plugin-openapi-docs": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.7.1.tgz", - "integrity": "sha512-RpqvTEnhIfdSuTn/Fa/8bmxeufijLL9HCRb//ELD33AKqEbCw147SKR/CqWu4H4gwi50FZLUbiHKZJbPtXLt9Q==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-5.2.0.tgz", + "integrity": "sha512-MjrfRAMB64uvdxRVz6L9AXWe4QFjCdoBAzYs306yyI3nnXHsFj2lv2FnLA90JV9CAUZaGiYMvvkzBo2Nrkq/9w==", "license": "MIT", "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.5.4", - "@redocly/openapi-core": "^1.34.3", + "@apidevtools/json-schema-ref-parser": "^15.3.3", + "@redocly/openapi-core": "^2.25.2", "allof-merge": "^0.6.6", - "chalk": "^4.1.2", + "chalk": "^5.6.2", "clsx": "^2.1.1", "fs-extra": "^11.3.0", "json-pointer": "^0.6.2", "json5": "^2.2.3", "lodash": "^4.17.21", "mustache": "^4.2.0", - "openapi-to-postmanv2": "^5.0.0", + "openapi-to-postmanv2": "^6.0.0", "postman-collection": "^5.0.2", "slugify": "^1.6.6", "swagger2openapi": "^7.0.8", @@ -9030,18 +9918,29 @@ "node": ">=14" }, "peerDependencies": { - "@docusaurus/plugin-content-docs": "^3.5.0", - "@docusaurus/utils": "^3.5.0", - "@docusaurus/utils-validation": "^3.5.0", + "@docusaurus/plugin-content-docs": "^3.10.0", + "@docusaurus/utils": "^3.10.0", + "@docusaurus/utils-validation": "^3.10.0", "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/docusaurus-plugin-openapi-docs/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/docusaurus-plugin-sass": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.6.tgz", "integrity": "sha512-2hKQQDkrufMong9upKoG/kSHJhuwd+FA3iAe/qzS/BmWpbIpe7XKmq5wlz4J5CJaOPu4x+iDJbgAxZqcoQf0kg==", "license": "MIT", - "peer": true, "dependencies": { "sass-loader": "^16.0.2" }, @@ -9051,9 +9950,9 @@ } }, "node_modules/docusaurus-theme-openapi-docs": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.7.1.tgz", - "integrity": "sha512-OPydf11LoEY3fdxaoqCVO+qCk7LBo6l6s28UvHJ5mIN/2xu+dOOio9+xnKZ5FIPOlD+dx0gVSKzaVCi/UFTxlg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-5.2.0.tgz", + "integrity": "sha512-L0b80LzaMUfr76a9EQXRPCf8nxkEz8Xo6Aknnke1UeE2oXsgoiVki6U+RTE7GmJRjO8zSNKXyckGmGmqqWuHeA==", "license": "MIT", "dependencies": { "@hookform/error-message": "^2.0.1", @@ -9065,7 +9964,8 @@ "crypto-js": "^4.2.0", "file-saver": "^2.0.5", "lodash": "^4.17.21", - "pako": "^2.1.0", + "pako": "^3.0.1", + "path-browserify": "^1.0.1", "postman-code-generators": "^2.0.0", "postman-collection": "^5.0.2", "prism-react-renderer": "^2.4.1", @@ -9079,7 +9979,7 @@ "rehype-raw": "^7.0.0", "remark-gfm": "4.0.1", "sass": "^1.89.2", - "sass-loader": "^16.0.5", + "sass-loader": "^17.0.0", "unist-util-visit": "^5.0.0", "url": "^0.11.4", "xml-formatter": "^3.6.6" @@ -9088,11 +9988,44 @@ "node": ">=14" }, "peerDependencies": { - "@docusaurus/theme-common": "^3.5.0", - "docusaurus-plugin-openapi-docs": "^4.0.0", - "docusaurus-plugin-sass": "^0.2.3", - "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "@docusaurus/theme-common": "^3.10.0", + "docusaurus-plugin-openapi-docs": "^5.0.0", + "docusaurus-plugin-sass": "^0.2.3", + "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/sass-loader": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-17.0.0.tgz", + "integrity": "sha512-0Ybm8ohBQ9LcrycVrFQp/KQBNX5a3Wda9/smS0mE/xLffzEnwvV8nykOzrbiSWNzTE3IB/jiXx8O4QmDPG2+Gw==", + "license": "MIT", + "engines": { + "node": ">= 22.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/dom-converter": { @@ -9226,9 +10159,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "version": "1.5.412", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.412.tgz", + "integrity": "sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -9444,19 +10377,6 @@ "node": ">=8.0.0" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -9878,30 +10798,6 @@ "node": ">=0.4.0" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", @@ -10323,28 +11219,6 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "license": "BSD-2-Clause" }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/global-dirs": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", @@ -10444,41 +11318,13 @@ "lodash": "^4.17.15" } }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "node_modules/graphql": { + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", "license": "MIT", - "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, "engines": { - "node": ">=6.0" - } - }, - "node_modules/gray-matter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, "node_modules/gzip-size": { @@ -11063,19 +11909,6 @@ "node": ">=10.19.0" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -11713,9 +12546,19 @@ "license": "MIT" }, "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==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -11789,6 +12632,20 @@ "node": ">=12.0.0" } }, + "node_modules/json-schema-to-ts": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-2.7.2.tgz", + "integrity": "sha512-R1JfqKqbBR4qE8UyBR56Ms30LL62/nlhoz+1UkfI/VE7p54Awu919FZ6ZUPG8zIa3XB65usPJgr1ONVncUGSaQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@types/json-schema": "^7.0.9", + "ts-algebra": "^1.2.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -11834,59 +12691,320 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=0.10.0" + } + }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "license": "MIT", + "dependencies": { + "package-json": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/launch-editor": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", + "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/klaw-sync": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11" + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "license": "MIT", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/latest-version": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", - "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", - "license": "MIT", - "dependencies": { - "package-json": "^8.1.0" - }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.16" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/launch-editor": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", - "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", - "license": "MIT", - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "license": "MIT", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lilconfig": { @@ -14423,9 +15541,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.0.tgz", - "integrity": "sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==", + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", + "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==", "license": "MIT", "dependencies": { "schema-utils": "^4.0.0", @@ -14449,15 +15567,15 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" + "node": "*" } }, "node_modules/minimist": { @@ -14518,9 +15636,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -14633,10 +15751,13 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "license": "MIT" + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -14710,9 +15831,9 @@ } }, "node_modules/null-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -14976,9 +16097,9 @@ } }, "node_modules/openapi-to-postmanv2": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-5.8.0.tgz", - "integrity": "sha512-7f02ypBlAx4G9z3bP/uDk8pBwRbYt97Eoso8XJLyclfyRvCC+CvERLUl0MD0x+GoumpkJYnQ0VGdib/kwtUdUw==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-6.3.3.tgz", + "integrity": "sha512-o0u6qqMMRLt7eAyFBpL/lCresQ1VqQFGe6/iPMBvNkWDXrpgGo5jo+YnP1e0MwF3tdUKLfkHssufGWMdojpPLg==", "license": "Apache-2.0", "dependencies": { "ajv": "^8.11.0", @@ -14987,13 +16108,14 @@ "async": "3.2.6", "commander": "2.20.3", "graphlib": "2.1.8", - "js-yaml": "4.1.0", + "js-yaml": "4.3.0", "json-pointer": "0.6.2", "json-schema-merge-allof": "0.8.1", - "lodash": "4.17.21", + "lodash": "4.18.1", "neotraverse": "0.6.15", "oas-resolver-browser": "2.5.6", "object-hash": "3.0.0", + "openapi-types": "^12.1.3", "path-browserify": "1.0.1", "postman-collection": "^5.0.0", "swagger2openapi": "7.0.8", @@ -15012,22 +16134,16 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, - "node_modules/openapi-to-postmanv2/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/openapi-to-postmanv2/node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", "license": "MIT" }, "node_modules/opener": { @@ -15166,9 +16282,19 @@ } }, "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pako/-/pako-3.0.1.tgz", + "integrity": "sha512-GupotUUI0mlhugKjUs4bjOwLt3nrehy9Ys2dxC0GtgVef5cnKggkDMmf2bq2poCCuVXopWPmqsc9VDT2iJUy+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "(MIT AND Zlib)" }, "node_modules/param-case": { @@ -15452,9 +16578,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -15471,7 +16597,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -15505,9 +16631,9 @@ } }, "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -15749,9 +16875,9 @@ } }, "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -15787,9 +16913,9 @@ } }, "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -15915,9 +17041,9 @@ } }, "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -15953,9 +17079,9 @@ } }, "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -16242,9 +17368,9 @@ } }, "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -16270,9 +17396,9 @@ } }, "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -16369,9 +17495,9 @@ } }, "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -16732,9 +17858,9 @@ } }, "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -16825,9 +17951,9 @@ } }, "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -16838,9 +17964,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -17399,9 +18525,9 @@ } }, "node_modules/react-loadable-ssr-addon-v5-slorber": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", - "integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.3.tgz", + "integrity": "sha512-GXfh9VLwB5ERaCsU6RULh7tkemeX15aNh6wuMEBtfdyMa7fFG8TXrhXlx1SoEK2Ty/l6XIkzzYIQmyaWW3JgdQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.3" @@ -17740,9 +18866,9 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", - "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.1.0" @@ -18018,15 +19144,6 @@ "entities": "^2.0.0" } }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -18308,9 +19425,9 @@ } }, "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -18483,42 +19600,20 @@ } }, "node_modules/serve-handler": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", - "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", "license": "MIT", "dependencies": { "bytes": "3.0.0", "content-disposition": "0.5.2", "mime-types": "2.1.18", - "minimatch": "3.1.2", + "minimatch": "3.1.5", "path-is-inside": "1.0.2", "path-to-regexp": "3.3.0", "range-parser": "1.2.0" } }, - "node_modules/serve-handler/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/serve-handler/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/serve-handler/node_modules/path-to-regexp": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", @@ -18876,9 +19971,9 @@ "license": "MIT" }, "node_modules/sitemap": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.2.tgz", - "integrity": "sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.3.tgz", + "integrity": "sha512-tAjEd+wt/YwnEbfNB2ht51ybBJxbEWwe5ki/Z//Wh0rpBFTCUSj46GnxUKEWzhfuJTsee8x3lybHxFgUMig2hw==", "license": "MIT", "dependencies": { "@types/node": "^17.0.5", @@ -19037,12 +20132,6 @@ "wbuf": "^1.7.3" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, "node_modules/srcset": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", @@ -19283,24 +20372,27 @@ } }, "node_modules/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", - "license": "MIT" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.5.tgz", + "integrity": "sha512-FuT74puvVMJQ8sgFxgOKPex5MbxPeltqCXJV3wR9Xq+vPHaF+tTdE1lhJcwspUxjtjvXy1NwqcSLZx9UNwHawg==", + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.4.tgz", + "integrity": "sha512-GsNRis4e8jxn2Y9ENz/8lbJ93CstG8svtMnuRaHbiF2LTJ5tK0/q3t/URPq9Zc7zVWBJnNnJMIp6bevK7bSmNg==", "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" @@ -19349,6 +20441,19 @@ "url": "https://github.com/Mermade/oas-kit?sponsor=1" } }, + "node_modules/swc-loader": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.7.tgz", + "integrity": "sha512-nwYWw3Fh9ame3Rtm7StS9SBLpHRRnYcK7bnpF3UKZmesAK0gw2/ADvlURFAINmPvKtDLzp+GBiP9yLoEjg6S9w==", + "license": "MIT", + "dependencies": { + "@swc/counter": "^0.1.3" + }, + "peerDependencies": { + "@swc/core": "^1.2.147", + "webpack": ">=2" + } + }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -19630,6 +20735,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-algebra": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-1.2.2.tgz", + "integrity": "sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==", + "license": "MIT" + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -19927,9 +21038,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", "funding": [ { "type": "opencollective", @@ -20675,75 +21786,30 @@ } }, "node_modules/webpackbar": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz", - "integrity": "sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-7.0.0.tgz", + "integrity": "sha512-aS9soqSO2iCHgqHoCrj4LbfGQUboDCYJPSFOAchEK+9psIjNrfSWW4Y0YEz67MKURNvMmfo0ycOg9d/+OOf9/Q==", "license": "MIT", "dependencies": { - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", + "ansis": "^3.2.0", "consola": "^3.2.3", - "figures": "^3.2.0", - "markdown-table": "^2.0.0", "pretty-time": "^1.1.0", - "std-env": "^3.7.0", - "wrap-ansi": "^7.0.0" + "std-env": "^3.7.0" }, "engines": { "node": ">=14.21.3" }, "peerDependencies": { + "@rspack/core": "*", "webpack": "3 || 4 || 5" - } - }, - "node_modules/webpackbar/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/webpackbar/node_modules/markdown-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", - "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", - "license": "MIT", - "dependencies": { - "repeat-string": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/webpackbar/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpackbar/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/websocket-driver": { @@ -21038,9 +22104,9 @@ "license": "Apache-2.0" }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", diff --git a/docs/package.json b/docs/package.json index a4ac47e3..ab8a63c1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -17,21 +17,23 @@ "clean-api-docs": "docusaurus clean-api-docs all" }, "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/preset-classic": "3.9.2", + "@docusaurus/core": "3.10.2", + "@docusaurus/faster": "^3.10.2", + "@docusaurus/preset-classic": "3.10.2", "@easyops-cn/docusaurus-search-local": "^0.55.0", "@mdx-js/react": "^3.1.1", "clsx": "^2.1.1", - "docusaurus-plugin-openapi-docs": "^4.7.1", - "docusaurus-theme-openapi-docs": "^4.7.1", + "docusaurus-plugin-openapi-docs": "^5.2.0", + "docusaurus-plugin-sass": "^0.2.6", + "docusaurus-theme-openapi-docs": "^5.2.0", "prism-react-renderer": "^2.4.1", "react": "^19.2.4", "react-dom": "^19.2.4" }, "devDependencies": { - "@docusaurus/module-type-aliases": "3.9.2", - "@docusaurus/tsconfig": "3.9.2", - "@docusaurus/types": "3.9.2", + "@docusaurus/module-type-aliases": "3.10.2", + "@docusaurus/tsconfig": "3.10.2", + "@docusaurus/types": "3.10.2", "typescript": "~5.9.3" }, "browserslist": { diff --git a/tests/api/access_group_visibility.rs b/tests/api/access_group_visibility.rs index 053984aa..0aebcba5 100644 --- a/tests/api/access_group_visibility.rs +++ b/tests/api/access_group_visibility.rs @@ -428,6 +428,7 @@ async fn test_recently_added_series_hides_denied() { } #[tokio::test] +#[allow(deprecated)] // exercises the deprecated `full` parameter async fn test_search_series_endpoint_hides_denied() { use codex::api::routes::v1::dto::series::SearchSeriesRequest; diff --git a/tests/api/binary_content_types.rs b/tests/api/binary_content_types.rs new file mode 100644 index 00000000..4a5bd9af --- /dev/null +++ b/tests/api/binary_content_types.rs @@ -0,0 +1,440 @@ +//! Conformance between the media types the document declares and the ones the +//! server actually writes, for every endpoint whose body is not JSON. +//! +//! This invariant cannot be checked from inside the OpenAPI document: nothing in +//! it is inconsistent, because the document has no idea what the handler sets on +//! the `Content-Type` header. It has to be asserted from the other side, by +//! making the request and comparing the answer against what was declared. +//! +//! A strict generator (`swift-openapi-generator` emits +//! `converter.validateContentType(in:matching:)`) throws on a response whose +//! media type is not among the declared ones, so a mismatch here is a hard +//! client failure on a response the server considers successful. +//! +//! The cases picked below are deliberately the *unhappy* ones — the SVG +//! placeholder a thumbnail serves before its generation task finishes, a PNG +//! page rather than a JPEG one, a PDF rather than a CBZ. The happy path was +//! always going to match; these are the ones that fired in the field. + +#[path = "../common/mod.rs"] +mod common; + +use codex::api::ApiDoc; +use codex::db::ScanningStrategy; +use codex::db::repositories::{ + BookRepository, LibraryRepository, PageRepository, SeriesExportRepository, SeriesRepository, + UserRepository, +}; +use codex::utils::password; +use common::*; +use hyper::StatusCode; +use serde_json::{Value, json}; +use std::path::Path; +use tempfile::TempDir; +use utoipa::OpenApi; + +// ============================================================================ +// Document lookup +// ============================================================================ + +/// The media types the document declares on `operation_id`'s 200 response. +/// +/// Panics if the operation is absent or declares no 200 content, because both +/// mean the assertion below would silently pass on nothing. +fn declared_200_content_types(operation_id: &str) -> Vec { + let spec: Value = serde_json::to_value(ApiDoc::openapi()).expect("spec should serialize"); + + let content = + spec["paths"] + .as_object() + .expect("paths object") + .values() + .flat_map(|item| item.as_object().expect("path item object").values()) + .find(|op| op.get("operationId").and_then(Value::as_str) == Some(operation_id)) + .unwrap_or_else(|| panic!("no operation with operationId {operation_id}"))["responses"] + ["200"]["content"] + .as_object() + .unwrap_or_else(|| panic!("{operation_id} declares no 200 content")) + .keys() + .cloned() + .collect::>(); + + assert!( + !content.is_empty(), + "{operation_id} declares an empty 200 content map" + ); + content +} + +/// True when `actual` is covered by `declared`, per the OpenAPI media-type +/// matching rules a strict client applies. +/// +/// Two things a naive string compare gets wrong: a declared `image/*` covers +/// every image subtype, and a header may carry parameters (`text/csv; +/// charset=utf-8`) that are not part of the media type key in the document. +fn media_type_matches(declared: &str, actual: &str) -> bool { + let actual = actual.split(';').next().unwrap_or("").trim(); + + if let Some(declared_type) = declared.strip_suffix("/*") { + return actual + .split('/') + .next() + .is_some_and(|actual_type| actual_type.eq_ignore_ascii_case(declared_type)); + } + + declared.eq_ignore_ascii_case(actual) +} + +/// Assert the response's `Content-Type` is one the document declares for this +/// operation, and that it is the specific one this test set out to exercise. +/// +/// The second half matters: without it a test that stopped reaching the +/// placeholder path would keep passing while covering nothing. +fn assert_declared_content_type( + operation_id: &str, + headers: &hyper::HeaderMap, + expected_actual: &str, +) { + let actual = headers + .get(hyper::header::CONTENT_TYPE) + .unwrap_or_else(|| panic!("{operation_id} sent no Content-Type")) + .to_str() + .expect("Content-Type is valid ASCII"); + + assert!( + media_type_matches(expected_actual, actual), + "{operation_id} was expected to exercise {expected_actual}, but sent {actual}; \ + the test no longer covers the path it was written for" + ); + + let declared = declared_200_content_types(operation_id); + assert!( + declared.iter().any(|d| media_type_matches(d, actual)), + "{operation_id} sent Content-Type {actual}, which none of its declared media \ + types {declared:?} covers. A strict generated client throws on this response \ + instead of decoding it." + ); +} + +// ============================================================================ +// Fixtures +// ============================================================================ + +async fn admin_token( + db: &sea_orm::DatabaseConnection, + state: &codex::api::extractors::AppState, +) -> String { + let password_hash = password::hash_password("admin123").unwrap(); + let user = create_test_user("admin", "admin@example.com", &password_hash, true); + let created = UserRepository::create(db, &user).await.unwrap(); + + state + .jwt_service + .generate_token(created.id, created.username.clone(), created.get_role()) + .unwrap() +} + +fn book_model( + series_id: uuid::Uuid, + library_id: uuid::Uuid, + path: &str, + file_name: &str, + format: &str, + page_count: i32, +) -> codex::db::entities::books::Model { + use chrono::Utc; + codex::db::entities::books::Model { + id: uuid::Uuid::new_v4(), + series_id, + library_id, + path: path.to_string(), + file_name: file_name.to_string(), + file_size: 1024, + file_hash: format!("hash_{}", uuid::Uuid::new_v4()), + partial_hash: String::new(), + format: format.to_string(), + page_count, + deleted: false, + analyzed: page_count > 0, + analysis_error: None, + analysis_errors: None, + modified_at: Utc::now(), + created_at: Utc::now(), + updated_at: Utc::now(), + thumbnail_path: None, + thumbnail_generated_at: None, + koreader_hash: None, + epub_positions: None, + epub_spine_items: None, + } +} + +/// Register a book whose file already exists on disk, and return its id. +async fn seed_book( + db: &sea_orm::DatabaseConnection, + dir: &Path, + file: &Path, + format: &str, + page_count: i32, +) -> uuid::Uuid { + let library = LibraryRepository::create( + db, + "Test Library", + dir.to_str().unwrap(), + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create(db, library.id, "Test Series", None) + .await + .unwrap(); + + let book = book_model( + series.id, + library.id, + file.to_str().unwrap(), + file.file_name().unwrap().to_str().unwrap(), + format, + page_count, + ); + + BookRepository::create(db, &book, None).await.unwrap().id +} + +// ============================================================================ +// get_book_file: one concrete type per book format +// ============================================================================ + +/// Every format the handler's match arm names, paired with the media type it +/// sets for that format. The handler never opens the archive on this route, so +/// the bytes on disk only have to exist. +async fn assert_book_file_content_type(format: &str, extension: &str, expected: &str) { + let (db, _temp_dir) = setup_test_db().await; + let dir = TempDir::new().unwrap(); + let file = dir.path().join(format!("volume.{extension}")); + std::fs::write(&file, b"not really an archive, but it is bytes").unwrap(); + + let book_id = seed_book(&db, dir.path(), &file, format, 1).await; + + let state = create_test_app_state(db.clone()).await; + let token = admin_token(&db, &state).await; + let app = create_test_router_with_app_state(state); + + let request = get_request_with_auth(&format!("/api/v1/books/{book_id}/file"), &token); + let (status, headers, _body) = make_full_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + assert_declared_content_type("get_book_file", &headers, expected); +} + +#[tokio::test] +async fn book_file_declares_the_zip_type_it_sends_for_a_cbz() { + assert_book_file_content_type("cbz", "cbz", "application/zip").await; +} + +#[tokio::test] +async fn book_file_declares_the_rar_type_it_sends_for_a_cbr() { + assert_book_file_content_type("cbr", "cbr", "application/x-rar-compressed").await; +} + +#[tokio::test] +async fn book_file_declares_the_epub_type_it_sends_for_an_epub() { + assert_book_file_content_type("epub", "epub", "application/epub+zip").await; +} + +#[tokio::test] +async fn book_file_declares_the_pdf_type_it_sends_for_a_pdf() { + assert_book_file_content_type("pdf", "pdf", "application/pdf").await; +} + +/// The handler's catch-all arm. Unreachable through the scanner, which only +/// writes the four formats above, but reachable through any other writer of +/// `books.format`, and the document has to describe it or a client throws. +#[tokio::test] +async fn book_file_declares_the_octet_stream_type_it_sends_for_an_unknown_format() { + assert_book_file_content_type("djvu", "djvu", "application/octet-stream").await; +} + +// ============================================================================ +// get_page_image and the OPDS-PSE copy: whatever is in the archive +// ============================================================================ + +/// The CBZ fixture holds PNG pages, so this is the non-JPEG path that +/// `image/jpeg` failed to describe. +async fn seed_png_page_book(db: &sea_orm::DatabaseConnection, dir: &TempDir) -> uuid::Uuid { + let cbz = create_test_cbz(dir, 3, true); + let book_id = seed_book(db, dir.path(), &cbz, "cbz", 3).await; + + let page = codex::db::entities::pages::Model { + id: uuid::Uuid::new_v4(), + book_id, + page_number: 1, + file_name: "page001.png".to_string(), + format: "png".to_string(), + width: 800, + height: 1200, + file_size: 50_000, + created_at: chrono::Utc::now(), + }; + PageRepository::create(db, &page).await.unwrap(); + + book_id +} + +#[tokio::test] +async fn page_image_declares_the_png_type_it_sends_for_a_png_page() { + let (db, _temp_dir) = setup_test_db().await; + let dir = TempDir::new().unwrap(); + let book_id = seed_png_page_book(&db, &dir).await; + + let state = create_test_app_state(db.clone()).await; + let token = admin_token(&db, &state).await; + let app = create_test_router_with_app_state(state); + + let request = get_request_with_auth(&format!("/api/v1/books/{book_id}/pages/1"), &token); + let (status, headers, _body) = make_full_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + assert_declared_content_type("get_page_image", &headers, "image/png"); +} + +/// The OPDS page-serving route delegates straight to `get_page_image`, so it +/// answers with the same media types and needs the same declaration. +#[tokio::test] +async fn opds_page_image_declares_the_png_type_it_sends_for_a_png_page() { + let (db, _temp_dir) = setup_test_db().await; + let dir = TempDir::new().unwrap(); + let book_id = seed_png_page_book(&db, &dir).await; + + let state = create_test_app_state(db.clone()).await; + let token = admin_token(&db, &state).await; + let app = create_test_router_with_app_state(state); + + let request = get_request_with_auth(&format!("/opds/books/{book_id}/pages/1"), &token); + let (status, headers, _body) = make_full_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + assert_declared_content_type("opds_book_page_image", &headers, "image/png"); +} + +// ============================================================================ +// Thumbnails: the SVG placeholder, which fires on timing rather than content +// ============================================================================ + +#[tokio::test] +async fn book_thumbnail_declares_the_svg_type_it_sends_for_a_placeholder() { + let (db, _temp_dir) = setup_test_db().await; + let dir = TempDir::new().unwrap(); + let file = dir.path().join("volume.cbz"); + std::fs::write(&file, b"bytes").unwrap(); + + // page_count 0 is the "nothing to render a cover from yet" state a book is + // in between being discovered and being analysed. + let book_id = seed_book(&db, dir.path(), &file, "cbz", 0).await; + + let state = create_test_app_state(db.clone()).await; + let token = admin_token(&db, &state).await; + let app = create_test_router_with_app_state(state); + + let request = get_request_with_auth(&format!("/api/v1/books/{book_id}/thumbnail"), &token); + let (status, headers, _body) = make_full_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + assert_declared_content_type("get_book_thumbnail", &headers, "image/svg+xml"); +} + +#[tokio::test] +async fn series_thumbnail_declares_the_svg_type_it_sends_for_a_placeholder() { + let (db, _temp_dir) = setup_test_db().await; + + let library = LibraryRepository::create( + &db, + "Test Library", + "/tmp/does-not-need-to-exist", + ScanningStrategy::Default, + ) + .await + .unwrap(); + let series = SeriesRepository::create(&db, library.id, "Test Series", None) + .await + .unwrap(); + + let state = create_test_app_state(db.clone()).await; + let token = admin_token(&db, &state).await; + let app = create_test_router_with_app_state(state); + + let request = get_request_with_auth(&format!("/api/v1/series/{}/thumbnail", series.id), &token); + let (status, headers, _body) = make_full_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + assert_declared_content_type("get_series_thumbnail", &headers, "image/svg+xml"); +} + +// ============================================================================ +// Series export download: a text type behind an octet-stream declaration +// ============================================================================ + +#[tokio::test] +async fn export_download_declares_the_csv_type_it_sends_for_a_csv_export() { + let (db, _temp_dir) = setup_test_db().await; + let dir = TempDir::new().unwrap(); + let export_file = dir.path().join("export.csv"); + std::fs::write(&export_file, b"name,year\nTest Series,2026\n").unwrap(); + + let password_hash = password::hash_password("admin123").unwrap(); + let user = create_test_user("admin", "admin@example.com", &password_hash, true); + let user = UserRepository::create(&db, &user).await.unwrap(); + + let export = SeriesExportRepository::create( + &db, + user.id, + "csv", + "series", + json!([]), + json!(["name"]), + None, + chrono::Utc::now() + chrono::Duration::days(1), + ) + .await + .unwrap(); + SeriesExportRepository::mark_completed(&db, export.id, export_file.to_str().unwrap(), 24, 1) + .await + .unwrap(); + + let state = create_test_app_state(db.clone()).await; + let token = state + .jwt_service + .generate_token(user.id, user.username.clone(), user.get_role()) + .unwrap(); + let app = create_test_router_with_app_state(state); + + let request = get_request_with_auth( + &format!("/api/v1/user/exports/series/{}/download", export.id), + &token, + ); + let (status, headers, _body) = make_full_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + assert_declared_content_type("download_export", &headers, "text/csv"); +} + +// ============================================================================ +// The matcher itself +// ============================================================================ + +#[test] +fn wildcard_media_types_cover_their_subtypes() { + assert!(media_type_matches("image/*", "image/svg+xml")); + assert!(media_type_matches("image/*", "image/jpeg")); + assert!(!media_type_matches("image/*", "application/octet-stream")); +} + +#[test] +fn media_type_parameters_are_not_part_of_the_match() { + assert!(media_type_matches("text/csv", "text/csv; charset=utf-8")); + assert!(!media_type_matches( + "text/csv", + "text/markdown; charset=utf-8" + )); +} diff --git a/tests/api/book_file_ranges.rs b/tests/api/book_file_ranges.rs new file mode 100644 index 00000000..7a71ad3e --- /dev/null +++ b/tests/api/book_file_ranges.rs @@ -0,0 +1,496 @@ +//! Range, conditional, and resume behaviour on `GET /api/v1/books/{id}/file`. +//! +//! Two capabilities motivate these, and they fail independently. A download +//! that drops partway has to resume rather than restart, which is `Range` plus +//! a validator. And a client that can read a ZIP central directory with +//! `bytes=-65536` can show page one of a large volume without fetching the rest, +//! which is the suffix form specifically — an implementation that handles only +//! `bytes=a-b` satisfies the first and none of the second. +//! +//! The assertions that matter most are the ones about bytes rather than status +//! codes: a 206 with the right `Content-Range` and the wrong slice of the file +//! is worse than no range support at all, because it corrupts a resumed +//! download silently. + +#[path = "../common/mod.rs"] +mod common; + +use codex::db::ScanningStrategy; +use codex::db::entities::user_sharing_tags::AccessMode; +use codex::db::repositories::{ + BookRepository, LibraryRepository, SeriesRepository, SharingTagRepository, UserRepository, +}; +use codex::utils::password; +use common::*; +use hyper::StatusCode; +use hyper::header; +use tempfile::TempDir; + +/// Big enough that a suffix range is a meaningfully small slice of it, and +/// patterned so a wrong offset shows up as wrong bytes rather than as a +/// plausible-looking blob. +const FILE_LEN: usize = 8192; + +fn file_bytes() -> Vec { + (0..FILE_LEN).map(|i| (i % 251) as u8).collect() +} + +async fn admin_token( + db: &sea_orm::DatabaseConnection, + state: &codex::api::extractors::AppState, +) -> String { + let password_hash = password::hash_password("admin123").unwrap(); + let user = create_test_user("admin", "admin@example.com", &password_hash, true); + let created = UserRepository::create(db, &user).await.unwrap(); + + state + .jwt_service + .generate_token(created.id, created.username.clone(), created.get_role()) + .unwrap() +} + +struct Fixture { + db: sea_orm::DatabaseConnection, + book_id: uuid::Uuid, + file_hash: String, + token: String, + state: std::sync::Arc, + _temp_db: TempDir, + _dir: TempDir, +} + +impl Fixture { + fn app(&self) -> axum::Router { + create_test_router_with_app_state(self.state.clone()) + } +} + +/// A book whose file is on disk, with a known `file_hash` so the ETag is +/// predictable. +async fn fixture_with_name(file_name: &str) -> Fixture { + let (db, _temp_db) = setup_test_db().await; + let dir = TempDir::new().unwrap(); + let path = dir.path().join(file_name); + std::fs::write(&path, file_bytes()).unwrap(); + + let library = LibraryRepository::create( + &db, + "Test Library", + dir.path().to_str().unwrap(), + ScanningStrategy::Default, + ) + .await + .unwrap(); + let series = SeriesRepository::create(&db, library.id, "Test Series", None) + .await + .unwrap(); + + let file_hash = "0123456789abcdef0123456789abcdef".to_string(); + let book = codex::db::entities::books::Model { + id: uuid::Uuid::new_v4(), + series_id: series.id, + library_id: library.id, + path: path.to_str().unwrap().to_string(), + file_name: file_name.to_string(), + file_size: FILE_LEN as i64, + file_hash: file_hash.clone(), + partial_hash: String::new(), + format: "cbz".to_string(), + page_count: 1, + deleted: false, + analyzed: true, + analysis_error: None, + analysis_errors: None, + modified_at: chrono::Utc::now(), + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + thumbnail_path: None, + thumbnail_generated_at: None, + koreader_hash: None, + epub_positions: None, + epub_spine_items: None, + }; + let book = BookRepository::create(&db, &book, None).await.unwrap(); + + let state = create_test_app_state(db.clone()).await; + let token = admin_token(&db, &state).await; + + Fixture { + db, + book_id: book.id, + file_hash, + token, + state, + _temp_db, + _dir: dir, + } +} + +async fn fixture() -> Fixture { + fixture_with_name("volume.cbz").await +} + +/// Issue a request with an arbitrary set of extra headers. +async fn get_file_with( + f: &Fixture, + extra: &[(header::HeaderName, &str)], +) -> (StatusCode, hyper::HeaderMap, Vec) { + let mut request = get_request_with_auth(&format!("/api/v1/books/{}/file", f.book_id), &f.token); + for (name, value) in extra { + request + .headers_mut() + .insert(name.clone(), value.parse().unwrap()); + } + make_full_request(f.app(), request).await +} + +fn header_str(headers: &hyper::HeaderMap, name: header::HeaderName) -> Option { + headers + .get(name) + .map(|v| v.to_str().expect("header is ASCII").to_string()) +} + +// ============================================================================ +// The unranged response, which must not have moved +// ============================================================================ + +#[tokio::test] +async fn a_request_without_a_range_still_returns_the_whole_file() { + let f = fixture().await; + let (status, headers, body) = get_file_with(&f, &[]).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(body, file_bytes()); + assert_eq!( + header_str(&headers, header::CONTENT_LENGTH), + Some(FILE_LEN.to_string()) + ); + assert_eq!( + header_str(&headers, header::CONTENT_TYPE), + Some("application/zip".to_string()) + ); + assert!( + headers.get(header::CONTENT_RANGE).is_none(), + "a 200 must not carry Content-Range" + ); +} + +/// The capability is a property of the resource, so it is advertised whether or +/// not the request asked for a range. A client that cannot see `Accept-Ranges` +/// on the plain response will not try to resume. +#[tokio::test] +async fn every_response_advertises_range_support_and_a_validator() { + let f = fixture().await; + + for extra in [vec![], vec![(header::RANGE, "bytes=0-9")]] { + let (_, headers, _) = get_file_with(&f, &extra).await; + assert_eq!( + header_str(&headers, header::ACCEPT_RANGES), + Some("bytes".to_string()), + "{extra:?}" + ); + assert!( + headers.get(header::ETAG).is_some(), + "no ETag, so a resume has nothing to pin to: {extra:?}" + ); + assert!(headers.get(header::LAST_MODIFIED).is_some(), "{extra:?}"); + } +} + +/// `books.file_hash` is a non-null column the scanner already computes, so it +/// costs no I/O, survives a rescan, and survives the file moving on disk. An +/// mtime validator would do none of those. +#[tokio::test] +async fn the_etag_is_the_stored_file_hash() { + let f = fixture().await; + let (_, headers, _) = get_file_with(&f, &[]).await; + + assert_eq!( + header_str(&headers, header::ETAG), + Some(format!("\"{}\"", f.file_hash)) + ); +} + +// ============================================================================ +// The three range forms +// ============================================================================ + +#[tokio::test] +async fn a_closed_range_returns_exactly_those_bytes() { + let f = fixture().await; + let (status, headers, body) = get_file_with(&f, &[(header::RANGE, "bytes=100-199")]).await; + + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert_eq!( + header_str(&headers, header::CONTENT_RANGE), + Some(format!("bytes 100-199/{FILE_LEN}")) + ); + assert_eq!( + header_str(&headers, header::CONTENT_LENGTH), + Some("100".to_string()) + ); + assert_eq!(body, file_bytes()[100..=199]); +} + +#[tokio::test] +async fn an_open_ended_range_runs_to_the_end_of_the_file() { + let f = fixture().await; + let (status, headers, body) = get_file_with(&f, &[(header::RANGE, "bytes=8000-")]).await; + + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert_eq!( + header_str(&headers, header::CONTENT_RANGE), + Some(format!("bytes 8000-{}/{FILE_LEN}", FILE_LEN - 1)) + ); + assert_eq!(body, file_bytes()[8000..]); +} + +/// The form that reads a ZIP central directory without downloading the archive. +#[tokio::test] +async fn a_suffix_range_returns_the_tail_of_the_file() { + let f = fixture().await; + let (status, headers, body) = get_file_with(&f, &[(header::RANGE, "bytes=-512")]).await; + + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert_eq!( + header_str(&headers, header::CONTENT_RANGE), + Some(format!( + "bytes {}-{}/{FILE_LEN}", + FILE_LEN - 512, + FILE_LEN - 1 + )) + ); + assert_eq!(body, file_bytes()[FILE_LEN - 512..]); +} + +/// The assertion that actually protects a resumed download: several ranged +/// reads, concatenated, must be byte-identical to the whole file. Offsets that +/// are individually plausible but collectively wrong show up here and nowhere +/// else. +#[tokio::test] +async fn ranged_reads_reassemble_into_the_original_file() { + let f = fixture().await; + let mut assembled = Vec::new(); + + for spec in ["bytes=0-2047", "bytes=2048-6143", "bytes=6144-"] { + let (status, _, body) = get_file_with(&f, &[(header::RANGE, spec)]).await; + assert_eq!(status, StatusCode::PARTIAL_CONTENT, "{spec}"); + assembled.extend_from_slice(&body); + } + + assert_eq!(assembled, file_bytes()); +} + +#[tokio::test] +async fn a_last_byte_past_the_end_is_clamped_rather_than_rejected() { + let f = fixture().await; + let (status, headers, body) = get_file_with(&f, &[(header::RANGE, "bytes=8100-99999")]).await; + + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert_eq!( + header_str(&headers, header::CONTENT_RANGE), + Some(format!("bytes 8100-{}/{FILE_LEN}", FILE_LEN - 1)) + ); + assert_eq!(body, file_bytes()[8100..]); +} + +// ============================================================================ +// Ranges that cannot be served +// ============================================================================ + +#[tokio::test] +async fn an_unsatisfiable_range_returns_416_with_the_full_length() { + let f = fixture().await; + let (status, headers, _) = get_file_with(&f, &[(header::RANGE, "bytes=99999-")]).await; + + assert_eq!(status, StatusCode::RANGE_NOT_SATISFIABLE); + assert_eq!( + header_str(&headers, header::CONTENT_RANGE), + Some(format!("bytes */{FILE_LEN}")), + "416 must tell the client the real length so it can retry correctly" + ); +} + +/// RFC 9110 permits ignoring a `Range` the server does not wish to satisfy, and +/// a multi-range request would need a multipart/byteranges body no client here +/// asks for. The whole file remains a correct answer. +#[tokio::test] +async fn a_multi_range_request_falls_back_to_the_whole_file() { + let f = fixture().await; + let (status, headers, body) = get_file_with(&f, &[(header::RANGE, "bytes=0-99,200-299")]).await; + + assert_eq!(status, StatusCode::OK); + assert!(headers.get(header::CONTENT_RANGE).is_none()); + assert_eq!(body, file_bytes()); +} + +#[tokio::test] +async fn a_malformed_range_falls_back_to_the_whole_file() { + let f = fixture().await; + for spec in ["bytes=not-a-range", "chapters=1-2", "bytes=500-100"] { + let (status, _, body) = get_file_with(&f, &[(header::RANGE, spec)]).await; + assert_eq!(status, StatusCode::OK, "{spec}"); + assert_eq!(body, file_bytes(), "{spec}"); + } +} + +// ============================================================================ +// Conditional requests +// ============================================================================ + +#[tokio::test] +async fn a_current_validator_returns_304_without_a_body() { + let f = fixture().await; + let etag = format!("\"{}\"", f.file_hash); + let (status, headers, body) = get_file_with(&f, &[(header::IF_NONE_MATCH, &etag)]).await; + + assert_eq!(status, StatusCode::NOT_MODIFIED); + assert!(body.is_empty()); + assert_eq!(header_str(&headers, header::ETAG), Some(etag)); +} + +#[tokio::test] +async fn a_stale_validator_returns_the_file() { + let f = fixture().await; + let (status, _, body) = get_file_with(&f, &[(header::IF_NONE_MATCH, "\"not-the-hash\"")]).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(body, file_bytes()); +} + +#[tokio::test] +async fn if_range_with_a_current_validator_serves_the_range() { + let f = fixture().await; + let etag = format!("\"{}\"", f.file_hash); + let (status, headers, body) = get_file_with( + &f, + &[(header::RANGE, "bytes=0-99"), (header::IF_RANGE, &etag)], + ) + .await; + + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert_eq!( + header_str(&headers, header::CONTENT_RANGE), + Some(format!("bytes 0-99/{FILE_LEN}")) + ); + assert_eq!(body, file_bytes()[0..=99]); +} + +/// The case that keeps a resume from corrupting a download. If the file changed +/// since the client started, splicing fresh bytes into its partial copy would +/// produce a file that is neither version, so the whole representation has to go +/// out instead. +#[tokio::test] +async fn if_range_with_a_stale_validator_returns_the_whole_file() { + let f = fixture().await; + let (status, headers, body) = get_file_with( + &f, + &[ + (header::RANGE, "bytes=0-99"), + (header::IF_RANGE, "\"a-hash-from-before-the-rescan\""), + ], + ) + .await; + + assert_eq!(status, StatusCode::OK); + assert!(headers.get(header::CONTENT_RANGE).is_none()); + assert_eq!(body, file_bytes()); +} + +// ============================================================================ +// Access control still runs first +// ============================================================================ + +#[tokio::test] +async fn a_range_request_still_requires_authentication() { + let f = fixture().await; + let mut request = get_request(&format!("/api/v1/books/{}/file", f.book_id)); + request + .headers_mut() + .insert(header::RANGE, "bytes=0-99".parse().unwrap()); + let (status, _) = make_request(f.app(), request).await; + + assert_eq!(status, StatusCode::UNAUTHORIZED); +} + +/// A 206 must never be a way around the checks a 200 has to pass. The book is +/// hidden from this user by the content filter, so the range must not be served +/// even though the bytes are on disk. +#[tokio::test] +async fn a_range_request_cannot_see_a_book_the_content_filter_hides() { + let f = fixture().await; + + let password_hash = password::hash_password("user123").unwrap(); + let restricted = create_test_user("reader", "reader@example.com", &password_hash, false); + let restricted = UserRepository::create(&f.db, &restricted).await.unwrap(); + + // Tag the series and deny this user that tag, which is how the content + // filter hides a book that is otherwise perfectly readable. + let tag = SharingTagRepository::create(&f.db, "restricted", None) + .await + .unwrap(); + let book = BookRepository::get_by_id(&f.db, f.book_id) + .await + .unwrap() + .unwrap(); + SharingTagRepository::add_tag_to_series(&f.db, book.series_id, tag.id) + .await + .unwrap(); + SharingTagRepository::set_user_grant(&f.db, restricted.id, tag.id, AccessMode::Deny) + .await + .unwrap(); + + let token = f + .state + .jwt_service + .generate_token( + restricted.id, + restricted.username.clone(), + restricted.get_role(), + ) + .unwrap(); + + let mut request = get_request_with_auth(&format!("/api/v1/books/{}/file", f.book_id), &token); + request + .headers_mut() + .insert(header::RANGE, "bytes=0-99".parse().unwrap()); + let (status, _) = make_request(f.app(), request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +// ============================================================================ +// Content-Disposition +// ============================================================================ + +/// The v1 route emitted a bare `filename="..."`, which mangles any non-ASCII +/// name. The Komga copy already encoded per RFC 5987; this route now does too. +#[tokio::test] +async fn a_non_ascii_filename_is_encoded_rather_than_mangled() { + let f = fixture_with_name("漫画 Vol 1.cbz").await; + let (status, headers, _) = get_file_with(&f, &[]).await; + + assert_eq!(status, StatusCode::OK); + let disposition = header_str(&headers, header::CONTENT_DISPOSITION).expect("disposition"); + assert!( + disposition.contains("filename*=UTF-8''"), + "expected an RFC 5987 encoded name, got {disposition}" + ); + assert!( + disposition.contains("%E6%BC%AB%E7%94%BB"), + "expected the name percent-encoded, got {disposition}" + ); +} + +#[tokio::test] +async fn a_partial_response_carries_the_same_disposition_as_a_full_one() { + let f = fixture().await; + let (_, full, _) = get_file_with(&f, &[]).await; + let (_, partial, _) = get_file_with(&f, &[(header::RANGE, "bytes=0-9")]).await; + + assert_eq!( + header_str(&full, header::CONTENT_DISPOSITION), + header_str(&partial, header::CONTENT_DISPOSITION), + "a resumed download must name the same file as the one it resumes" + ); +} diff --git a/tests/api/books.rs b/tests/api/books.rs index 1e8180c6..bceb3d4d 100644 --- a/tests/api/books.rs +++ b/tests/api/books.rs @@ -5091,3 +5091,136 @@ async fn test_delete_book_external_link_book_not_found() { assert_eq!(status, StatusCode::NOT_FOUND); } + +// ============================================================================ +// GET /api/v1/books/{book_id}/full and /api/v1/series/{series_id}/full +// ============================================================================ +// +// These exist because a response whose schema depends on a query parameter +// cannot be expressed in OpenAPI, so `?full=true` is invisible to a generated +// client. The tests below pin the two properties that matter: the route returns +// the full shape, and it is the *same* shape `?full=true` returns, so the +// deprecated parameter can be removed later without changing any payload. + +/// Drop the metadata row's own timestamps before comparing two responses. +/// +/// A book with no metadata row gets one created on read, so two requests +/// produce two different `createdAt`/`updatedAt` values. That is a per-request +/// timestamp rather than a difference in shape, and comparing it would make the +/// test fail for a reason it is not about. +fn strip_metadata_timestamps(body: Option) -> Option { + let mut body = body?; + if let Some(metadata) = body.get_mut("metadata").and_then(|m| m.as_object_mut()) { + metadata.remove("createdAt"); + metadata.remove("updatedAt"); + } + Some(body) +} + +#[tokio::test] +async fn full_book_route_matches_the_deprecated_full_query_parameter() { + let (db, _temp_dir) = setup_test_db().await; + let library = + LibraryRepository::create(&db, "Test Library", "/test", ScanningStrategy::Default) + .await + .unwrap(); + let series = SeriesRepository::create(&db, library.id, "Test Series", None) + .await + .unwrap(); + let book = create_test_book_model( + series.id, + library.id, + "/test/book1.cbz", + "book1.cbz", + Some("Book 1".to_string()), + ); + let book = BookRepository::create(&db, &book, None).await.unwrap(); + + let state = create_test_auth_state(db.clone()).await; + let token = create_admin_and_token(&db, &state).await; + + let (route_status, route_body): (StatusCode, Option) = make_json_request( + create_test_router(state.clone()).await, + get_request_with_auth(&format!("/api/v1/books/{}/full", book.id), &token), + ) + .await; + let (query_status, query_body): (StatusCode, Option) = make_json_request( + create_test_router(state).await, + get_request_with_auth(&format!("/api/v1/books/{}?full=true", book.id), &token), + ) + .await; + + assert_eq!(route_status, StatusCode::OK); + assert_eq!(query_status, StatusCode::OK); + assert_eq!( + strip_metadata_timestamps(route_body.clone()), + strip_metadata_timestamps(query_body), + "the dedicated route must return exactly what ?full=true returns, or \ + removing the parameter in 3.0 would be a payload change" + ); + + // Fields that only exist on the full shape, so this cannot pass against the + // plain BookDto. + let body = route_body.unwrap(); + for field in ["metadata", "genres", "tags", "readCount"] { + assert!(body.get(field).is_some(), "full book is missing {field}"); + } +} + +#[tokio::test] +async fn full_series_route_matches_the_deprecated_full_query_parameter() { + let (db, _temp_dir) = setup_test_db().await; + let library = + LibraryRepository::create(&db, "Test Library", "/test", ScanningStrategy::Default) + .await + .unwrap(); + let series = SeriesRepository::create(&db, library.id, "Test Series", None) + .await + .unwrap(); + + let state = create_test_auth_state(db.clone()).await; + let token = create_admin_and_token(&db, &state).await; + + let (route_status, route_body): (StatusCode, Option) = make_json_request( + create_test_router(state.clone()).await, + get_request_with_auth(&format!("/api/v1/series/{}/full", series.id), &token), + ) + .await; + let (query_status, query_body): (StatusCode, Option) = make_json_request( + create_test_router(state).await, + get_request_with_auth(&format!("/api/v1/series/{}?full=true", series.id), &token), + ) + .await; + + assert_eq!(route_status, StatusCode::OK); + assert_eq!(query_status, StatusCode::OK); + assert_eq!( + route_body, query_body, + "the dedicated route must return exactly what ?full=true returns" + ); + + let body = route_body.unwrap(); + for field in ["metadata", "genres", "tags", "externalIds", "externalLinks"] { + assert!(body.get(field).is_some(), "full series is missing {field}"); + } +} + +#[tokio::test] +async fn full_routes_404_on_a_missing_entity() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let token = create_admin_and_token(&db, &state).await; + let missing = uuid::Uuid::new_v4(); + + for path in [ + format!("/api/v1/books/{missing}/full"), + format!("/api/v1/series/{missing}/full"), + ] { + let (status, _) = make_request( + create_test_router(state.clone()).await, + get_request_with_auth(&path, &token), + ) + .await; + assert_eq!(status, StatusCode::NOT_FOUND, "{path}"); + } +} diff --git a/tests/api/mod.rs b/tests/api/mod.rs index 5716de1d..1f75795b 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -9,6 +9,8 @@ mod alternate_titles; mod analyze; mod api_keys; mod auth; +mod binary_content_types; +mod book_file_ranges; mod books; mod bulk_metadata; mod bulk_operations; diff --git a/tests/api/openapi_spec.rs b/tests/api/openapi_spec.rs index 37f7feac..3ca8c14b 100644 --- a/tests/api/openapi_spec.rs +++ b/tests/api/openapi_spec.rs @@ -213,3 +213,398 @@ fn absent_value_endpoints_document_204() { ); } } + +/// A generic DTO named through a `pub type` alias loses its type argument and +/// collapses to the base component, which then holds whichever instantiation the +/// schema registry happened to render into that slot. Four series endpoints were +/// documented as returning books this way, and the document generated without a +/// warning: the alias `SeriesListResponse` emitted `$ref: PaginatedResponse`, and +/// the bare `PaginatedResponse` carried `data: [BookDto]`. +/// +/// The signature is a component whose name is the bare prefix of one or more +/// `Base_Argument` components in the same document. Referencing the bare form is +/// never what the handler means, so a `body =` position must name the generic +/// inline (`PaginatedResponse`) rather than through an alias. +#[test] +fn no_operation_references_an_unparameterised_generic_wrapper() { + let spec = spec(); + let schemas = spec["components"]["schemas"] + .as_object() + .expect("schemas object"); + + // Bases that the registry has rendered at least one concrete instantiation for. + let parameterised: Vec<&str> = schemas + .keys() + .filter_map(|name| name.split_once('_').map(|(base, _)| base)) + .collect(); + + let mut collapsed: Vec = Vec::new(); + for (operation, op) in operations(&spec) { + for (_, node) in all_nodes(op) { + let Some(reference) = node.get("$ref").and_then(Value::as_str) else { + continue; + }; + let Some(name) = reference.strip_prefix("#/components/schemas/") else { + continue; + }; + if parameterised.contains(&name) { + collapsed.push(format!("{} -> {}", operation, name)); + } + } + } + collapsed.sort(); + collapsed.dedup(); + + assert!( + collapsed.is_empty(), + "operations reference a generic wrapper with its type argument discarded, \ + so they document whichever instantiation the registry rendered rather than \ + their own: {:#?}", + collapsed + ); +} + +/// A parameter documented as `in: path` but absent from the path template +/// describes a call that cannot be constructed, and a `{placeholder}` with no +/// declared parameter describes one that cannot be filled in. Both halves broke +/// when four handlers annotated an `IntoParams` struct they never extracted: +/// `page` and `pageSize` rendered as path parameters of a path that has no such +/// segments. +#[test] +fn path_parameters_match_their_path_template() { + let spec = spec(); + let mut mismatches: Vec = Vec::new(); + + for (path, item) in spec["paths"].as_object().expect("paths object") { + let template: Vec<&str> = path + .split('/') + .filter_map(|segment| segment.strip_prefix('{')?.strip_suffix('}')) + .collect(); + + for (method, op) in item.as_object().expect("path item object") { + if !matches!( + method.as_str(), + "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace" + ) { + continue; + } + let declared: Vec<&str> = op + .get("parameters") + .and_then(Value::as_array) + .map(|params| { + params + .iter() + .filter(|p| p.get("in").and_then(Value::as_str) == Some("path")) + .filter_map(|p| p.get("name").and_then(Value::as_str)) + .collect() + }) + .unwrap_or_default(); + + let operation = format!("{} {}", method.to_uppercase(), path); + for name in &declared { + if !template.contains(name) { + mismatches.push(format!( + "{}: declares path parameter `{}` that the template does not contain", + operation, name + )); + } + } + for name in &template { + if !declared.contains(name) { + mismatches.push(format!( + "{}: template contains `{{{}}}` with no declared path parameter", + operation, name + )); + } + } + } + } + mismatches.sort(); + + assert!( + mismatches.is_empty(), + "path parameters and path templates disagree: {:#?}", + mismatches + ); +} + +/// The filter grammar is the primary query interface: `POST /series/list` and +/// `POST /books/list` route all filtering through `condition`. Both fields once +/// carried `#[schema(value_type = Option)]`, which erased a fully +/// modelled recursive grammar to a bare untyped object, so a generated client got +/// a freeform dictionary for the one field that carries the actual query and the +/// grammar was discoverable only by reading the Rust source. +/// +/// The condition schemas are recursive, which is legal OpenAPI and is what the +/// override was presumably working around. Asserting the `$ref` keeps that +/// workaround from being reintroduced quietly. +#[test] +fn filter_conditions_reference_their_grammar() { + let spec = spec(); + + for (request, condition) in [ + ("SeriesListRequest", "SeriesCondition"), + ("BookListRequest", "BookCondition"), + ] { + let property = &spec["components"]["schemas"][request]["properties"]["condition"]; + assert_eq!( + property.get("$ref").and_then(Value::as_str), + Some(format!("#/components/schemas/{}", condition).as_str()), + "{}.condition should reference {} rather than erasing the grammar, got {}", + request, + condition, + property + ); + assert!( + spec["components"]["schemas"] + .get(condition) + .is_some_and(|schema| schema.get("oneOf").is_some()), + "{} should be modelled as a oneOf over its combinators and predicates", + condition + ); + } +} + +/// Components no operation can reach, transitively, that are known and accepted. +/// +/// A component nothing references is the signature this codebase's worst OpenAPI +/// defects have shared: `PaginatedResponse_SeriesDto`, `SeriesCondition` and the +/// five filter operator types were all correct schemas, generated faithfully, +/// and wired to nothing, which is precisely why nobody noticed for two releases. +/// The check below fails on any orphan that is not listed here, so a new one has +/// to be looked at rather than absorbed. +/// +/// Every name is a decision, grouped by why it is acceptable. Adding a name is +/// cheap; adding one without reading which group it belongs in is how this list +/// stops meaning anything. +const ACCEPTED_UNREFERENCED_COMPONENTS: &[&str] = &[ + // -- The generic wrapper's own base ------------------------------------- + // Rendered from `PaginatedResponse`'s `ToSchema` derive rather than from + // any reference. Every operation names a concrete instantiation, so nothing + // points here. An operation that *did* reference it would be the generic + // alias defect, which `no_operation_references_an_unparameterised_generic_wrapper` + // catches separately. + "PaginatedResponse", + // -- `IntoParams` query structs ----------------------------------------- + // These render as inline `parameters` entries, never as a `$ref`, so being + // an unreferenced schema is their normal state. They are on this list only + // because they are also registered in `docs.rs` `schemas()`, which is + // unnecessary but harmless. + "BooksPaginationQuery", + "ListFilterPresetsQuery", + "ListSettingsQuery", + "OrphanStatsQuery", + "SeriesPaginationQuery", + "SyncStatusQuery", + "TriggerScanQuery", + "UserPluginTasksQuery", + // -- Server-sent event payloads ----------------------------------------- + // A `text/event-stream` response body carries no schema, so the types that + // describe the events on the wire are unreachable by construction. That the + // event shapes are undocumented is a real gap, but it is a gap in how SSE is + // described, not an accidentally-unwired schema. + "EntityChangeEvent", + "EntityEvent", + "EntityType", + "TaskProgress", + "TaskProgressEvent", + "TaskStatus", + // The library-jobs DTOs used to sit here, because none of their routes + // carried a `#[utoipa::path]` and so nothing could reach them. This check + // is what surfaced that; the routes are documented now and all fourteen + // names have left the list. + // The `full=true` shapes used to sit here, unreachable because no operation + // could name a response whose schema depends on a query parameter. + // `GET /books/{book_id}/full` and `GET /series/{series_id}/full` describe + // them properly now, so all four have left the list. + // -- Not on the HTTP surface at all ------------------------------------- + // Scanner and access-control internals that derive `ToSchema` for reasons of + // their own and get swept into the registry. No handler takes or returns + // them. + "AnalysisResult", + "CalibreSeriesMode", + "CalibreStrategyConfig", + "CustomStrategyConfig", + "FlatStrategyConfig", + "MembershipSource", + "PublisherHierarchyConfig", + "SmartBookConfig", + // -- Metadata preprocessing template context ---------------------------- + // The context object handed to plugin preprocessing templates. It is a + // plugin-facing shape rather than a request or response body, so nothing in + // `paths` reaches it. + "AlternateTitleContextDto", + "AuthorContextDto", + "BookAwardContextDto", + "BookContextDto", + "BookMetadataContextDto", + "ExternalIdContextDto", + "ExternalLinkContextDto", + "ExternalRatingContextDto", + "MetadataContextDto", + "SeriesContextDto", + // `SeriesExternalIndexDto` used to sit here, unreachable because its + // paginated wrapper inlined it rather than emitting a `$ref`. Every generic + // wrapper did that; `GenericArgumentReferencer` now collapses the copies + // back to references, so it is reachable and has left the list. + // -- Registered but referenced by no handler ---------------------------- + // Dead DTOs. Each is named in a `components(schemas(...))` list or in + // `docs.rs`, and no operation returns it. `SharingTagListResponse` is + // superseded by `PaginatedResponse`; `TokenResponse` is + // superseded by `LoginResponse` and `TokenPair`; `ReleaseLedgerListResponse` + // is kept alive only by `_opening_api_keepalive()`; the OIDC pair describes + // a callback that redirects rather than answering with a body. + "OidcCallbackResponse", + "OidcErrorResponse", + "PluginSearchResponse", + "ReleaseLedgerListResponse", + "ReprocessLibraryTitlesResponse", + "ReprocessTitleResult", + "SharingTagListResponse", + "TokenResponse", +]; + +/// Every schema component reachable from `paths`, following `$ref`s transitively. +/// +/// Transitivity is the whole point. `SeriesCondition` references itself and the +/// operator schemas, so a direct-reference count finds them all "used" and +/// misses that no operation can reach any of them. +fn components_reachable_from_operations(spec: &Value) -> std::collections::BTreeSet { + fn collect_refs(node: &Value, out: &mut std::collections::BTreeSet) { + match node { + Value::Object(map) => { + if let Some(reference) = map.get("$ref").and_then(Value::as_str) + && let Some(name) = reference.strip_prefix("#/components/schemas/") + { + out.insert(name.to_string()); + } + for value in map.values() { + collect_refs(value, out); + } + } + Value::Array(items) => { + for value in items { + collect_refs(value, out); + } + } + _ => {} + } + } + + let schemas = spec["components"]["schemas"] + .as_object() + .expect("schemas object"); + + let mut queue = std::collections::BTreeSet::new(); + collect_refs(&spec["paths"], &mut queue); + + let mut reached = std::collections::BTreeSet::new(); + while let Some(name) = queue.pop_first() { + if !reached.insert(name.clone()) { + continue; + } + if let Some(schema) = schemas.get(&name) { + let mut next = std::collections::BTreeSet::new(); + collect_refs(schema, &mut next); + queue.extend(next.difference(&reached).cloned()); + } + } + + reached +} + +/// A schema no operation can reach documents nothing. The four defects fixed on +/// this branch all announced themselves this way and nobody was looking, so this +/// pins the set: a new orphan fails until someone decides which group above it +/// belongs to, and an allowlisted name that becomes reachable fails too, so the +/// list cannot quietly rot into a list of things that used to be true. +#[test] +fn unreferenced_components_are_all_accounted_for() { + let spec = spec(); + let reached = components_reachable_from_operations(&spec); + + let all: std::collections::BTreeSet = spec["components"]["schemas"] + .as_object() + .expect("schemas object") + .keys() + .cloned() + .collect(); + + let accepted: std::collections::BTreeSet = ACCEPTED_UNREFERENCED_COMPONENTS + .iter() + .map(|name| name.to_string()) + .collect(); + + let unexpected: Vec<&String> = all + .difference(&reached) + .filter(|n| !accepted.contains(*n)) + .collect(); + assert!( + unexpected.is_empty(), + "no operation can reach these components, and they are not in \ + ACCEPTED_UNREFERENCED_COMPONENTS. Either an operation should reference one and \ + does not, or the list needs a new entry saying why not: {:#?}", + unexpected + ); + + let stale: Vec<&String> = accepted + .iter() + .filter(|n| reached.contains(*n) || !all.contains(*n)) + .collect(); + assert!( + stale.is_empty(), + "ACCEPTED_UNREFERENCED_COMPONENTS names components that are now reachable, or that \ + no longer exist. Remove them so the list keeps describing the document: {:#?}", + stale + ); +} + +/// utoipa renders a generic instantiation by expanding its type argument inline +/// rather than referencing the argument's own component, even when that +/// component is registered. The document stays correct, but every generator +/// that names inline schemas produces a distinct nominal type per wrapper: +/// `swift-openapi-generator` yields +/// `PaginatedResponseSeriesDto.DataPayloadPayload` where `getSeries` yields +/// `SeriesDto`, structurally identical and not interchangeable. +/// +/// The cost lands on the consumer, once per paginated endpoint, which is why +/// this is asserted rather than left to be rediscovered. +#[test] +fn generic_wrappers_reference_their_type_argument() { + let spec = spec(); + let schemas = spec["components"]["schemas"] + .as_object() + .expect("schemas object"); + + let mut inlined: Vec = Vec::new(); + for (name, schema) in schemas { + // `Base_Argument`, the shape utoipa emits for a concrete instantiation. + let Some((_, argument)) = name.split_once('_') else { + continue; + }; + let Some(argument_schema) = schemas.get(argument) else { + continue; + }; + + // An inlined argument is a subschema byte-identical to the component it + // should have referenced. + let reference = format!("#/components/schemas/{}", argument); + for (pointer, node) in all_nodes(schema) { + if node.get("$ref").and_then(Value::as_str) == Some(reference.as_str()) { + continue; + } + if &node == argument_schema { + inlined.push(format!("{} at {}", name, pointer)); + } + } + } + inlined.sort(); + + assert!( + inlined.is_empty(), + "these generic wrappers expand their type argument inline instead of \ + referencing its component, so a generated client gets a distinct type \ + per wrapper rather than the shared DTO: {:#?}", + inlined + ); +} diff --git a/tests/api/search.rs b/tests/api/search.rs index 300f320a..b6586dd8 100644 --- a/tests/api/search.rs +++ b/tests/api/search.rs @@ -114,6 +114,7 @@ async fn seed_series( } #[tokio::test] +#[allow(deprecated)] // exercises the deprecated `full` parameter async fn fuzzy_search_matches_gap_skipped_query() { let (db, _temp_dir) = setup_test_db().await; let library = LibraryRepository::create(&db, "Library", "/lib", ScanningStrategy::Default) @@ -151,6 +152,7 @@ async fn fuzzy_search_matches_gap_skipped_query() { } #[tokio::test] +#[allow(deprecated)] // exercises the deprecated `full` parameter async fn fuzzy_search_ignores_punctuation_between_words() { let (db, _temp_dir) = setup_test_db().await; let library = LibraryRepository::create(&db, "Library", "/lib", ScanningStrategy::Default) @@ -186,6 +188,7 @@ async fn fuzzy_search_ignores_punctuation_between_words() { } #[tokio::test] +#[allow(deprecated)] // exercises the deprecated `full` parameter async fn fuzzy_search_empty_query_returns_no_results() { let (db, _temp_dir) = setup_test_db().await; let library = LibraryRepository::create(&db, "Library", "/lib", ScanningStrategy::Default) @@ -217,6 +220,7 @@ async fn fuzzy_search_empty_query_returns_no_results() { } #[tokio::test] +#[allow(deprecated)] // exercises the deprecated `full` parameter async fn fuzzy_search_respects_library_filter() { let (db, _temp_dir) = setup_test_db().await; let library_a = LibraryRepository::create(&db, "Manga", "/manga", ScanningStrategy::Default) @@ -263,6 +267,7 @@ async fn fuzzy_search_respects_library_filter() { } #[tokio::test] +#[allow(deprecated)] // exercises the deprecated `full` parameter async fn fuzzy_flag_off_falls_back_to_like_path() { let (db, _temp_dir) = setup_test_db().await; let library = LibraryRepository::create(&db, "Library", "/lib", ScanningStrategy::Default) diff --git a/tests/api/series.rs b/tests/api/series.rs index 22500f13..aab6d377 100644 --- a/tests/api/series.rs +++ b/tests/api/series.rs @@ -1045,6 +1045,7 @@ async fn test_get_series_not_found() { // ============================================================================ #[tokio::test] +#[allow(deprecated)] // exercises the deprecated `full` parameter async fn test_search_series_by_name() { let (db, _temp_dir) = setup_test_db().await; @@ -1083,6 +1084,7 @@ async fn test_search_series_by_name() { } #[tokio::test] +#[allow(deprecated)] // exercises the deprecated `full` parameter async fn test_search_series_no_results() { let (db, _temp_dir) = setup_test_db().await; @@ -1114,6 +1116,7 @@ async fn test_search_series_no_results() { } #[tokio::test] +#[allow(deprecated)] // exercises the deprecated `full` parameter async fn test_search_series_without_auth() { let (db, _temp_dir) = setup_test_db().await; let state = create_test_auth_state(db).await; @@ -5251,6 +5254,7 @@ async fn test_list_series_filtered_with_full() { } #[tokio::test] +#[allow(deprecated)] // exercises the deprecated `full` parameter async fn test_search_series_with_full() { use codex::api::routes::v1::dto::series::FullSeriesResponse; @@ -7962,3 +7966,128 @@ async fn test_list_series_single_library_is_condition_unchanged() { assert_eq!(series_list.data.len(), 1); assert_eq!(series_list.data[0].title, "Lib1 Series"); } + +// ============================================================================ +// GET /api/v1/series/full +// ============================================================================ + +/// The dedicated listing must return exactly what `?full=true` returns. Shisho +/// pages this query across a whole library for its candidate pools, so a +/// difference here would be a silent data change for it, not just a shape one. +#[tokio::test] +async fn full_series_listing_matches_the_deprecated_full_query_parameter() { + let (db, _temp_dir) = setup_test_db().await; + let library = + LibraryRepository::create(&db, "Test Library", "/test", ScanningStrategy::Default) + .await + .unwrap(); + for name in ["Alpha", "Beta", "Gamma"] { + SeriesRepository::create(&db, library.id, name, None) + .await + .unwrap(); + } + + let state = create_test_auth_state(db.clone()).await; + let token = create_admin_and_token(&db, &state).await; + + let (route_status, route_body): (StatusCode, Option) = make_json_request( + create_test_router(state.clone()).await, + get_request_with_auth("/api/v1/series/full?page=1&pageSize=50", &token), + ) + .await; + let (query_status, query_body): (StatusCode, Option) = make_json_request( + create_test_router(state).await, + get_request_with_auth("/api/v1/series?full=true&page=1&pageSize=50", &token), + ) + .await; + + assert_eq!(route_status, StatusCode::OK); + assert_eq!(query_status, StatusCode::OK); + + let route = route_body.expect("route body"); + let query = query_body.expect("query body"); + assert_eq!( + route["data"], query["data"], + "the dedicated listing must carry the same series as ?full=true" + ); + assert_eq!(route["total"], query["total"]); + + // The pagination links differ on purpose: the route means "full", so it must + // not echo the parameter it replaces. + let links = route["links"].as_object().expect("links"); + for (rel, link) in links { + if let Some(href) = link.as_str() { + assert!( + !href.contains("full=true"), + "{rel} link should not carry full=true: {href}" + ); + assert!( + href.contains("/api/v1/series/full"), + "{rel} link should point at the dedicated route: {href}" + ); + } + } +} + +/// Full listing entries must carry the related data that makes the route worth +/// having, so this cannot pass against the plain `SeriesDto` page. +#[tokio::test] +async fn full_series_listing_entries_carry_their_related_data() { + let (db, _temp_dir) = setup_test_db().await; + let library = + LibraryRepository::create(&db, "Test Library", "/test", ScanningStrategy::Default) + .await + .unwrap(); + SeriesRepository::create(&db, library.id, "Alpha", None) + .await + .unwrap(); + + let state = create_test_auth_state(db.clone()).await; + let token = create_admin_and_token(&db, &state).await; + let (status, body): (StatusCode, Option) = make_json_request( + create_test_router(state).await, + get_request_with_auth("/api/v1/series/full", &token), + ) + .await; + + assert_eq!(status, StatusCode::OK); + let entry = &body.expect("body")["data"][0]; + for field in [ + "metadata", + "genres", + "tags", + "externalIds", + "externalLinks", + "externalRatings", + "alternateTitles", + ] { + assert!( + entry.get(field).is_some(), + "full series listing entry is missing {field}" + ); + } +} + +/// `/series/full` sits in the same path slot as `/series/{series_id}`, alongside +/// `/series/external-index` and the other static siblings. This pins that the +/// literal route wins, so it cannot start being read as a series id. +#[tokio::test] +async fn the_full_listing_route_is_not_shadowed_by_the_series_id_route() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let token = create_admin_and_token(&db, &state).await; + + let (status, body): (StatusCode, Option) = make_json_request( + create_test_router(state).await, + get_request_with_auth("/api/v1/series/full", &token), + ) + .await; + + // A series id lookup for the literal "full" would be a 400 (bad uuid) or a + // 404, never a page. + assert_eq!(status, StatusCode::OK); + assert!( + body.expect("body").get("data").is_some(), + "expected the paginated listing, not a series lookup" + ); +} diff --git a/web/openapi.json b/web/openapi.json index 732dc6d6..821fe09e 100644 --- a/web/openapi.json +++ b/web/openapi.json @@ -2796,8 +2796,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -2809,7 +2810,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3386,8 +3387,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -3399,7 +3401,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3467,8 +3469,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -3490,7 +3493,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3580,8 +3583,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -3593,7 +3597,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3683,8 +3687,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -3696,7 +3701,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3891,8 +3896,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -4739,7 +4745,7 @@ "Books" ], "summary": "Download book file", - "description": "Streams the original book file (CBZ, CBR, EPUB, PDF) for download.\nUsed by OPDS clients for acquisition links.", + "description": "Streams the original book file (CBZ, CBR, EPUB, PDF) for download.\nUsed by OPDS clients for acquisition links.\n\nRange-capable and conditional. `Accept-Ranges: bytes` and a strong `ETag`\ngo out on every response, so a download interrupted partway can resume with\n`Range` rather than starting again, and a client that understands the\ncontainer can read part of an archive without fetching all of it — the\nsuffix form `bytes=-65536` reaches a ZIP central directory directly.", "operationId": "get_book_file", "parameters": [ { @@ -4751,15 +4757,119 @@ "type": "string", "format": "uuid" } + }, + { + "name": "Range", + "in": "header", + "description": "Byte range, e.g. `bytes=0-1023`, `bytes=1024-` or `bytes=-65536`. A single range only; multiple ranges are answered with the whole file.", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "If-Range", + "in": "header", + "description": "Serve the range only if this ETag still matches; otherwise the whole file is returned, so a resume cannot splice bytes from two versions.", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "If-None-Match", + "in": "header", + "description": "Return 304 if this ETag still matches.", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } } ], "responses": { "200": { "description": "Book file", "content": { + "application/zip": {}, + "application/x-rar-compressed": {}, + "application/epub+zip": {}, + "application/pdf": {}, + "application/octet-stream": {} + } + }, + "206": { + "description": "The requested byte range", + "content": { + "application/zip": {}, + "application/x-rar-compressed": {}, + "application/epub+zip": {}, + "application/pdf": {}, "application/octet-stream": {} } }, + "304": { + "description": "Not modified (client cache is valid)" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Book not found" + }, + "416": { + "description": "The requested range names no byte of the file" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/books/{book_id}/full": { + "get": { + "tags": [ + "Books" + ], + "summary": "Get a book with its metadata, genres and tags in one response", + "description": "The same book `GET /api/v1/books/{book_id}` returns, plus the related data a\ndetail screen needs, so it does not have to fan out into separate metadata,\ngenre and tag requests.\n\nThis exists as its own route because the shape is genuinely different, not\nmerely richer: it carries `metadata`, `genres`, `tags`, `readCount` and\n`lastCompletedAt`, and it moves `chapter`, `summary` and `volume` inside\n`metadata`. A response whose schema depends on a query parameter cannot be\nexpressed in OpenAPI, so the deprecated `?full=true` form is undescribable\nand unusable from a generated client. This route is describable.", + "operationId": "get_book_full", + "parameters": [ + { + "name": "book_id", + "in": "path", + "description": "Book ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Book with its related data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FullBookResponse" + } + } + } + }, "403": { "description": "Forbidden" }, @@ -5091,7 +5201,7 @@ "200": { "description": "Page image", "content": { - "image/jpeg": {} + "image/*": {} } }, "304": { @@ -5592,7 +5702,7 @@ "200": { "description": "Thumbnail image", "content": { - "image/jpeg": {} + "image/*": {} } }, "304": { @@ -7389,9 +7499,9 @@ }, { "name": "page", - "in": "path", + "in": "query", "description": "Page number (1-indexed, minimum 1)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", @@ -7400,23 +7510,212 @@ }, { "name": "pageSize", + "in": "query", + "description": "Number of items per page (max 100, default 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sort parameter (format: \"field,direction\" e.g. \"title,asc\")", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", + "required": false, + "deprecated": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Paginated list of books in library", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_BookDto" + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/libraries/{library_id}/books/in-progress": { + "get": { + "tags": [ + "Books" + ], + "summary": "List books with reading progress in a specific library (in-progress books)", + "operationId": "list_library_in_progress_books", + "parameters": [ + { + "name": "library_id", "in": "path", + "description": "Library ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "page", + "in": "query", + "description": "Page number (1-indexed, minimum 1)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "pageSize", + "in": "query", "description": "Number of items per page (max 100, default 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sort parameter (format: \"field,direction\" e.g. \"title,asc\")", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", + "required": false, + "deprecated": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Paginated list of in-progress books in library", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_BookDto" + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/libraries/{library_id}/books/on-deck": { + "get": { + "tags": [ + "Books" + ], + "summary": "List on-deck books in a specific library", + "operationId": "list_library_on_deck_books", + "parameters": [ + { + "name": "library_id", + "in": "path", + "description": "Library ID", "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "page", + "in": "query", + "description": "Page number (1-indexed, minimum 1)", + "required": false, "schema": { "type": "integer", "format": "int64", "minimum": 0 } + }, + { + "name": "pageSize", + "in": "query", + "description": "Number of items per page (max 100, default 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sort parameter (format: \"field,direction\" e.g. \"title,asc\")", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", + "required": false, + "deprecated": true, + "schema": { + "type": "boolean" + } } ], "responses": { "200": { - "description": "Paginated list of books in library", + "description": "Paginated list of on-deck books in library", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -7435,13 +7734,13 @@ ] } }, - "/api/v1/libraries/{library_id}/books/in-progress": { + "/api/v1/libraries/{library_id}/books/recently-added": { "get": { "tags": [ "Books" ], - "summary": "List books with reading progress in a specific library (in-progress books)", - "operationId": "list_library_in_progress_books", + "summary": "List recently added books in a specific library", + "operationId": "list_library_recently_added_books", "parameters": [ { "name": "library_id", @@ -7455,9 +7754,9 @@ }, { "name": "page", - "in": "path", + "in": "query", "description": "Page number (1-indexed, minimum 1)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", @@ -7466,23 +7765,42 @@ }, { "name": "pageSize", - "in": "path", + "in": "query", "description": "Number of items per page (max 100, default 50)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", "minimum": 0 } + }, + { + "name": "sort", + "in": "query", + "description": "Sort parameter (format: \"field,direction\" e.g. \"title,asc\")", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", + "required": false, + "deprecated": true, + "schema": { + "type": "boolean" + } } ], "responses": { "200": { - "description": "Paginated list of in-progress books in library", + "description": "Paginated list of recently added books in library", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -7501,13 +7819,13 @@ ] } }, - "/api/v1/libraries/{library_id}/books/on-deck": { + "/api/v1/libraries/{library_id}/books/recently-read": { "get": { "tags": [ "Books" ], - "summary": "List on-deck books in a specific library", - "operationId": "list_library_on_deck_books", + "summary": "List recently read books in a specific library", + "operationId": "list_library_recently_read_books", "parameters": [ { "name": "library_id", @@ -7520,41 +7838,91 @@ } }, { - "name": "page", - "in": "path", - "description": "Page number (1-indexed, minimum 1)", - "required": true, + "name": "limit", + "in": "query", + "description": "Maximum number of books to return (default: 50)", + "required": false, "schema": { "type": "integer", "format": "int64", "minimum": 0 } + } + ], + "responses": { + "200": { + "description": "List of recently read books in library", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BookDto" + } + } + } + } }, + "403": { + "description": "Forbidden" + } + }, + "security": [ { - "name": "pageSize", + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/libraries/{library_id}/books/thumbnails/generate": { + "post": { + "tags": [ + "Thumbnails" + ], + "summary": "Generate thumbnails for all books in a library", + "description": "Queues a fan-out task that enqueues individual thumbnail generation tasks for each book in the library.\n\n# Permission Required\n- `tasks:write`", + "operationId": "generate_library_book_thumbnails", + "parameters": [ + { + "name": "library_id", "in": "path", - "description": "Number of items per page (max 100, default 50)", + "description": "Library ID", "required": true, "schema": { - "type": "integer", - "format": "int64", - "minimum": 0 + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForceRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Paginated list of on-deck books in library", + "description": "Thumbnail generation task queued", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/CreateTaskResponse" } } } }, "403": { - "description": "Forbidden" + "description": "Permission denied" + }, + "404": { + "description": "Library not found" } }, "security": [ @@ -7567,13 +7935,60 @@ ] } }, - "/api/v1/libraries/{library_id}/books/recently-added": { + "/api/v1/libraries/{library_id}/jobs": { "get": { "tags": [ - "Books" + "Library Jobs" ], - "summary": "List recently added books in a specific library", - "operationId": "list_library_recently_added_books", + "summary": "List the scheduled jobs configured for a library", + "description": "Jobs are per-library scheduled work; today the only kind is a metadata\nrefresh. The schedule is a cron expression evaluated in the job's timezone,\nor the server's if it declares none.", + "operationId": "list_jobs", + "parameters": [ + { + "name": "library_id", + "in": "path", + "description": "Library ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The library's jobs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListLibraryJobsResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Library not found" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "Library Jobs" + ], + "summary": "Create a scheduled job for a library", + "description": "The job's type is taken from the `config` variant rather than named\nseparately, so the two cannot disagree. A blank `name` is generated from the\nconfig.", + "operationId": "create_job", "parameters": [ { "name": "library_id", @@ -7584,43 +7999,94 @@ "type": "string", "format": "uuid" } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateLibraryJobRequest" + } + } }, + "required": true + }, + "responses": { + "201": { + "description": "The created job", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LibraryJobDto" + } + } + } + }, + "400": { + "description": "Invalid cron expression, timezone, or config" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Library not found" + } + }, + "security": [ { - "name": "page", + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/libraries/{library_id}/jobs/{job_id}": { + "get": { + "tags": [ + "Library Jobs" + ], + "summary": "Get one scheduled job", + "operationId": "get_job", + "parameters": [ + { + "name": "library_id", "in": "path", - "description": "Page number (1-indexed, minimum 1)", + "description": "Library ID", "required": true, "schema": { - "type": "integer", - "format": "int64", - "minimum": 0 + "type": "string", + "format": "uuid" } }, { - "name": "pageSize", + "name": "job_id", "in": "path", - "description": "Number of items per page (max 100, default 50)", + "description": "Job ID", "required": true, "schema": { - "type": "integer", - "format": "int64", - "minimum": 0 + "type": "string", + "format": "uuid" } } ], "responses": { "200": { - "description": "Paginated list of recently added books in library", + "description": "The job", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/LibraryJobDto" } } } }, "403": { "description": "Forbidden" + }, + "404": { + "description": "Job not found, or it belongs to another library" } }, "security": [ @@ -7631,15 +8097,13 @@ "api_key": [] } ] - } - }, - "/api/v1/libraries/{library_id}/books/recently-read": { - "get": { + }, + "delete": { "tags": [ - "Books" + "Library Jobs" ], - "summary": "List recently read books in a specific library", - "operationId": "list_library_recently_read_books", + "summary": "Delete a scheduled job", + "operationId": "delete_job", "parameters": [ { "name": "library_id", @@ -7652,33 +8116,94 @@ } }, { - "name": "limit", - "in": "query", - "description": "Maximum number of books to return (default: 50)", - "required": false, + "name": "job_id", + "in": "path", + "description": "Job ID", + "required": true, "schema": { - "type": "integer", - "format": "int64", - "minimum": 0 + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Job deleted" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Job not found, or it belongs to another library" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + }, + "patch": { + "tags": [ + "Library Jobs" + ], + "summary": "Update a scheduled job", + "description": "Every field is optional. `timezone` is tri-state: absent leaves it, `null`\nclears it back to the server default, and a value sets it. A `config` whose\ntype differs from the job's is rejected rather than migrating the job.", + "operationId": "patch_job", + "parameters": [ + { + "name": "library_id", + "in": "path", + "description": "Library ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "job_id", + "in": "path", + "description": "Job ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchLibraryJobRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "List of recently read books in library", + "description": "The updated job", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BookDto" - } + "$ref": "#/components/schemas/LibraryJobDto" } } } }, + "400": { + "description": "Empty name, invalid schedule, or a config of the wrong type" + }, "403": { "description": "Forbidden" + }, + "404": { + "description": "Job not found, or it belongs to another library" } }, "security": [ @@ -7691,14 +8216,14 @@ ] } }, - "/api/v1/libraries/{library_id}/books/thumbnails/generate": { + "/api/v1/libraries/{library_id}/jobs/{job_id}/dry-run": { "post": { "tags": [ - "Thumbnails" + "Library Jobs" ], - "summary": "Generate thumbnails for all books in a library", - "description": "Queues a fan-out task that enqueues individual thumbnail generation tasks for each book in the library.\n\n# Permission Required\n- `tasks:write`", - "operationId": "generate_library_book_thumbnails", + "summary": "Preview what a job would change, without changing anything", + "description": "Plans the refresh and returns a sample of the per-series field changes it\nwould make. Nothing is written. `configOverride` plans against a config the\njob does not have yet, which is what lets an editor preview an edit before\nsaving it; it must be of the job's own type.", + "operationId": "dry_run_job", "parameters": [ { "name": "library_id", @@ -7709,13 +8234,23 @@ "type": "string", "format": "uuid" } + }, + { + "name": "job_id", + "in": "path", + "description": "Job ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForceRequest" + "$ref": "#/components/schemas/DryRunRequest" } } }, @@ -7723,20 +8258,84 @@ }, "responses": { "200": { - "description": "Thumbnail generation task queued", + "description": "The planned changes", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateTaskResponse" + "$ref": "#/components/schemas/DryRunResponse" } } } }, + "400": { + "description": "Override config is of the wrong type, or is invalid" + }, "403": { - "description": "Permission denied" + "description": "Forbidden" }, "404": { - "description": "Library not found" + "description": "Job not found, or it belongs to another library" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/libraries/{library_id}/jobs/{job_id}/run-now": { + "post": { + "tags": [ + "Library Jobs" + ], + "summary": "Run a scheduled job immediately", + "description": "Enqueues the job's task and returns its id; the run itself is asynchronous\nand its progress is followed through the task queue. Refuses if a run for\nthis job is already in flight, so a double click cannot queue the work\ntwice.", + "operationId": "run_job_now", + "parameters": [ + { + "name": "library_id", + "in": "path", + "description": "Library ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "job_id", + "in": "path", + "description": "Job ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The enqueued task", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunNowResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Job not found, or it belongs to another library" + }, + "409": { + "description": "A run for this job is already in flight" } }, "security": [ @@ -8042,8 +8641,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, alternate titles,\nexternal ratings, and external links. Default is false for backward compatibility.", + "description": "Return full series data including metadata, locks, genres, tags, alternate titles,\nexternal ratings, and external links. Default is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -8055,7 +8655,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" } } } @@ -8095,8 +8695,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -8175,8 +8776,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -8255,8 +8857,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -8406,6 +9009,42 @@ ] } }, + "/api/v1/library-jobs/metadata-refresh/field-groups": { + "get": { + "tags": [ + "Library Jobs" + ], + "summary": "List the metadata field groups a refresh job can target", + "description": "A static catalog: the groups and the concrete metadata fields each one\ncovers. It is what a job editor offers as refreshable fields, so a client\ndoes not have to hardcode the list.", + "operationId": "list_field_groups", + "responses": { + "200": { + "description": "The field-group catalog", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldGroupDto" + } + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, "/api/v1/metrics/inventory": { "get": { "tags": [ @@ -10553,8 +11192,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, alternate titles,\nexternal ratings, and external links. Default is false for backward compatibility.", + "description": "Return full series data including metadata, locks, genres, tags, alternate titles,\nexternal ratings, and external links. Default is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -10566,7 +11206,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" } } } @@ -11293,8 +11933,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -11306,7 +11947,113 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesExternalIndexDto" + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/series/full": { + "get": { + "tags": [ + "Series" + ], + "summary": "List series with their metadata, genres, tags and external links", + "description": "The same page `GET /api/v1/series` returns, with each entry carrying the\nrelated data inline, so a caller that needs metadata for a whole library\npages it once instead of following up per series.\n\nThis exists as its own route because the shape is genuinely different, not\nmerely richer, and because a response whose schema depends on a query\nparameter cannot be expressed in OpenAPI — which made the deprecated\n`GET /api/v1/series?full=true` invisible to every generated client. Accepts\nthe same filters and pagination as the plain listing.", + "operationId": "list_series_full", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "Page number (1-indexed, default 1)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "pageSize", + "in": "query", + "description": "Number of items per page (max 100, default 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sort parameter (format: \"field,direction\" e.g. \"name,asc\")", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "genres", + "in": "query", + "description": "Filter by genres (comma-separated, AND logic - series must have ALL specified genres)", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "tags", + "in": "query", + "description": "Filter by tags (comma-separated, AND logic - series must have ALL specified tags)", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "libraryId", + "in": "query", + "description": "Filter by library ID", + "required": false, + "schema": { + "type": [ + "string", + "null" + ], + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Paginated series with their related data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_FullSeriesResponse" } } } @@ -11349,8 +12096,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -11433,8 +12181,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -11456,7 +12205,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" } } } @@ -11608,8 +12357,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -11678,8 +12428,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -12182,8 +12933,9 @@ { "name": "full", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -12779,8 +13531,9 @@ { "name": "full", "in": "query", - "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.", + "description": "Return full data including metadata and locks.\nDefault is false for backward compatibility.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", "required": false, + "deprecated": true, "schema": { "type": "boolean" } @@ -13718,6 +14471,54 @@ ] } }, + "/api/v1/series/{series_id}/full": { + "get": { + "tags": [ + "Series" + ], + "summary": "Get a series with its metadata, genres, tags and external links in one response", + "description": "The same series `GET /api/v1/series/{series_id}` returns, plus the related\ndata a detail screen needs, so it does not have to fan out into separate\nmetadata, genre, tag, external-id, external-link and rating requests.\n\nThis exists as its own route because the shape is genuinely different, not\nmerely richer: it carries `metadata`, `genres`, `tags`, `alternateTitles`,\n`externalIds`, `externalLinks`, `externalRatings`, `readCount` and\n`lastCompletedAt`, and it moves `title`, `titleSort`, `publisher`, `summary`\nand `year` inside `metadata`. A response whose schema depends on a query\nparameter cannot be expressed in OpenAPI, so the deprecated `?full=true`\nform is undescribable and unusable from a generated client. This route is\ndescribable.", + "operationId": "get_series_full", + "parameters": [ + { + "name": "series_id", + "in": "path", + "description": "Series ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Series with its related data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FullSeriesResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Series not found" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, "/api/v1/series/{series_id}/genres": { "get": { "tags": [ @@ -15179,7 +15980,7 @@ "200": { "description": "Thumbnail image", "content": { - "image/jpeg": {} + "image/*": {} } }, "304": { @@ -16457,7 +17258,9 @@ "200": { "description": "Export file", "content": { - "application/octet-stream": {} + "text/csv": {}, + "text/markdown": {}, + "application/json": {} } }, "404": { @@ -18299,7 +19102,7 @@ "200": { "description": "Page image (also records reading progress)", "content": { - "image/jpeg": {} + "image/*": {} } }, "403": { @@ -23766,10 +24569,7 @@ "description": "Request body for POST /books/list\n\nPagination parameters (page, pageSize, sort) are passed as query parameters,\nnot in the request body. This enables proper HATEOAS links.", "properties": { "condition": { - "type": [ - "object", - "null" - ], + "$ref": "#/components/schemas/BookCondition", "description": "Filter condition (optional - no condition returns all)" }, "fullTextSearch": { @@ -31141,100 +31941,7 @@ "content": { "type": "array", "items": { - "type": "object", - "description": "Komga book DTO\n\nBased on actual Komic traffic analysis. This is the main book representation.", - "required": [ - "id", - "seriesId", - "seriesTitle", - "libraryId", - "name", - "url", - "number", - "created", - "lastModified", - "fileLastModified", - "sizeBytes", - "size", - "media", - "metadata" - ], - "properties": { - "created": { - "type": "string", - "description": "Created timestamp (ISO 8601)" - }, - "deleted": { - "type": "boolean", - "description": "Whether book is deleted (soft delete)" - }, - "fileHash": { - "type": "string", - "description": "File hash" - }, - "fileLastModified": { - "type": "string", - "description": "File last modified timestamp (ISO 8601)" - }, - "id": { - "type": "string", - "description": "Book unique identifier (UUID as string)" - }, - "lastModified": { - "type": "string", - "description": "Last modified timestamp (ISO 8601)" - }, - "libraryId": { - "type": "string", - "description": "Library ID" - }, - "media": { - "$ref": "#/components/schemas/KomgaMediaDto", - "description": "Media information" - }, - "metadata": { - "$ref": "#/components/schemas/KomgaBookMetadataDto", - "description": "Book metadata" - }, - "name": { - "type": "string", - "description": "Book filename/name" - }, - "number": { - "type": "integer", - "format": "int32", - "description": "Book number in series" - }, - "oneshot": { - "type": "boolean", - "description": "Whether this is a oneshot" - }, - "readProgress": { - "$ref": "#/components/schemas/KomgaReadProgressDto", - "description": "User's read progress (null if not started)" - }, - "seriesId": { - "type": "string", - "description": "Series ID" - }, - "seriesTitle": { - "type": "string", - "description": "Series title (required by Komic for display)" - }, - "size": { - "type": "string", - "description": "Human-readable file size (e.g., \"869.9 MiB\")" - }, - "sizeBytes": { - "type": "integer", - "format": "int64", - "description": "File size in bytes" - }, - "url": { - "type": "string", - "description": "File URL/path" - } - } + "$ref": "#/components/schemas/KomgaBookDto" }, "description": "The content items for this page" }, @@ -31305,50 +32012,7 @@ "content": { "type": "array", "items": { - "type": "object", - "description": "Minimal collection DTO (stub)\n\nKomga collections are user-created groupings of series.\nCodex doesn't support this feature, so we return empty results.", - "required": [ - "id", - "name", - "ordered", - "seriesIds", - "createdDate", - "lastModifiedDate", - "filtered" - ], - "properties": { - "createdDate": { - "type": "string", - "description": "Created timestamp (ISO 8601)" - }, - "filtered": { - "type": "boolean", - "description": "Whether this collection is filtered from the user's view" - }, - "id": { - "type": "string", - "description": "Collection unique identifier" - }, - "lastModifiedDate": { - "type": "string", - "description": "Last modified timestamp (ISO 8601)" - }, - "name": { - "type": "string", - "description": "Collection name" - }, - "ordered": { - "type": "boolean", - "description": "Whether the collection is ordered" - }, - "seriesIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Series IDs in the collection" - } - } + "$ref": "#/components/schemas/KomgaCollectionDto" }, "description": "The content items for this page" }, @@ -31419,55 +32083,7 @@ "content": { "type": "array", "items": { - "type": "object", - "description": "Minimal read list DTO (stub)\n\nKomga read lists are user-created lists of books to read.\nCodex doesn't support this feature, so we return empty results.", - "required": [ - "id", - "name", - "summary", - "ordered", - "bookIds", - "createdDate", - "lastModifiedDate", - "filtered" - ], - "properties": { - "bookIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Book IDs in the read list" - }, - "createdDate": { - "type": "string", - "description": "Created timestamp (ISO 8601)" - }, - "filtered": { - "type": "boolean", - "description": "Whether this read list is filtered from the user's view" - }, - "id": { - "type": "string", - "description": "Read list unique identifier" - }, - "lastModifiedDate": { - "type": "string", - "description": "Last modified timestamp (ISO 8601)" - }, - "name": { - "type": "string", - "description": "Read list name" - }, - "ordered": { - "type": "boolean", - "description": "Whether the read list is ordered" - }, - "summary": { - "type": "string", - "description": "Read list summary/description" - } - } + "$ref": "#/components/schemas/KomgaReadListDto" }, "description": "The content items for this page" }, @@ -31538,89 +32154,7 @@ "content": { "type": "array", "items": { - "type": "object", - "description": "Komga series DTO\n\nBased on actual Komic traffic analysis.", - "required": [ - "id", - "libraryId", - "name", - "url", - "created", - "lastModified", - "fileLastModified", - "booksCount", - "booksReadCount", - "booksUnreadCount", - "booksInProgressCount", - "metadata", - "booksMetadata" - ], - "properties": { - "booksCount": { - "type": "integer", - "format": "int32", - "description": "Total books count" - }, - "booksInProgressCount": { - "type": "integer", - "format": "int32", - "description": "In-progress books count" - }, - "booksMetadata": { - "$ref": "#/components/schemas/KomgaBooksMetadataAggregationDto", - "description": "Aggregated books metadata" - }, - "booksReadCount": { - "type": "integer", - "format": "int32", - "description": "Read books count" - }, - "booksUnreadCount": { - "type": "integer", - "format": "int32", - "description": "Unread books count" - }, - "created": { - "type": "string", - "description": "Created timestamp (ISO 8601)" - }, - "deleted": { - "type": "boolean", - "description": "Whether series is deleted (soft delete)" - }, - "fileLastModified": { - "type": "string", - "description": "File last modified timestamp (ISO 8601)" - }, - "id": { - "type": "string", - "description": "Series unique identifier (UUID as string)" - }, - "lastModified": { - "type": "string", - "description": "Last modified timestamp (ISO 8601)" - }, - "libraryId": { - "type": "string", - "description": "Library ID" - }, - "metadata": { - "$ref": "#/components/schemas/KomgaSeriesMetadataDto", - "description": "Series metadata" - }, - "name": { - "type": "string", - "description": "Series name" - }, - "oneshot": { - "type": "boolean", - "description": "Whether this is a oneshot (single book)" - }, - "url": { - "type": "string", - "description": "File URL/path" - } - } + "$ref": "#/components/schemas/KomgaSeriesDto" }, "description": "The content items for this page" }, @@ -34261,7 +34795,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/BookDto" + "$ref": "#/components/schemas/FullBookResponse" }, "description": "The data items for this page" }, @@ -34314,80 +34848,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "API key data transfer object", - "required": [ - "id", - "userId", - "name", - "keyPrefix", - "permissions", - "isActive", - "createdAt", - "updatedAt" - ], - "properties": { - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the key was created", - "example": "2024-01-01T00:00:00Z" - }, - "expiresAt": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When the key expires (if set)", - "example": "2025-12-31T23:59:59Z" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Unique API key identifier", - "example": "550e8400-e29b-41d4-a716-446655440000" - }, - "isActive": { - "type": "boolean", - "description": "Whether the key is currently active", - "example": true - }, - "keyPrefix": { - "type": "string", - "description": "Prefix of the key for identification", - "example": "cdx_a1b2c3" - }, - "lastUsedAt": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When the key was last used", - "example": "2024-01-15T10:30:00Z" - }, - "name": { - "type": "string", - "description": "Human-readable name for the key", - "example": "Mobile App Key" - }, - "permissions": { - "description": "Permissions granted to this key" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "When the key was last updated", - "example": "2024-01-15T10:30:00Z" - }, - "userId": { - "type": "string", - "format": "uuid", - "description": "Owner user ID", - "example": "550e8400-e29b-41d4-a716-446655440001" - } - } + "$ref": "#/components/schemas/ApiKeyDto" }, "description": "The data items for this page" }, @@ -34440,187 +34901,60 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Book data transfer object", - "required": [ - "id", - "libraryId", - "libraryName", - "seriesId", - "seriesName", - "title", - "path", - "fileFormat", - "fileSize", - "fileHash", - "pageCount", - "createdAt", - "updatedAt", - "deleted", - "analyzed" - ], - "properties": { - "analysisError": { - "type": [ - "string", - "null" - ], - "description": "Error message if book analysis failed", - "example": "Failed to parse CBZ: invalid archive" - }, - "analyzed": { - "type": "boolean", - "description": "Whether the book has been analyzed (page dimensions available)", - "example": true - }, - "chapter": { - "type": [ - "number", - "null" - ], - "format": "float", - "description": "Chapter number from book metadata (may be fractional)", - "example": 42.5 - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the book was added to the library", - "example": "2024-01-01T00:00:00Z" - }, - "deleted": { - "type": "boolean", - "description": "Whether the book has been soft-deleted", - "example": false - }, - "fileFormat": { - "type": "string", - "description": "File format (cbz, cbr, epub, pdf)", - "example": "cbz" - }, - "fileHash": { - "type": "string", - "description": "File hash for deduplication", - "example": "a1b2c3d4e5f6g7h8i9j0" - }, - "fileSize": { - "type": "integer", - "format": "int64", - "description": "File size in bytes", - "example": 52428800 - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Book unique identifier", - "example": "550e8400-e29b-41d4-a716-446655440001" - }, - "koreaderHash": { - "type": [ - "string", - "null" - ], - "description": "KOReader-compatible partial MD5 hash for sync" - }, - "libraryId": { - "type": "string", - "format": "uuid", - "description": "Library this book belongs to", - "example": "550e8400-e29b-41d4-a716-446655440000" - }, - "libraryName": { - "type": "string", - "description": "Name of the library", - "example": "Comics" - }, - "number": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "description": "Book number within the series", - "example": 1 - }, - "pageCount": { - "type": "integer", - "format": "int32", - "description": "Number of pages in the book", - "example": 32 - }, - "path": { - "type": "string", - "description": "Filesystem path to the book file", - "example": "/media/comics/Batman/Batman - Year One 001.cbz" - }, - "readProgress": { - "$ref": "#/components/schemas/ReadProgressResponse", - "description": "User's read progress for this book" - }, - "readingDirection": { - "type": [ - "string", - "null" - ], - "description": "Effective reading direction (from series metadata, or library default if not set)\nValues: ltr, rtl, ttb or webtoon", - "example": "ltr" - }, - "seriesId": { - "type": "string", - "format": "uuid", - "description": "Series this book belongs to", - "example": "550e8400-e29b-41d4-a716-446655440002" - }, - "seriesName": { - "type": "string", - "description": "Name of the series", - "example": "Batman: Year One" - }, - "summary": { - "type": [ - "string", - "null" - ], - "description": "Book summary/description from book_metadata (ComicInfo `` or\nEPUB description). Surfaced on the list response so cards can show it on\nhover without fetching the full detail payload.", - "example": "Bruce Wayne returns to Gotham to begin his war on crime." - }, - "title": { - "type": "string", - "description": "Book title", - "example": "Batman: Year One #1" - }, - "titleSort": { - "type": [ - "string", - "null" - ], - "description": "Title used for sorting (title_sort field)", - "example": "batman year one 001" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "When the book was last updated", - "example": "2024-01-15T10:30:00Z" - }, - "volume": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "description": "Volume number from book metadata", - "example": 1 - }, - "wantToRead": { - "type": [ - "boolean", - "null" - ], - "description": "Whether the requesting user has this book in their want-to-read queue.\n\n`None` when not computed for this response; populated on book list and\ndetail endpoints that have a user context.", - "example": false - } - } + "$ref": "#/components/schemas/BookDto" + }, + "description": "The data items for this page" + }, + "links": { + "$ref": "#/components/schemas/PaginationLinks", + "description": "HATEOAS navigation links" + }, + "page": { + "type": "integer", + "format": "int64", + "description": "Current page number (1-indexed)", + "example": 1, + "minimum": 0 + }, + "pageSize": { + "type": "integer", + "format": "int64", + "description": "Number of items per page", + "example": 50, + "minimum": 0 + }, + "total": { + "type": "integer", + "format": "int64", + "description": "Total number of items across all pages", + "example": 150, + "minimum": 0 + }, + "totalPages": { + "type": "integer", + "format": "int64", + "description": "Total number of pages", + "example": 3, + "minimum": 0 + } + } + }, + "PaginatedResponse_FullSeriesResponse": { + "type": "object", + "description": "Generic paginated response wrapper with HATEOAS links", + "required": [ + "data", + "page", + "pageSize", + "total", + "totalPages", + "links" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FullSeriesResponse" }, "description": "The data items for this page" }, @@ -34673,42 +35007,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Genre data transfer object", - "required": [ - "id", - "name", - "createdAt" - ], - "properties": { - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the genre was created", - "example": "2024-01-01T00:00:00Z" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Genre ID", - "example": "550e8400-e29b-41d4-a716-446655440010" - }, - "name": { - "type": "string", - "description": "Genre name", - "example": "Action" - }, - "seriesCount": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Number of series with this genre", - "example": 42, - "minimum": 0 - } - } + "$ref": "#/components/schemas/GenreDto" }, "description": "The data items for this page" }, @@ -34761,149 +35060,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Library data transfer object", - "required": [ - "id", - "name", - "path", - "isActive", - "seriesStrategy", - "bookStrategy", - "numberStrategy", - "createdAt", - "updatedAt", - "defaultReadingDirection" - ], - "properties": { - "allowedFormats": { - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - }, - "description": "Allowed file formats (e.g., [\"CBZ\", \"CBR\", \"EPUB\"])", - "example": [ - "CBZ", - "CBR", - "PDF" - ] - }, - "autoMatchConditions": { - "description": "Auto-match conditions (JSON object with mode and rules)\nControls when auto-matching runs for this library" - }, - "bookConfig": { - "description": "Book strategy-specific configuration (JSON)" - }, - "bookCount": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Total number of books in this library", - "example": 1250 - }, - "bookStrategy": { - "$ref": "#/components/schemas/BookStrategy", - "description": "Book naming strategy (filename, metadata_first, smart, series_name)" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the library was created", - "example": "2024-01-01T00:00:00Z" - }, - "defaultReadingDirection": { - "type": "string", - "description": "Default reading direction for books in this library (ltr, rtl, ttb or webtoon)", - "example": "ltr" - }, - "description": { - "type": [ - "string", - "null" - ], - "description": "Optional description", - "example": "My comic book collection" - }, - "excludedPatterns": { - "type": [ - "string", - "null" - ], - "description": "Excluded path patterns (newline-separated, e.g., \".DS_Store\\nThumbs.db\")", - "example": ".DS_Store\nThumbs.db" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Library unique identifier", - "example": "550e8400-e29b-41d4-a716-446655440000" - }, - "isActive": { - "type": "boolean", - "description": "Whether the library is active", - "example": true - }, - "lastScannedAt": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When the library was last scanned", - "example": "2024-01-15T10:30:00Z" - }, - "name": { - "type": "string", - "description": "Library name", - "example": "Comics" - }, - "numberConfig": { - "description": "Number strategy-specific configuration (JSON)" - }, - "numberStrategy": { - "$ref": "#/components/schemas/NumberStrategy", - "description": "Book number strategy (file_order, metadata, filename, smart)" - }, - "path": { - "type": "string", - "description": "Filesystem path to the library root", - "example": "/media/comics" - }, - "scanningConfig": { - "$ref": "#/components/schemas/ScanningConfigDto", - "description": "Scanning configuration for scheduled scans" - }, - "seriesConfig": { - "description": "Strategy-specific configuration (JSON)" - }, - "seriesCount": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Total number of series in this library", - "example": 85 - }, - "seriesStrategy": { - "$ref": "#/components/schemas/SeriesStrategy", - "description": "Series detection strategy (series_volume, series_volume_chapter, flat, etc.)" - }, - "titlePreprocessingRules": { - "description": "Title preprocessing rules (JSON array of regex rules)\nApplied during scan to clean series titles before metadata search" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "When the library was last updated", - "example": "2024-01-15T10:30:00Z" - } - } + "$ref": "#/components/schemas/LibraryDto" }, "description": "The data items for this page" }, @@ -34956,136 +35113,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "A single release announcement. Sources write these; the inbox reads them.", - "required": [ - "id", - "seriesId", - "seriesTitle", - "sourceId", - "externalReleaseId", - "payloadUrl", - "confidence", - "state", - "observedAt", - "createdAt" - ], - "properties": { - "chapters": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/ReleaseSpanDto" - }, - "description": "Full chapter coverage as a normalized span list. Decimals supported\n(`c12.5` → `[{start: 12.5, end: 12.5}]`). `null` when the upstream\ntitle carried no chapter info." - }, - "confidence": { - "type": "number", - "format": "double" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "externalReleaseId": { - "type": "string", - "description": "Plugin-stable identity for the release (used for dedup).", - "example": "nyaa:1234567" - }, - "formatHints": { - "description": "Sparse `{ \"jxl\": true, \"container\": \"cbz\", ... }`." - }, - "groupOrUploader": { - "type": [ - "string", - "null" - ], - "description": "Group/scanlator/uploader attribution." - }, - "id": { - "type": "string", - "format": "uuid", - "example": "550e8400-e29b-41d4-a716-446655440a00" - }, - "infoHash": { - "type": [ - "string", - "null" - ], - "description": "Torrent info_hash, if applicable." - }, - "language": { - "type": [ - "string", - "null" - ] - }, - "mediaUrl": { - "type": [ - "string", - "null" - ], - "description": "Optional second URL for direct fetch (`.torrent`, `magnet:`, DDL\nlink). Travels paired with [`Self::media_url_kind`]." - }, - "mediaUrlKind": { - "type": [ - "string", - "null" - ], - "description": "Classifies what `media_url` points at: `torrent` | `magnet` |\n`direct` | `other`. The frontend uses this to pick a kind-specific\nicon next to the standard external-link icon." - }, - "metadata": { - "description": "Source-specific extras (free-form)." - }, - "observedAt": { - "type": "string", - "format": "date-time", - "description": "When Codex detected this release." - }, - "payloadUrl": { - "type": "string", - "description": "Where to acquire the release. Conventionally a human-readable\nlanding page (Nyaa view page, MangaUpdates release page)." - }, - "releasedAt": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Upstream publish date from the source feed. `null` when unavailable." - }, - "seriesId": { - "type": "string", - "format": "uuid", - "example": "550e8400-e29b-41d4-a716-446655440002" - }, - "seriesTitle": { - "type": "string", - "description": "Series title at the time of the response. Joined from the `series`\ntable so the inbox UI can render a human-readable label without a\nfollow-up fetch. Falls back to the empty string only if the series\nrow was hard-deleted between the join and the read.", - "example": "Chainsaw Man" - }, - "sourceId": { - "type": "string", - "format": "uuid", - "example": "550e8400-e29b-41d4-a716-446655440b00" - }, - "state": { - "type": "string", - "description": "`announced` | `dismissed` | `marked_acquired` | `hidden`." - }, - "volumes": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/ReleaseSpanDto" - }, - "description": "Full volume coverage as a normalized span list. `null` semantics\nmirror [`Self::chapters`]." - } - } + "$ref": "#/components/schemas/ReleaseLedgerEntryDto" }, "description": "The data items for this page" }, @@ -35138,192 +35166,60 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Series data transfer object", - "required": [ - "id", - "libraryId", - "libraryName", - "title", - "bookCount", - "tracked", - "createdAt", - "updatedAt" - ], - "properties": { - "bookCount": { - "type": "integer", - "format": "int64", - "description": "Total number of books in this series", - "example": 4 - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the series was created", - "example": "2024-01-01T00:00:00Z" - }, - "hasCustomCover": { - "type": [ - "boolean", - "null" - ], - "description": "Whether the series has a custom cover uploaded", - "example": false - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Series unique identifier", - "example": "550e8400-e29b-41d4-a716-446655440002" - }, - "libraryId": { - "type": "string", - "format": "uuid", - "description": "Library unique identifier", - "example": "550e8400-e29b-41d4-a716-446655440000" - }, - "libraryName": { - "type": "string", - "description": "Name of the library this series belongs to", - "example": "Comics" - }, - "localMaxChapter": { - "type": [ - "number", - "null" - ], - "format": "float", - "description": "Highest `book_metadata.chapter` across the books in this series.\n\n`None` when no book in the series has `chapter` populated. When\nnon-null and `metadata.totalChapterCount` is also known, the UI renders\n`/ ch`.", - "example": 137.5 - }, - "localMaxVolume": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "description": "Highest `book_metadata.volume` across the books in this series.\n\n`None` when no book in the series has `volume` populated. When\nnon-null and `metadata.totalVolumeCount` is also known, the UI renders\n`/ vol` instead of the legacy\n`/ vol`.", - "example": 14 - }, - "path": { - "type": [ - "string", - "null" - ], - "description": "Filesystem path to the series directory", - "example": "/media/comics/Batman - Year One" - }, - "publisher": { - "type": [ - "string", - "null" - ], - "description": "Publisher name", - "example": "DC Comics" - }, - "selectedCoverSource": { - "type": [ - "string", - "null" - ], - "description": "Selected cover source (e.g., \"first_book\", \"custom\")", - "example": "first_book" - }, - "summary": { - "type": [ - "string", - "null" - ], - "description": "Summary/description from series_metadata", - "example": "The definitive origin story of Batman, following Bruce Wayne's first year as a vigilante." - }, - "title": { - "type": "string", - "description": "Series title from series_metadata", - "example": "Batman: Year One" - }, - "titleSort": { - "type": [ - "string", - "null" - ], - "description": "Sort title from series_metadata (for ordering)", - "example": "batman year one" - }, - "tracked": { - "type": "boolean", - "description": "Whether release tracking is enabled for this series.\n\nMirrors `series_tracking.tracked`. `false` when no tracking row exists.\nExposed so list views can surface a tracking indicator without an extra\nper-card request.", - "example": false - }, - "unreadCount": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Number of unread books in this series (user-specific)", - "example": 2 - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "When the series was last updated", - "example": "2024-01-15T10:30:00Z" - }, - "upstreamChapterGap": { - "type": [ - "number", - "null" - ], - "format": "float", - "description": "Difference between the upstream original-language chapter count\n(`series_metadata.total_chapter_count`, supplied by metadata\nproviders like MangaBaka or AniList) and the highest locally-owned\nchapter (`local_max_chapter`).\n\nAlways `None` unless the series is tracked AND `track_chapters` is\nenabled AND the provider count is populated AND the rounded-to-1-\ndecimal gap is positive. **This is an informational signal, not a\nrelease announcement**; the MangaUpdates plugin owns the\ntranslation-release feed.", - "example": 3.0 - }, - "upstreamGapProvider": { - "type": [ - "string", - "null" - ], - "description": "Display name of the metadata provider that supplied the upstream\ncounts (e.g., \"MangaBaka\", \"AniList\"). Set whenever at least one of\n`upstream_chapter_gap` / `upstream_volume_gap` is populated. Used by\nthe gap badge tooltip.", - "example": "MangaBaka" - }, - "upstreamVolumeGap": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "description": "Difference between the upstream original-language volume count\n(`series_metadata.total_volume_count`) and the highest locally-owned\nvolume (`local_max_volume`). Same suppression rules as\n`upstream_chapter_gap`, gated on `track_volumes`.", - "example": 1 - }, - "volumesOwned": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Number of books in this series classified as a complete volume\n(`volume IS NOT NULL AND chapter IS NULL`).\n\nDistinct from `bookCount`: a chapter inside a volume (`v15 c126`)\ncounts as a chapter, not a volume. `None` when no books exist;\n`Some(0)` when books exist but none are complete volumes.", - "example": 14 - }, - "wantToRead": { - "type": [ - "boolean", - "null" - ], - "description": "Whether the requesting user has this series in their want-to-read queue.\n\n`None` when not computed for this response (e.g. list endpoints that\ndon't enrich it); populated on the series detail endpoint.", - "example": false - }, - "year": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "description": "Release year", - "example": 1987 - } - } + "$ref": "#/components/schemas/SeriesDto" + }, + "description": "The data items for this page" + }, + "links": { + "$ref": "#/components/schemas/PaginationLinks", + "description": "HATEOAS navigation links" + }, + "page": { + "type": "integer", + "format": "int64", + "description": "Current page number (1-indexed)", + "example": 1, + "minimum": 0 + }, + "pageSize": { + "type": "integer", + "format": "int64", + "description": "Number of items per page", + "example": 50, + "minimum": 0 + }, + "total": { + "type": "integer", + "format": "int64", + "description": "Total number of items across all pages", + "example": 150, + "minimum": 0 + }, + "totalPages": { + "type": "integer", + "format": "int64", + "description": "Total number of pages", + "example": 3, + "minimum": 0 + } + } + }, + "PaginatedResponse_SeriesExternalIndexDto": { + "type": "object", + "description": "Generic paginated response wrapper with HATEOAS links", + "required": [ + "data", + "page", + "pageSize", + "total", + "totalPages", + "links" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesExternalIndexDto" }, "description": "The data items for this page" }, @@ -35376,63 +35272,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Sharing tag data transfer object", - "required": [ - "id", - "name", - "seriesCount", - "userCount", - "createdAt", - "updatedAt" - ], - "properties": { - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Creation timestamp", - "example": "2024-01-01T00:00:00Z" - }, - "description": { - "type": [ - "string", - "null" - ], - "description": "Optional description", - "example": "Content appropriate for children" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Unique sharing tag identifier", - "example": "550e8400-e29b-41d4-a716-446655440000" - }, - "name": { - "type": "string", - "description": "Display name of the sharing tag", - "example": "Kids Content" - }, - "seriesCount": { - "type": "integer", - "format": "int64", - "description": "Number of series tagged with this sharing tag", - "example": 42, - "minimum": 0 - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "Last update timestamp", - "example": "2024-01-15T10:30:00Z" - }, - "userCount": { - "type": "integer", - "format": "int64", - "description": "Number of users with grants for this sharing tag", - "example": 5, - "minimum": 0 - } - } + "$ref": "#/components/schemas/SharingTagDto" }, "description": "The data items for this page" }, @@ -35485,42 +35325,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Tag data transfer object", - "required": [ - "id", - "name", - "createdAt" - ], - "properties": { - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the tag was created", - "example": "2024-01-01T00:00:00Z" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Tag ID", - "example": "550e8400-e29b-41d4-a716-446655440020" - }, - "name": { - "type": "string", - "description": "Tag name", - "example": "Completed" - }, - "seriesCount": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Number of series with this tag", - "example": 15, - "minimum": 0 - } - } + "$ref": "#/components/schemas/TagDto" }, "description": "The data items for this page" }, @@ -35573,73 +35378,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "User data transfer object", - "required": [ - "id", - "username", - "email", - "role", - "permissions", - "isActive", - "createdAt", - "updatedAt" - ], - "properties": { - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Account creation timestamp", - "example": "2024-01-01T00:00:00Z" - }, - "email": { - "type": "string", - "description": "User email address", - "example": "john.doe@example.com" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Unique user identifier", - "example": "550e8400-e29b-41d4-a716-446655440000" - }, - "isActive": { - "type": "boolean", - "description": "Whether the account is active", - "example": true - }, - "lastLoginAt": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Timestamp of last login", - "example": "2024-01-15T10:30:00Z" - }, - "permissions": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Custom permissions that extend the role's base permissions" - }, - "role": { - "$ref": "#/components/schemas/UserRole", - "description": "User role (reader, maintainer, admin)" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "Last account update timestamp", - "example": "2024-01-15T10:30:00Z" - }, - "username": { - "type": "string", - "description": "Username for login", - "example": "johndoe" - } - } + "$ref": "#/components/schemas/UserDto" }, "description": "The data items for this page" }, @@ -40522,7 +40261,8 @@ "properties": { "full": { "type": "boolean", - "description": "Return full series data including metadata, locks, genres, tags, etc." + "description": "Return full series data including metadata, locks, genres, tags, etc.\n**Deprecated.** Prefer `GET /books/{book_id}/full` and\n`GET /series/{series_id}/full`. A response whose schema depends on a\nquery parameter cannot be expressed in OpenAPI, so this form is\ninvisible to a generated client. Scheduled for removal in 3.0.", + "deprecated": true }, "libraryId": { "type": [ @@ -41835,10 +41575,7 @@ "description": "Request body for POST /series/list\n\nPagination parameters (page, pageSize, sort) are passed as query parameters,\nnot in the request body. This enables proper HATEOAS links.", "properties": { "condition": { - "type": [ - "object", - "null" - ], + "$ref": "#/components/schemas/SeriesCondition", "description": "Filter condition (optional - no condition returns all)" }, "fullTextSearch": { diff --git a/web/src/api/books.ts b/web/src/api/books.ts index 6612fc4d..b4211ff6 100644 --- a/web/src/api/books.ts +++ b/web/src/api/books.ts @@ -86,18 +86,20 @@ export const booksApi = { return response.data.book; }, - // Get a single book with full details including metadata - getDetail: async ( - id: string, - options?: { full?: T }, - ): Promise => { - const params = new URLSearchParams(); - if (options?.full) params.set("full", "true"); - const queryString = params.toString(); - const url = `/books/${id}${queryString ? `?${queryString}` : ""}`; + // Get a single book with its `{ book, metadata }` detail shape. + getDetail: async (id: string): Promise => { + const response = await api.get(`/books/${id}`); + return response.data; + }, - const response = - await api.get(url); + // Get a book with its metadata, genres and tags in one response. + // + // Uses the dedicated route rather than the deprecated `?full=true`. The query + // parameter form returns a different schema depending on its value, which + // cannot be described in OpenAPI, so it forced the conditional return type + // this pair replaces. `full` is removed in 3.0. + getFull: async (id: string): Promise => { + const response = await api.get(`/books/${id}/full`); return response.data; }, diff --git a/web/src/api/queryKeys.ts b/web/src/api/queryKeys.ts index b9e5c4e5..4133386e 100644 --- a/web/src/api/queryKeys.ts +++ b/web/src/api/queryKeys.ts @@ -13,7 +13,7 @@ export const bookKeys = { /** Basic detail shape: `{ book, metadata }` from `getDetail(id)`. */ detail: (bookId: string | undefined) => ["books", bookId, "detail"] as const, /** - * Full flat detail shape from `getDetail(id, { full: true })`. Kept as a + * Full flat detail shape from `getFull(id)`. Kept as a * distinct key: the two shapes must never share a cache entry, or whichever * query runs first poisons the other's reads. */ diff --git a/web/src/api/series.ts b/web/src/api/series.ts index 1ad38ec5..6d20dc9e 100644 --- a/web/src/api/series.ts +++ b/web/src/api/series.ts @@ -1,6 +1,5 @@ import type { Book, - FullBook, FullSeries, PaginatedResponse, Series, @@ -29,16 +28,14 @@ export interface SeriesFilters { status?: string; publisher?: string; year?: number; - /** When true, returns FullSeriesResponse with complete metadata, genres, tags, etc. */ - full?: boolean; } export const seriesApi = { // Get series by library ID with filters - getByLibrary: async ( + getByLibrary: async ( libraryId: string, - filters?: SeriesFilters & { full?: T }, - ): Promise> => { + filters?: SeriesFilters, + ): Promise> => { const params = new URLSearchParams(); // Add library filter if not "all" @@ -54,47 +51,41 @@ export const seriesApi = { if (filters?.status) params.set("status", filters.status); if (filters?.publisher) params.set("publisher", filters.publisher); if (filters?.year) params.set("year", filters.year.toString()); - if (filters?.full) params.set("full", "true"); const queryString = params.toString(); const url = `/series${queryString ? `?${queryString}` : ""}`; - const response = - await api.get>( - url, - ); + const response = await api.get>(url); return response.data; }, - // Get a single series by ID - getById: async ( - id: string, - options?: { full?: T }, - ): Promise => { - const params = new URLSearchParams(); - if (options?.full) params.set("full", "true"); - const queryString = params.toString(); - const url = `/series/${id}${queryString ? `?${queryString}` : ""}`; + // Get a single series by ID. + getById: async (id: string): Promise => { + const response = await api.get(`/series/${id}`); + return response.data; + }, - const response = await api.get(url); + // Get a series with its metadata, genres, tags and external links in one + // response. + // + // Uses the dedicated route rather than the deprecated `?full=true`, which + // returns a different schema depending on its value and so cannot be + // described in OpenAPI. `full` is removed in 3.0. + getFull: async (id: string): Promise => { + const response = await api.get(`/series/${id}/full`); return response.data; }, // Get series with in-progress books - getInProgress: async ( - libraryId: string, - options?: { full?: T }, - ): Promise<(T extends true ? FullSeries : Series)[]> => { + getInProgress: async (libraryId: string): Promise => { const params = new URLSearchParams(); if (libraryId !== "all") { params.set("libraryId", libraryId); } - if (options?.full) params.set("full", "true"); const queryString = params.toString(); const url = `/series/in-progress${queryString ? `?${queryString}` : ""}`; - const response = - await api.get<(T extends true ? FullSeries : Series)[]>(url); + const response = await api.get(url); return response.data; }, @@ -179,57 +170,52 @@ export const seriesApi = { }, // Get recently added series - getRecentlyAdded: async ( + getRecentlyAdded: async ( libraryId: string, - options?: { limit?: number; full?: T }, - ): Promise<(T extends true ? FullSeries : Series)[]> => { + options?: { limit?: number }, + ): Promise => { const params = new URLSearchParams(); if (libraryId !== "all") { params.set("libraryId", libraryId); } params.set("limit", (options?.limit ?? 50).toString()); - if (options?.full) params.set("full", "true"); const queryString = params.toString(); const url = `/series/recently-added?${queryString}`; - const response = - await api.get<(T extends true ? FullSeries : Series)[]>(url); + const response = await api.get(url); return response.data; }, // Get recently updated series - getRecentlyUpdated: async ( + getRecentlyUpdated: async ( libraryId: string, - options?: { limit?: number; full?: T }, - ): Promise<(T extends true ? FullSeries : Series)[]> => { + options?: { limit?: number }, + ): Promise => { const params = new URLSearchParams(); if (libraryId !== "all") { params.set("libraryId", libraryId); } params.set("limit", (options?.limit ?? 50).toString()); - if (options?.full) params.set("full", "true"); const queryString = params.toString(); const url = `/series/recently-updated?${queryString}`; - const response = - await api.get<(T extends true ? FullSeries : Series)[]>(url); + const response = await api.get(url); return response.data; }, // Get books in a series - getBooks: async ( + getBooks: async ( seriesId: string, - options?: { includeDeleted?: boolean; full?: T }, - ): Promise<(T extends true ? FullBook : Book)[]> => { + options?: { includeDeleted?: boolean }, + ): Promise => { const params = new URLSearchParams(); if (options?.includeDeleted) { params.set("includeDeleted", "true"); } - if (options?.full) params.set("full", "true"); const queryString = params.toString(); const url = `/series/${seriesId}/books${queryString ? `?${queryString}` : ""}`; - const response = await api.get<(T extends true ? FullBook : Book)[]>(url); + const response = await api.get(url); return response.data; }, @@ -245,7 +231,7 @@ export const seriesApi = { * @param libraryId - Library to filter by, or "all" for all libraries * @param request - The search request with condition, pagination, and sort options */ - search: async ( + search: async ( libraryId: string, request: { condition?: SeriesCondition; @@ -253,9 +239,8 @@ export const seriesApi = { page?: number; pageSize?: number; sort?: string; - full?: T; }, - ): Promise> => { + ): Promise> => { // Build the full condition including library filter let finalCondition: SeriesCondition | undefined = request.condition; @@ -281,7 +266,6 @@ export const seriesApi = { if (request.pageSize !== undefined) params.set("pageSize", String(request.pageSize)); if (request.sort) params.set("sort", request.sort); - if (request.full) params.set("full", "true"); // Body only contains filter condition and search const body: SeriesListRequest = { @@ -292,9 +276,7 @@ export const seriesApi = { const queryString = params.toString(); const url = queryString ? `/series/list?${queryString}` : "/series/list"; - const response = await api.post< - PaginatedResponse - >(url, body); + const response = await api.post>(url, body); return response.data; }, diff --git a/web/src/mocks/handlers/books.ts b/web/src/mocks/handlers/books.ts index 8b76d66f..0ee5a3d4 100644 --- a/web/src/mocks/handlers/books.ts +++ b/web/src/mocks/handlers/books.ts @@ -892,6 +892,17 @@ export const bookHandlers = [ }); }), + // A single book with its related data — the dedicated route replacing + // `GET /api/v1/books/:id?full=true`. + http.get("/api/v1/books/:id/full", async ({ params }) => { + await delay(100); + const book = mockBooks.find((b) => b.id === params.id); + if (!book) { + return HttpResponse.json({ error: "Book not found" }, { status: 404 }); + } + return HttpResponse.json(toFullBookResponse(book)); + }), + // Get book thumbnail http.get("/api/v1/books/:id/thumbnail", async () => { await delay(50); diff --git a/web/src/mocks/handlers/coverage.test.ts b/web/src/mocks/handlers/coverage.test.ts new file mode 100644 index 00000000..db298a7b --- /dev/null +++ b/web/src/mocks/handlers/coverage.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { handlers } from "./index"; + +/** + * The mock handlers back `make frontend-mock`, the dev workflow that runs the + * app against MSW with no backend. They are wired through `setupWorker`, which + * only runs in a browser, so the vitest suite never loads them — nothing else in + * this repo can catch a route the app calls and the mocks do not serve. + * + * That gap is not hypothetical. `/books/{id}/full` and `/series/{id}/full` were + * added to the API and adopted by BookDetail and SeriesDetail while the mocks + * still only knew the deprecated `?full=true` form, and the suite stayed green + * because neither page has a test. + */ +describe("mock handler coverage", () => { + const paths = new Set( + handlers.map((handler) => String(handler.info.path)).filter(Boolean), + ); + + it.each([ + ["/api/v1/books/:id/full"], + ["/api/v1/series/:id/full"], + ["/api/v1/series/full"], + ])("serves %s", (path) => { + expect(paths).toContain(path); + }); + + /** + * MSW resolves handlers in array order, so a `:id` pattern declared first + * would swallow `/series/full` and read "full" as a series id. The real router + * has the same constraint and solves it the same way. + */ + it("declares the literal /series/full before the /series/:id pattern", () => { + // Method matters: MSW matches on method *and* path, so the PATCH handler + // for /series/:id cannot shadow a GET however early it is declared. + const ordered = handlers + .filter((handler) => String(handler.info.method) === "GET") + .map((handler) => String(handler.info.path)); + const literal = ordered.indexOf("/api/v1/series/full"); + const pattern = ordered.indexOf("/api/v1/series/:id"); + + expect(literal).toBeGreaterThanOrEqual(0); + expect(pattern).toBeGreaterThanOrEqual(0); + expect(literal).toBeLessThan(pattern); + }); +}); diff --git a/web/src/mocks/handlers/series.ts b/web/src/mocks/handlers/series.ts index 94015de6..52e727f4 100644 --- a/web/src/mocks/handlers/series.ts +++ b/web/src/mocks/handlers/series.ts @@ -1388,6 +1388,41 @@ export const seriesHandlers = [ }, ), + // Paginated series with their related data. + // + // The dedicated route that replaces the deprecated `?full=true` listing. It + // must be declared before `/series/:id`, since `full` would otherwise be read + // as a series id — the same ordering constraint `/in-progress` and friends + // have. + http.get("/api/v1/series/full", async ({ request }) => { + await delay(200); + const url = new URL(request.url); + const page = Math.max( + 1, + Number.parseInt(url.searchParams.get("page") || "1", 10), + ); + const pageSize = Number.parseInt( + url.searchParams.get("pageSize") || "50", + 10, + ); + const libraryId = url.searchParams.get("libraryId"); + + const filteredSeries = libraryId + ? getSeriesByLibrary(libraryId) + : mockSeries; + const start = (page - 1) * pageSize; + const items = filteredSeries.slice(start, start + pageSize); + + return HttpResponse.json( + createPaginatedResponse(items.map(toFullSeriesResponse), { + page, + pageSize, + total: filteredSeries.length, + basePath: "/api/v1/series/full", + }), + ); + }), + // Get series by ID (must come AFTER specific routes like /in-progress, /recently-added, etc.) // Supports ?full=true for full series response with metadata http.get("/api/v1/series/:id", async ({ params, request }) => { @@ -1406,6 +1441,17 @@ export const seriesHandlers = [ return HttpResponse.json(seriesItem); }), + // A single series with its related data — the dedicated route replacing + // `GET /api/v1/series/:id?full=true`. + http.get("/api/v1/series/:id/full", async ({ params }) => { + await delay(100); + const seriesItem = mockSeries.find((s) => s.id === params.id); + if (!seriesItem) { + return HttpResponse.json({ error: "Series not found" }, { status: 404 }); + } + return HttpResponse.json(toFullSeriesResponse(seriesItem)); + }), + // Get series thumbnail http.get("/api/v1/series/:id/thumbnail", async () => { await delay(50); diff --git a/web/src/pages/BookDetail.tsx b/web/src/pages/BookDetail.tsx index 12e4c4ba..0d06c28e 100644 --- a/web/src/pages/BookDetail.tsx +++ b/web/src/pages/BookDetail.tsx @@ -153,7 +153,7 @@ export function BookDetail() { error, } = useQuery({ queryKey: bookKeys.detailFull(bookId), - queryFn: () => booksApi.getDetail(bookId!, { full: true }), + queryFn: () => booksApi.getFull(bookId!), enabled: !!bookId, }); @@ -192,7 +192,7 @@ export function BookDetail() { // Fetch parent series (full) for building the book context's embedded series context const { data: parentSeries } = useQuery({ queryKey: ["series", bookDetail?.seriesId, "full"], - queryFn: () => seriesApi.getById(bookDetail!.seriesId, { full: true }), + queryFn: () => seriesApi.getFull(bookDetail!.seriesId), enabled: !!bookDetail?.seriesId, staleTime: 5 * 60 * 1000, }); diff --git a/web/src/pages/SeriesDetail.tsx b/web/src/pages/SeriesDetail.tsx index fd3eda5b..d9729c87 100644 --- a/web/src/pages/SeriesDetail.tsx +++ b/web/src/pages/SeriesDetail.tsx @@ -150,7 +150,7 @@ export function SeriesDetail() { error: seriesError, } = useQuery({ queryKey: ["series", seriesId, "full"], - queryFn: () => seriesApi.getById(seriesId!, { full: true }), + queryFn: () => seriesApi.getFull(seriesId!), enabled: !!seriesId, }); diff --git a/web/src/types/api.generated.ts b/web/src/types/api.generated.ts index 845f21d9..fca5bfba 100644 --- a/web/src/types/api.generated.ts +++ b/web/src/types/api.generated.ts @@ -1571,6 +1571,12 @@ export interface paths { * Download book file * @description Streams the original book file (CBZ, CBR, EPUB, PDF) for download. * Used by OPDS clients for acquisition links. + * + * Range-capable and conditional. `Accept-Ranges: bytes` and a strong `ETag` + * go out on every response, so a download interrupted partway can resume with + * `Range` rather than starting again, and a client that understands the + * container can read part of an archive without fetching all of it — the + * suffix form `bytes=-65536` reaches a ZIP central directory directly. */ get: operations["get_book_file"]; put?: never; @@ -1581,6 +1587,35 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/books/{book_id}/full": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get a book with its metadata, genres and tags in one response + * @description The same book `GET /api/v1/books/{book_id}` returns, plus the related data a + * detail screen needs, so it does not have to fan out into separate metadata, + * genre and tag requests. + * + * This exists as its own route because the shape is genuinely different, not + * merely richer: it carries `metadata`, `genres`, `tags`, `readCount` and + * `lastCompletedAt`, and it moves `chapter`, `summary` and `volume` inside + * `metadata`. A response whose schema depends on a query parameter cannot be + * expressed in OpenAPI, so the deprecated `?full=true` form is undescribable + * and unusable from a generated client. This route is describable. + */ + get: operations["get_book_full"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/books/{book_id}/metadata": { parameters: { query?: never; @@ -2498,6 +2533,104 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/libraries/{library_id}/jobs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List the scheduled jobs configured for a library + * @description Jobs are per-library scheduled work; today the only kind is a metadata + * refresh. The schedule is a cron expression evaluated in the job's timezone, + * or the server's if it declares none. + */ + get: operations["list_jobs"]; + put?: never; + /** + * Create a scheduled job for a library + * @description The job's type is taken from the `config` variant rather than named + * separately, so the two cannot disagree. A blank `name` is generated from the + * config. + */ + post: operations["create_job"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/libraries/{library_id}/jobs/{job_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get one scheduled job */ + get: operations["get_job"]; + put?: never; + post?: never; + /** Delete a scheduled job */ + delete: operations["delete_job"]; + options?: never; + head?: never; + /** + * Update a scheduled job + * @description Every field is optional. `timezone` is tri-state: absent leaves it, `null` + * clears it back to the server default, and a value sets it. A `config` whose + * type differs from the job's is rejected rather than migrating the job. + */ + patch: operations["patch_job"]; + trace?: never; + }; + "/api/v1/libraries/{library_id}/jobs/{job_id}/dry-run": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Preview what a job would change, without changing anything + * @description Plans the refresh and returns a sample of the per-series field changes it + * would make. Nothing is written. `configOverride` plans against a config the + * job does not have yet, which is what lets an editor preview an edit before + * saving it; it must be of the job's own type. + */ + post: operations["dry_run_job"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/libraries/{library_id}/jobs/{job_id}/run-now": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Run a scheduled job immediately + * @description Enqueues the job's task and returns its id; the run itself is asynchronous + * and its progress is followed through the task queue. Refuses if a run for + * this job is already in flight, so a double click cannot queue the work + * twice. + */ + post: operations["run_job_now"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/libraries/{library_id}/purge-deleted": { parameters: { query?: never; @@ -2704,6 +2837,28 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/library-jobs/metadata-refresh/field-groups": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List the metadata field groups a refresh job can target + * @description A static catalog: the groups and the concrete metadata fields each one + * covers. It is what a job editor offers as refreshable fields, so a client + * does not have to hardcode the list. + */ + get: operations["list_field_groups"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/metrics/inventory": { parameters: { query?: never; @@ -3846,6 +4001,34 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/series/full": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List series with their metadata, genres, tags and external links + * @description The same page `GET /api/v1/series` returns, with each entry carrying the + * related data inline, so a caller that needs metadata for a whole library + * pages it once instead of following up per series. + * + * This exists as its own route because the shape is genuinely different, not + * merely richer, and because a response whose schema depends on a query + * parameter cannot be expressed in OpenAPI — which made the deprecated + * `GET /api/v1/series?full=true` invisible to every generated client. Accepts + * the same filters and pagination as the plain listing. + */ + get: operations["list_series_full"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/series/in-progress": { parameters: { query?: never; @@ -4590,6 +4773,37 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/series/{series_id}/full": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get a series with its metadata, genres, tags and external links in one response + * @description The same series `GET /api/v1/series/{series_id}` returns, plus the related + * data a detail screen needs, so it does not have to fan out into separate + * metadata, genre, tag, external-id, external-link and rating requests. + * + * This exists as its own route because the shape is genuinely different, not + * merely richer: it carries `metadata`, `genres`, `tags`, `alternateTitles`, + * `externalIds`, `externalLinks`, `externalRatings`, `readCount` and + * `lastCompletedAt`, and it moves `title`, `titleSort`, `publisher`, `summary` + * and `year` inside `metadata`. A response whose schema depends on a query + * parameter cannot be expressed in OpenAPI, so the deprecated `?full=true` + * form is undescribable and unusable from a generated client. This route is + * describable. + */ + get: operations["get_series_full"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/series/{series_id}/genres": { parameters: { query?: never; @@ -8929,7 +9143,7 @@ export interface components { */ BookListRequest: { /** @description Filter condition (optional - no condition returns all) */ - condition?: Record | null; + condition?: components["schemas"]["BookCondition"]; /** @description Full-text search query (case-insensitive search on book title) */ fullTextSearch?: string | null; /** @description Include soft-deleted books in results (default: false) */ @@ -12899,50 +13113,7 @@ export interface components { */ KomgaPage_KomgaBookDto: { /** @description The content items for this page */ - content: { - /** @description Created timestamp (ISO 8601) */ - created: string; - /** @description Whether book is deleted (soft delete) */ - deleted?: boolean; - /** @description File hash */ - fileHash?: string; - /** @description File last modified timestamp (ISO 8601) */ - fileLastModified: string; - /** @description Book unique identifier (UUID as string) */ - id: string; - /** @description Last modified timestamp (ISO 8601) */ - lastModified: string; - /** @description Library ID */ - libraryId: string; - /** @description Media information */ - media: components["schemas"]["KomgaMediaDto"]; - /** @description Book metadata */ - metadata: components["schemas"]["KomgaBookMetadataDto"]; - /** @description Book filename/name */ - name: string; - /** - * Format: int32 - * @description Book number in series - */ - number: number; - /** @description Whether this is a oneshot */ - oneshot?: boolean; - /** @description User's read progress (null if not started) */ - readProgress?: components["schemas"]["KomgaReadProgressDto"]; - /** @description Series ID */ - seriesId: string; - /** @description Series title (required by Komic for display) */ - seriesTitle: string; - /** @description Human-readable file size (e.g., "869.9 MiB") */ - size: string; - /** - * Format: int64 - * @description File size in bytes - */ - sizeBytes: number; - /** @description File URL/path */ - url: string; - }[]; + content: components["schemas"]["KomgaBookDto"][]; /** @description Whether the page is empty */ empty: boolean; /** @description Whether this is the first page */ @@ -12986,22 +13157,7 @@ export interface components { */ KomgaPage_KomgaCollectionDto: { /** @description The content items for this page */ - content: { - /** @description Created timestamp (ISO 8601) */ - createdDate: string; - /** @description Whether this collection is filtered from the user's view */ - filtered: boolean; - /** @description Collection unique identifier */ - id: string; - /** @description Last modified timestamp (ISO 8601) */ - lastModifiedDate: string; - /** @description Collection name */ - name: string; - /** @description Whether the collection is ordered */ - ordered: boolean; - /** @description Series IDs in the collection */ - seriesIds: string[]; - }[]; + content: components["schemas"]["KomgaCollectionDto"][]; /** @description Whether the page is empty */ empty: boolean; /** @description Whether this is the first page */ @@ -13045,24 +13201,7 @@ export interface components { */ KomgaPage_KomgaReadListDto: { /** @description The content items for this page */ - content: { - /** @description Book IDs in the read list */ - bookIds: string[]; - /** @description Created timestamp (ISO 8601) */ - createdDate: string; - /** @description Whether this read list is filtered from the user's view */ - filtered: boolean; - /** @description Read list unique identifier */ - id: string; - /** @description Last modified timestamp (ISO 8601) */ - lastModifiedDate: string; - /** @description Read list name */ - name: string; - /** @description Whether the read list is ordered */ - ordered: boolean; - /** @description Read list summary/description */ - summary: string; - }[]; + content: components["schemas"]["KomgaReadListDto"][]; /** @description Whether the page is empty */ empty: boolean; /** @description Whether this is the first page */ @@ -13106,50 +13245,7 @@ export interface components { */ KomgaPage_KomgaSeriesDto: { /** @description The content items for this page */ - content: { - /** - * Format: int32 - * @description Total books count - */ - booksCount: number; - /** - * Format: int32 - * @description In-progress books count - */ - booksInProgressCount: number; - /** @description Aggregated books metadata */ - booksMetadata: components["schemas"]["KomgaBooksMetadataAggregationDto"]; - /** - * Format: int32 - * @description Read books count - */ - booksReadCount: number; - /** - * Format: int32 - * @description Unread books count - */ - booksUnreadCount: number; - /** @description Created timestamp (ISO 8601) */ - created: string; - /** @description Whether series is deleted (soft delete) */ - deleted?: boolean; - /** @description File last modified timestamp (ISO 8601) */ - fileLastModified: string; - /** @description Series unique identifier (UUID as string) */ - id: string; - /** @description Last modified timestamp (ISO 8601) */ - lastModified: string; - /** @description Library ID */ - libraryId: string; - /** @description Series metadata */ - metadata: components["schemas"]["KomgaSeriesMetadataDto"]; - /** @description Series name */ - name: string; - /** @description Whether this is a oneshot (single book) */ - oneshot?: boolean; - /** @description File URL/path */ - url: string; - }[]; + content: components["schemas"]["KomgaSeriesDto"][]; /** @description Whether the page is empty */ empty: boolean; /** @description Whether this is the first page */ @@ -14554,7 +14650,7 @@ export interface components { /** @description Generic paginated response wrapper with HATEOAS links */ PaginatedResponse: { /** @description The data items for this page */ - data: components["schemas"]["BookDto"][]; + data: components["schemas"]["FullBookResponse"][]; /** @description HATEOAS navigation links */ links: components["schemas"]["PaginationLinks"]; /** @@ -14585,61 +14681,7 @@ export interface components { /** @description Generic paginated response wrapper with HATEOAS links */ PaginatedResponse_ApiKeyDto: { /** @description The data items for this page */ - data: { - /** - * Format: date-time - * @description When the key was created - * @example 2024-01-01T00:00:00Z - */ - createdAt: string; - /** - * Format: date-time - * @description When the key expires (if set) - * @example 2025-12-31T23:59:59Z - */ - expiresAt?: string | null; - /** - * Format: uuid - * @description Unique API key identifier - * @example 550e8400-e29b-41d4-a716-446655440000 - */ - id: string; - /** - * @description Whether the key is currently active - * @example true - */ - isActive: boolean; - /** - * @description Prefix of the key for identification - * @example cdx_a1b2c3 - */ - keyPrefix: string; - /** - * Format: date-time - * @description When the key was last used - * @example 2024-01-15T10:30:00Z - */ - lastUsedAt?: string | null; - /** - * @description Human-readable name for the key - * @example Mobile App Key - */ - name: string; - /** @description Permissions granted to this key */ - permissions: unknown; - /** - * Format: date-time - * @description When the key was last updated - * @example 2024-01-15T10:30:00Z - */ - updatedAt: string; - /** - * Format: uuid - * @description Owner user ID - * @example 550e8400-e29b-41d4-a716-446655440001 - */ - userId: string; - }[]; + data: components["schemas"]["ApiKeyDto"][]; /** @description HATEOAS navigation links */ links: components["schemas"]["PaginationLinks"]; /** @@ -14670,143 +14712,38 @@ export interface components { /** @description Generic paginated response wrapper with HATEOAS links */ PaginatedResponse_BookDto: { /** @description The data items for this page */ - data: { - /** - * @description Error message if book analysis failed - * @example Failed to parse CBZ: invalid archive - */ - analysisError?: string | null; - /** - * @description Whether the book has been analyzed (page dimensions available) - * @example true - */ - analyzed: boolean; - /** - * Format: float - * @description Chapter number from book metadata (may be fractional) - * @example 42.5 - */ - chapter?: number | null; - /** - * Format: date-time - * @description When the book was added to the library - * @example 2024-01-01T00:00:00Z - */ - createdAt: string; - /** - * @description Whether the book has been soft-deleted - * @example false - */ - deleted: boolean; - /** - * @description File format (cbz, cbr, epub, pdf) - * @example cbz - */ - fileFormat: string; - /** - * @description File hash for deduplication - * @example a1b2c3d4e5f6g7h8i9j0 - */ - fileHash: string; - /** - * Format: int64 - * @description File size in bytes - * @example 52428800 - */ - fileSize: number; - /** - * Format: uuid - * @description Book unique identifier - * @example 550e8400-e29b-41d4-a716-446655440001 - */ - id: string; - /** @description KOReader-compatible partial MD5 hash for sync */ - koreaderHash?: string | null; - /** - * Format: uuid - * @description Library this book belongs to - * @example 550e8400-e29b-41d4-a716-446655440000 - */ - libraryId: string; - /** - * @description Name of the library - * @example Comics - */ - libraryName: string; - /** - * Format: int32 - * @description Book number within the series - * @example 1 - */ - number?: number | null; - /** - * Format: int32 - * @description Number of pages in the book - * @example 32 - */ - pageCount: number; - /** - * @description Filesystem path to the book file - * @example /media/comics/Batman/Batman - Year One 001.cbz - */ - path: string; - /** @description User's read progress for this book */ - readProgress?: components["schemas"]["ReadProgressResponse"]; - /** - * @description Effective reading direction (from series metadata, or library default if not set) - * Values: ltr, rtl, ttb or webtoon - * @example ltr - */ - readingDirection?: string | null; - /** - * Format: uuid - * @description Series this book belongs to - * @example 550e8400-e29b-41d4-a716-446655440002 - */ - seriesId: string; - /** - * @description Name of the series - * @example Batman: Year One - */ - seriesName: string; - /** - * @description Book summary/description from book_metadata (ComicInfo `` or - * EPUB description). Surfaced on the list response so cards can show it on - * hover without fetching the full detail payload. - * @example Bruce Wayne returns to Gotham to begin his war on crime. - */ - summary?: string | null; - /** - * @description Book title - * @example Batman: Year One #1 - */ - title: string; - /** - * @description Title used for sorting (title_sort field) - * @example batman year one 001 - */ - titleSort?: string | null; - /** - * Format: date-time - * @description When the book was last updated - * @example 2024-01-15T10:30:00Z - */ - updatedAt: string; - /** - * Format: int32 - * @description Volume number from book metadata - * @example 1 - */ - volume?: number | null; - /** - * @description Whether the requesting user has this book in their want-to-read queue. - * - * `None` when not computed for this response; populated on book list and - * detail endpoints that have a user context. - * @example false - */ - wantToRead?: boolean | null; - }[]; + data: components["schemas"]["BookDto"][]; + /** @description HATEOAS navigation links */ + links: components["schemas"]["PaginationLinks"]; + /** + * Format: int64 + * @description Current page number (1-indexed) + * @example 1 + */ + page: number; + /** + * Format: int64 + * @description Number of items per page + * @example 50 + */ + pageSize: number; + /** + * Format: int64 + * @description Total number of items across all pages + * @example 150 + */ + total: number; + /** + * Format: int64 + * @description Total number of pages + * @example 3 + */ + totalPages: number; + }; + /** @description Generic paginated response wrapper with HATEOAS links */ + PaginatedResponse_FullSeriesResponse: { + /** @description The data items for this page */ + data: components["schemas"]["FullSeriesResponse"][]; /** @description HATEOAS navigation links */ links: components["schemas"]["PaginationLinks"]; /** @@ -14837,31 +14774,7 @@ export interface components { /** @description Generic paginated response wrapper with HATEOAS links */ PaginatedResponse_GenreDto: { /** @description The data items for this page */ - data: { - /** - * Format: date-time - * @description When the genre was created - * @example 2024-01-01T00:00:00Z - */ - createdAt: string; - /** - * Format: uuid - * @description Genre ID - * @example 550e8400-e29b-41d4-a716-446655440010 - */ - id: string; - /** - * @description Genre name - * @example Action - */ - name: string; - /** - * Format: int64 - * @description Number of series with this genre - * @example 42 - */ - seriesCount?: number | null; - }[]; + data: components["schemas"]["GenreDto"][]; /** @description HATEOAS navigation links */ links: components["schemas"]["PaginationLinks"]; /** @@ -14892,108 +14805,7 @@ export interface components { /** @description Generic paginated response wrapper with HATEOAS links */ PaginatedResponse_LibraryDto: { /** @description The data items for this page */ - data: { - /** - * @description Allowed file formats (e.g., ["CBZ", "CBR", "EPUB"]) - * @example [ - * "CBZ", - * "CBR", - * "PDF" - * ] - */ - allowedFormats?: string[] | null; - /** - * @description Auto-match conditions (JSON object with mode and rules) - * Controls when auto-matching runs for this library - */ - autoMatchConditions?: unknown; - /** @description Book strategy-specific configuration (JSON) */ - bookConfig?: unknown; - /** - * Format: int64 - * @description Total number of books in this library - * @example 1250 - */ - bookCount?: number | null; - /** @description Book naming strategy (filename, metadata_first, smart, series_name) */ - bookStrategy: components["schemas"]["BookStrategy"]; - /** - * Format: date-time - * @description When the library was created - * @example 2024-01-01T00:00:00Z - */ - createdAt: string; - /** - * @description Default reading direction for books in this library (ltr, rtl, ttb or webtoon) - * @example ltr - */ - defaultReadingDirection: string; - /** - * @description Optional description - * @example My comic book collection - */ - description?: string | null; - /** - * @description Excluded path patterns (newline-separated, e.g., ".DS_Store\nThumbs.db") - * @example .DS_Store - * Thumbs.db - */ - excludedPatterns?: string | null; - /** - * Format: uuid - * @description Library unique identifier - * @example 550e8400-e29b-41d4-a716-446655440000 - */ - id: string; - /** - * @description Whether the library is active - * @example true - */ - isActive: boolean; - /** - * Format: date-time - * @description When the library was last scanned - * @example 2024-01-15T10:30:00Z - */ - lastScannedAt?: string | null; - /** - * @description Library name - * @example Comics - */ - name: string; - /** @description Number strategy-specific configuration (JSON) */ - numberConfig?: unknown; - /** @description Book number strategy (file_order, metadata, filename, smart) */ - numberStrategy: components["schemas"]["NumberStrategy"]; - /** - * @description Filesystem path to the library root - * @example /media/comics - */ - path: string; - /** @description Scanning configuration for scheduled scans */ - scanningConfig?: components["schemas"]["ScanningConfigDto"]; - /** @description Strategy-specific configuration (JSON) */ - seriesConfig?: unknown; - /** - * Format: int64 - * @description Total number of series in this library - * @example 85 - */ - seriesCount?: number | null; - /** @description Series detection strategy (series_volume, series_volume_chapter, flat, etc.) */ - seriesStrategy: components["schemas"]["SeriesStrategy"]; - /** - * @description Title preprocessing rules (JSON array of regex rules) - * Applied during scan to clean series titles before metadata search - */ - titlePreprocessingRules?: unknown; - /** - * Format: date-time - * @description When the library was last updated - * @example 2024-01-15T10:30:00Z - */ - updatedAt: string; - }[]; + data: components["schemas"]["LibraryDto"][]; /** @description HATEOAS navigation links */ links: components["schemas"]["PaginationLinks"]; /** @@ -15024,88 +14836,7 @@ export interface components { /** @description Generic paginated response wrapper with HATEOAS links */ PaginatedResponse_ReleaseLedgerEntryDto: { /** @description The data items for this page */ - data: { - /** - * @description Full chapter coverage as a normalized span list. Decimals supported - * (`c12.5` → `[{start: 12.5, end: 12.5}]`). `null` when the upstream - * title carried no chapter info. - */ - chapters?: components["schemas"]["ReleaseSpanDto"][] | null; - /** Format: double */ - confidence: number; - /** Format: date-time */ - createdAt: string; - /** - * @description Plugin-stable identity for the release (used for dedup). - * @example nyaa:1234567 - */ - externalReleaseId: string; - /** @description Sparse `{ "jxl": true, "container": "cbz", ... }`. */ - formatHints?: unknown; - /** @description Group/scanlator/uploader attribution. */ - groupOrUploader?: string | null; - /** - * Format: uuid - * @example 550e8400-e29b-41d4-a716-446655440a00 - */ - id: string; - /** @description Torrent info_hash, if applicable. */ - infoHash?: string | null; - language?: string | null; - /** - * @description Optional second URL for direct fetch (`.torrent`, `magnet:`, DDL - * link). Travels paired with [`Self::media_url_kind`]. - */ - mediaUrl?: string | null; - /** - * @description Classifies what `media_url` points at: `torrent` | `magnet` | - * `direct` | `other`. The frontend uses this to pick a kind-specific - * icon next to the standard external-link icon. - */ - mediaUrlKind?: string | null; - /** @description Source-specific extras (free-form). */ - metadata?: unknown; - /** - * Format: date-time - * @description When Codex detected this release. - */ - observedAt: string; - /** - * @description Where to acquire the release. Conventionally a human-readable - * landing page (Nyaa view page, MangaUpdates release page). - */ - payloadUrl: string; - /** - * Format: date-time - * @description Upstream publish date from the source feed. `null` when unavailable. - */ - releasedAt?: string | null; - /** - * Format: uuid - * @example 550e8400-e29b-41d4-a716-446655440002 - */ - seriesId: string; - /** - * @description Series title at the time of the response. Joined from the `series` - * table so the inbox UI can render a human-readable label without a - * follow-up fetch. Falls back to the empty string only if the series - * row was hard-deleted between the join and the read. - * @example Chainsaw Man - */ - seriesTitle: string; - /** - * Format: uuid - * @example 550e8400-e29b-41d4-a716-446655440b00 - */ - sourceId: string; - /** @description `announced` | `dismissed` | `marked_acquired` | `hidden`. */ - state: string; - /** - * @description Full volume coverage as a normalized span list. `null` semantics - * mirror [`Self::chapters`]. - */ - volumes?: components["schemas"]["ReleaseSpanDto"][] | null; - }[]; + data: components["schemas"]["ReleaseLedgerEntryDto"][]; /** @description HATEOAS navigation links */ links: components["schemas"]["PaginationLinks"]; /** @@ -15136,171 +14867,38 @@ export interface components { /** @description Generic paginated response wrapper with HATEOAS links */ PaginatedResponse_SeriesDto: { /** @description The data items for this page */ - data: { - /** - * Format: int64 - * @description Total number of books in this series - * @example 4 - */ - bookCount: number; - /** - * Format: date-time - * @description When the series was created - * @example 2024-01-01T00:00:00Z - */ - createdAt: string; - /** - * @description Whether the series has a custom cover uploaded - * @example false - */ - hasCustomCover?: boolean | null; - /** - * Format: uuid - * @description Series unique identifier - * @example 550e8400-e29b-41d4-a716-446655440002 - */ - id: string; - /** - * Format: uuid - * @description Library unique identifier - * @example 550e8400-e29b-41d4-a716-446655440000 - */ - libraryId: string; - /** - * @description Name of the library this series belongs to - * @example Comics - */ - libraryName: string; - /** - * Format: float - * @description Highest `book_metadata.chapter` across the books in this series. - * - * `None` when no book in the series has `chapter` populated. When - * non-null and `metadata.totalChapterCount` is also known, the UI renders - * `/ ch`. - * @example 137.5 - */ - localMaxChapter?: number | null; - /** - * Format: int32 - * @description Highest `book_metadata.volume` across the books in this series. - * - * `None` when no book in the series has `volume` populated. When - * non-null and `metadata.totalVolumeCount` is also known, the UI renders - * `/ vol` instead of the legacy - * `/ vol`. - * @example 14 - */ - localMaxVolume?: number | null; - /** - * @description Filesystem path to the series directory - * @example /media/comics/Batman - Year One - */ - path?: string | null; - /** - * @description Publisher name - * @example DC Comics - */ - publisher?: string | null; - /** - * @description Selected cover source (e.g., "first_book", "custom") - * @example first_book - */ - selectedCoverSource?: string | null; - /** - * @description Summary/description from series_metadata - * @example The definitive origin story of Batman, following Bruce Wayne's first year as a vigilante. - */ - summary?: string | null; - /** - * @description Series title from series_metadata - * @example Batman: Year One - */ - title: string; - /** - * @description Sort title from series_metadata (for ordering) - * @example batman year one - */ - titleSort?: string | null; - /** - * @description Whether release tracking is enabled for this series. - * - * Mirrors `series_tracking.tracked`. `false` when no tracking row exists. - * Exposed so list views can surface a tracking indicator without an extra - * per-card request. - * @example false - */ - tracked: boolean; - /** - * Format: int64 - * @description Number of unread books in this series (user-specific) - * @example 2 - */ - unreadCount?: number | null; - /** - * Format: date-time - * @description When the series was last updated - * @example 2024-01-15T10:30:00Z - */ - updatedAt: string; - /** - * Format: float - * @description Difference between the upstream original-language chapter count - * (`series_metadata.total_chapter_count`, supplied by metadata - * providers like MangaBaka or AniList) and the highest locally-owned - * chapter (`local_max_chapter`). - * - * Always `None` unless the series is tracked AND `track_chapters` is - * enabled AND the provider count is populated AND the rounded-to-1- - * decimal gap is positive. **This is an informational signal, not a - * release announcement**; the MangaUpdates plugin owns the - * translation-release feed. - * @example 3 - */ - upstreamChapterGap?: number | null; - /** - * @description Display name of the metadata provider that supplied the upstream - * counts (e.g., "MangaBaka", "AniList"). Set whenever at least one of - * `upstream_chapter_gap` / `upstream_volume_gap` is populated. Used by - * the gap badge tooltip. - * @example MangaBaka - */ - upstreamGapProvider?: string | null; - /** - * Format: int32 - * @description Difference between the upstream original-language volume count - * (`series_metadata.total_volume_count`) and the highest locally-owned - * volume (`local_max_volume`). Same suppression rules as - * `upstream_chapter_gap`, gated on `track_volumes`. - * @example 1 - */ - upstreamVolumeGap?: number | null; - /** - * Format: int64 - * @description Number of books in this series classified as a complete volume - * (`volume IS NOT NULL AND chapter IS NULL`). - * - * Distinct from `bookCount`: a chapter inside a volume (`v15 c126`) - * counts as a chapter, not a volume. `None` when no books exist; - * `Some(0)` when books exist but none are complete volumes. - * @example 14 - */ - volumesOwned?: number | null; - /** - * @description Whether the requesting user has this series in their want-to-read queue. - * - * `None` when not computed for this response (e.g. list endpoints that - * don't enrich it); populated on the series detail endpoint. - * @example false - */ - wantToRead?: boolean | null; - /** - * Format: int32 - * @description Release year - * @example 1987 - */ - year?: number | null; - }[]; + data: components["schemas"]["SeriesDto"][]; + /** @description HATEOAS navigation links */ + links: components["schemas"]["PaginationLinks"]; + /** + * Format: int64 + * @description Current page number (1-indexed) + * @example 1 + */ + page: number; + /** + * Format: int64 + * @description Number of items per page + * @example 50 + */ + pageSize: number; + /** + * Format: int64 + * @description Total number of items across all pages + * @example 150 + */ + total: number; + /** + * Format: int64 + * @description Total number of pages + * @example 3 + */ + totalPages: number; + }; + /** @description Generic paginated response wrapper with HATEOAS links */ + PaginatedResponse_SeriesExternalIndexDto: { + /** @description The data items for this page */ + data: components["schemas"]["SeriesExternalIndexDto"][]; /** @description HATEOAS navigation links */ links: components["schemas"]["PaginationLinks"]; /** @@ -15331,48 +14929,7 @@ export interface components { /** @description Generic paginated response wrapper with HATEOAS links */ PaginatedResponse_SharingTagDto: { /** @description The data items for this page */ - data: { - /** - * Format: date-time - * @description Creation timestamp - * @example 2024-01-01T00:00:00Z - */ - createdAt: string; - /** - * @description Optional description - * @example Content appropriate for children - */ - description?: string | null; - /** - * Format: uuid - * @description Unique sharing tag identifier - * @example 550e8400-e29b-41d4-a716-446655440000 - */ - id: string; - /** - * @description Display name of the sharing tag - * @example Kids Content - */ - name: string; - /** - * Format: int64 - * @description Number of series tagged with this sharing tag - * @example 42 - */ - seriesCount: number; - /** - * Format: date-time - * @description Last update timestamp - * @example 2024-01-15T10:30:00Z - */ - updatedAt: string; - /** - * Format: int64 - * @description Number of users with grants for this sharing tag - * @example 5 - */ - userCount: number; - }[]; + data: components["schemas"]["SharingTagDto"][]; /** @description HATEOAS navigation links */ links: components["schemas"]["PaginationLinks"]; /** @@ -15403,31 +14960,7 @@ export interface components { /** @description Generic paginated response wrapper with HATEOAS links */ PaginatedResponse_TagDto: { /** @description The data items for this page */ - data: { - /** - * Format: date-time - * @description When the tag was created - * @example 2024-01-01T00:00:00Z - */ - createdAt: string; - /** - * Format: uuid - * @description Tag ID - * @example 550e8400-e29b-41d4-a716-446655440020 - */ - id: string; - /** - * @description Tag name - * @example Completed - */ - name: string; - /** - * Format: int64 - * @description Number of series with this tag - * @example 15 - */ - seriesCount?: number | null; - }[]; + data: components["schemas"]["TagDto"][]; /** @description HATEOAS navigation links */ links: components["schemas"]["PaginationLinks"]; /** @@ -15458,51 +14991,7 @@ export interface components { /** @description Generic paginated response wrapper with HATEOAS links */ PaginatedResponse_UserDto: { /** @description The data items for this page */ - data: { - /** - * Format: date-time - * @description Account creation timestamp - * @example 2024-01-01T00:00:00Z - */ - createdAt: string; - /** - * @description User email address - * @example john.doe@example.com - */ - email: string; - /** - * Format: uuid - * @description Unique user identifier - * @example 550e8400-e29b-41d4-a716-446655440000 - */ - id: string; - /** - * @description Whether the account is active - * @example true - */ - isActive: boolean; - /** - * Format: date-time - * @description Timestamp of last login - * @example 2024-01-15T10:30:00Z - */ - lastLoginAt?: string | null; - /** @description Custom permissions that extend the role's base permissions */ - permissions: string[]; - /** @description User role (reader, maintainer, admin) */ - role: components["schemas"]["UserRole"]; - /** - * Format: date-time - * @description Last account update timestamp - * @example 2024-01-15T10:30:00Z - */ - updatedAt: string; - /** - * @description Username for login - * @example johndoe - */ - username: string; - }[]; + data: components["schemas"]["UserDto"][]; /** @description HATEOAS navigation links */ links: components["schemas"]["PaginationLinks"]; /** @@ -18399,7 +17888,14 @@ export interface components { }; /** @description Search series request */ SearchSeriesRequest: { - /** @description Return full series data including metadata, locks, genres, tags, etc. */ + /** + * @deprecated + * @description Return full series data including metadata, locks, genres, tags, etc. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. + */ full?: boolean; /** * Format: uuid @@ -19177,7 +18673,7 @@ export interface components { */ SeriesListRequest: { /** @description Filter condition (optional - no condition returns all) */ - condition?: Record | null; + condition?: components["schemas"]["SeriesCondition"]; /** @description Full-text search query (case-insensitive search on series name) */ fullTextSearch?: string | null; }; @@ -23842,8 +23338,13 @@ export interface operations { /** @description Sort parameter (format: "field,direction" e.g. "title,asc") */ sort?: string | null; /** + * @deprecated * @description Return full data including metadata and locks. * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. */ full?: boolean; }; @@ -23859,7 +23360,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -24275,8 +23776,13 @@ export interface operations { /** @description Sort parameter (format: "field,direction" e.g. "title,asc") */ sort?: string | null; /** + * @deprecated * @description Return full data including metadata and locks. * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. */ full?: boolean; }; @@ -24292,7 +23798,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -24314,8 +23820,13 @@ export interface operations { /** @description Sort field and direction (e.g., "name,asc" or "createdAt,desc") */ sort?: string | null; /** + * @deprecated * @description Return full data including metadata, locks, and related entities. * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. */ full?: boolean; }; @@ -24335,7 +23846,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -24361,8 +23872,13 @@ export interface operations { /** @description Sort parameter (format: "field,direction" e.g. "title,asc") */ sort?: string | null; /** + * @deprecated * @description Return full data including metadata and locks. * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. */ full?: boolean; }; @@ -24378,7 +23894,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -24404,8 +23920,13 @@ export interface operations { /** @description Sort parameter (format: "field,direction" e.g. "title,asc") */ sort?: string | null; /** + * @deprecated * @description Return full data including metadata and locks. * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. */ full?: boolean; }; @@ -24421,7 +23942,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -24543,8 +24064,13 @@ export interface operations { parameters: { query?: { /** + * @deprecated * @description Return full data including metadata and locks. * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. */ full?: boolean; }; @@ -25201,7 +24727,14 @@ export interface operations { get_book_file: { parameters: { query?: never; - header?: never; + header?: { + /** @description Byte range, e.g. `bytes=0-1023`, `bytes=1024-` or `bytes=-65536`. A single range only; multiple ranges are answered with the whole file. */ + Range?: string | null; + /** @description Serve the range only if this ETag still matches; otherwise the whole file is returned, so a resume cannot splice bytes from two versions. */ + "If-Range"?: string | null; + /** @description Return 304 if this ETag still matches. */ + "If-None-Match"?: string | null; + }; path: { /** @description Book ID */ book_id: string; @@ -25216,9 +24749,77 @@ export interface operations { [name: string]: unknown; }; content: { + "application/zip": unknown; + "application/x-rar-compressed": unknown; + "application/epub+zip": unknown; + "application/pdf": unknown; + "application/octet-stream": unknown; + }; + }; + /** @description The requested byte range */ + 206: { + headers: { + [name: string]: unknown; + }; + content: { + "application/zip": unknown; + "application/x-rar-compressed": unknown; + "application/epub+zip": unknown; + "application/pdf": unknown; "application/octet-stream": unknown; }; }; + /** @description Not modified (client cache is valid) */ + 304: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Book not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description The requested range names no byte of the file */ + 416: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + get_book_full: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Book ID */ + book_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Book with its related data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FullBookResponse"]; + }; + }; /** @description Forbidden */ 403: { headers: { @@ -25455,7 +25056,7 @@ export interface operations { [name: string]: unknown; }; content: { - "image/jpeg": unknown; + "image/*": unknown; }; }; /** @description Not modified (client cache is valid) */ @@ -25892,7 +25493,7 @@ export interface operations { [name: string]: unknown; }; content: { - "image/jpeg": unknown; + "image/*": unknown; }; }; /** @description Not modified (client cache is valid) */ @@ -27279,15 +26880,28 @@ export interface operations { }; list_library_books: { parameters: { - query?: never; + query?: { + /** @description Page number (1-indexed, minimum 1) */ + page?: number; + /** @description Number of items per page (max 100, default 50) */ + pageSize?: number; + /** @description Sort parameter (format: "field,direction" e.g. "title,asc") */ + sort?: string; + /** + * @deprecated + * @description Return full data including metadata and locks. + * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. + */ + full?: boolean; + }; header?: never; path: { /** @description Library ID */ library_id: string; - /** @description Page number (1-indexed, minimum 1) */ - page: number; - /** @description Number of items per page (max 100, default 50) */ - pageSize: number; }; cookie?: never; }; @@ -27299,7 +26913,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -27313,15 +26927,28 @@ export interface operations { }; list_library_in_progress_books: { parameters: { - query?: never; + query?: { + /** @description Page number (1-indexed, minimum 1) */ + page?: number; + /** @description Number of items per page (max 100, default 50) */ + pageSize?: number; + /** @description Sort parameter (format: "field,direction" e.g. "title,asc") */ + sort?: string; + /** + * @deprecated + * @description Return full data including metadata and locks. + * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. + */ + full?: boolean; + }; header?: never; path: { /** @description Library ID */ library_id: string; - /** @description Page number (1-indexed, minimum 1) */ - page: number; - /** @description Number of items per page (max 100, default 50) */ - pageSize: number; }; cookie?: never; }; @@ -27333,7 +26960,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -27347,15 +26974,28 @@ export interface operations { }; list_library_on_deck_books: { parameters: { - query?: never; + query?: { + /** @description Page number (1-indexed, minimum 1) */ + page?: number; + /** @description Number of items per page (max 100, default 50) */ + pageSize?: number; + /** @description Sort parameter (format: "field,direction" e.g. "title,asc") */ + sort?: string; + /** + * @deprecated + * @description Return full data including metadata and locks. + * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. + */ + full?: boolean; + }; header?: never; path: { /** @description Library ID */ library_id: string; - /** @description Page number (1-indexed, minimum 1) */ - page: number; - /** @description Number of items per page (max 100, default 50) */ - pageSize: number; }; cookie?: never; }; @@ -27367,7 +27007,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -27381,15 +27021,28 @@ export interface operations { }; list_library_recently_added_books: { parameters: { - query?: never; + query?: { + /** @description Page number (1-indexed, minimum 1) */ + page?: number; + /** @description Number of items per page (max 100, default 50) */ + pageSize?: number; + /** @description Sort parameter (format: "field,direction" e.g. "title,asc") */ + sort?: string; + /** + * @deprecated + * @description Return full data including metadata and locks. + * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. + */ + full?: boolean; + }; header?: never; path: { /** @description Library ID */ library_id: string; - /** @description Page number (1-indexed, minimum 1) */ - page: number; - /** @description Number of items per page (max 100, default 50) */ - pageSize: number; }; cookie?: never; }; @@ -27401,7 +27054,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -27487,6 +27140,313 @@ export interface operations { }; }; }; + list_jobs: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Library ID */ + library_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The library's jobs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListLibraryJobsResponse"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Library not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + create_job: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Library ID */ + library_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateLibraryJobRequest"]; + }; + }; + responses: { + /** @description The created job */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LibraryJobDto"]; + }; + }; + /** @description Invalid cron expression, timezone, or config */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Library not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + get_job: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Library ID */ + library_id: string; + /** @description Job ID */ + job_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The job */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LibraryJobDto"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Job not found, or it belongs to another library */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete_job: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Library ID */ + library_id: string; + /** @description Job ID */ + job_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Job deleted */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Job not found, or it belongs to another library */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + patch_job: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Library ID */ + library_id: string; + /** @description Job ID */ + job_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PatchLibraryJobRequest"]; + }; + }; + responses: { + /** @description The updated job */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LibraryJobDto"]; + }; + }; + /** @description Empty name, invalid schedule, or a config of the wrong type */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Job not found, or it belongs to another library */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + dry_run_job: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Library ID */ + library_id: string; + /** @description Job ID */ + job_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DryRunRequest"]; + }; + }; + responses: { + /** @description The planned changes */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DryRunResponse"]; + }; + }; + /** @description Override config is of the wrong type, or is invalid */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Job not found, or it belongs to another library */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + run_job_now: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Library ID */ + library_id: string; + /** @description Job ID */ + job_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The enqueued task */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RunNowResponse"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Job not found, or it belongs to another library */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description A run for this job is already in flight */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; purge_deleted_books: { parameters: { query?: never; @@ -27666,8 +27626,13 @@ export interface operations { /** @description Filter by library ID */ libraryId?: string | null; /** + * @deprecated * @description Return full series data including metadata, locks, genres, tags, alternate titles, * external ratings, and external links. Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. */ full?: boolean; }; @@ -27686,7 +27651,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_SeriesDto"]; }; }; /** @description Forbidden */ @@ -27701,7 +27666,14 @@ export interface operations { list_library_in_progress_series: { parameters: { query?: { - /** @description Return full series data including metadata, locks, genres, tags, etc. */ + /** + * @deprecated + * @description Return full series data including metadata, locks, genres, tags, etc. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. + */ full?: boolean; }; header?: never; @@ -27738,7 +27710,14 @@ export interface operations { limit?: number; /** @description Filter by library ID (optional) */ libraryId?: string | null; - /** @description Return full series data including metadata, locks, genres, tags, etc. */ + /** + * @deprecated + * @description Return full series data including metadata, locks, genres, tags, etc. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. + */ full?: boolean; }; header?: never; @@ -27775,7 +27754,14 @@ export interface operations { limit?: number; /** @description Filter by library ID (optional) */ libraryId?: string | null; - /** @description Return full series data including metadata, locks, genres, tags, etc. */ + /** + * @deprecated + * @description Return full series data including metadata, locks, genres, tags, etc. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. + */ full?: boolean; }; header?: never; @@ -27887,6 +27873,33 @@ export interface operations { }; }; }; + list_field_groups: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The field-group catalog */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FieldGroupDto"][]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; get_inventory_metrics: { parameters: { query?: never; @@ -29497,8 +29510,13 @@ export interface operations { /** @description Filter by library ID */ libraryId?: string | null; /** + * @deprecated * @description Return full series data including metadata, locks, genres, tags, alternate titles, * external ratings, and external links. Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. */ full?: boolean; }; @@ -29514,7 +29532,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_SeriesDto"]; }; }; /** @description Forbidden */ @@ -30110,8 +30128,13 @@ export interface operations { /** @description Sort field and direction (e.g., "name,asc" or "createdAt,desc") */ sort?: string | null; /** + * @deprecated * @description Return full data including metadata, locks, and related entities. * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. */ full?: boolean; }; @@ -30127,7 +30150,47 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_SeriesExternalIndexDto"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + list_series_full: { + parameters: { + query?: { + /** @description Page number (1-indexed, default 1) */ + page?: number; + /** @description Number of items per page (max 100, default 50) */ + pageSize?: number; + /** @description Sort parameter (format: "field,direction" e.g. "name,asc") */ + sort?: string | null; + /** @description Filter by genres (comma-separated, AND logic - series must have ALL specified genres) */ + genres?: string | null; + /** @description Filter by tags (comma-separated, AND logic - series must have ALL specified tags) */ + tags?: string | null; + /** @description Filter by library ID */ + libraryId?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated series with their related data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PaginatedResponse_FullSeriesResponse"]; }; }; /** @description Forbidden */ @@ -30144,7 +30207,14 @@ export interface operations { query?: { /** @description Filter by library ID (optional) */ libraryId?: string | null; - /** @description Return full series data including metadata, locks, genres, tags, etc. */ + /** + * @deprecated + * @description Return full series data including metadata, locks, genres, tags, etc. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. + */ full?: boolean; }; header?: never; @@ -30181,8 +30251,13 @@ export interface operations { /** @description Sort field and direction (e.g., "name,asc" or "createdAt,desc") */ sort?: string | null; /** + * @deprecated * @description Return full data including metadata, locks, and related entities. * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. */ full?: boolean; }; @@ -30202,7 +30277,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_SeriesDto"]; }; }; /** @description Forbidden */ @@ -30304,7 +30379,14 @@ export interface operations { limit?: number; /** @description Filter by library ID (optional) */ libraryId?: string | null; - /** @description Return full series data including metadata, locks, genres, tags, etc. */ + /** + * @deprecated + * @description Return full series data including metadata, locks, genres, tags, etc. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. + */ full?: boolean; }; header?: never; @@ -30338,7 +30420,14 @@ export interface operations { limit?: number; /** @description Filter by library ID (optional) */ libraryId?: string | null; - /** @description Return full series data including metadata, locks, genres, tags, etc. */ + /** + * @deprecated + * @description Return full series data including metadata, locks, genres, tags, etc. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. + */ full?: boolean; }; header?: never; @@ -30728,7 +30817,14 @@ export interface operations { get_series: { parameters: { query?: { - /** @description Return full series data including metadata, locks, genres, tags, etc. */ + /** + * @deprecated + * @description Return full series data including metadata, locks, genres, tags, etc. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. + */ full?: boolean; }; header?: never; @@ -31168,8 +31264,13 @@ export interface operations { /** @description Include deleted books in the result */ includeDeleted?: boolean; /** + * @deprecated * @description Return full data including metadata and locks. * Default is false for backward compatibility. + * **Deprecated.** Prefer `GET /books/{book_id}/full` and + * `GET /series/{series_id}/full`. A response whose schema depends on a + * query parameter cannot be expressed in OpenAPI, so this form is + * invisible to a generated client. Scheduled for removal in 3.0. */ full?: boolean; }; @@ -31905,6 +32006,43 @@ export interface operations { }; }; }; + get_series_full: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Series ID */ + series_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Series with its related data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FullSeriesResponse"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Series not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; get_series_genres: { parameters: { query?: never; @@ -33021,7 +33159,7 @@ export interface operations { [name: string]: unknown; }; content: { - "image/jpeg": unknown; + "image/*": unknown; }; }; /** @description Not modified (client cache is valid) */ @@ -34019,7 +34157,9 @@ export interface operations { [name: string]: unknown; }; content: { - "application/octet-stream": unknown; + "text/csv": unknown; + "text/markdown": unknown; + "application/json": unknown; }; }; /** @description Export not found or file missing */ @@ -35571,7 +35711,7 @@ export interface operations { [name: string]: unknown; }; content: { - "image/jpeg": unknown; + "image/*": unknown; }; }; /** @description Forbidden */ diff --git a/web/vitest.config.ts b/web/vitest.config.ts index 5d597bcb..b3451663 100644 --- a/web/vitest.config.ts +++ b/web/vitest.config.ts @@ -9,6 +9,28 @@ export default defineConfig({ globals: true, environment: "jsdom", setupFiles: "./src/test/setup.ts", + // Vitest's 5s default is sized for fast unit tests. 74 of these files drive + // the UI through `userEvent`, which awaits a React render per keystroke, and + // the runner gives every core its own jsdom environment. Under that + // contention the heavier interaction tests legitimately exceed 5s: the full + // suite failed a different one on each run while every one of them passed in + // isolation. The budget was wrong for the environment, not the tests. + // + // Individual `findBy*` calls keep their own shorter timeouts, so a genuinely + // missing element still fails quickly. What this costs is that a truly hung + // test now takes 20s to report rather than 5s. + testTimeout: 20000, + hookTimeout: 20000, + // Cap the worker pool at half the logical cores. Vitest defaults to one + // worker per core, and each runs a full jsdom + React + Mantine portal + // pipeline, so on a 12-thread machine twelve of them starve each other and + // the heavier interaction tests miss their deadline. The failures were + // spread across whichever tests happened to lose the race — InstallNudgeModal, + // TemplateSelector, MediaCard, AddLibraryModal — rather than concentrated in + // one broken test, which is the signature of contention rather than a bug. + // + // Fewer workers with real CPU each finish sooner than more workers thrashing. + maxWorkers: "50%", coverage: { provider: "v8", reporter: ["text", "json", "html"],