From b020b8719db41791e2f9167dc58327a8a4eb91ad Mon Sep 17 00:00:00 2001 From: Sylvain Cau Date: Thu, 20 Aug 2026 20:09:35 -0700 Subject: [PATCH 1/9] fix(api): describe series pages, library book params, and the filter grammar correctly Three OpenAPI description defects, reported by a strict generated client. None changes the wire format; all three make the document match what the handlers already do. Series listings were documented as pages of books. `PaginatedResponse` named through a `pub type` alias loses its type argument and collapses to the base component, which held whichever instantiation the registry rendered into that slot: `data: [BookDto]`. Four series operations therefore claimed to return books, and a strict client threw on the first item of the first page. All alias-named bodies now name the generic inline. The nine book listings were correct only by accident of that slot's contents, so they are fixed too, which is what stops them becoming a silent regression later. Four library book endpoints documented an `IntoParams` struct they never extracted. utoipa infers a parameter's location from the handler's extractor, and with nothing to infer from it falls back to `Path`, so `page` and `pageSize` rendered as path parameters of a path with no such segments: an operation no client could construct. They now name a struct describing exactly what those routes honour, with an explicit `parameter_in`, which also exposes `sort` and `full` for the first time. The struct that existed only to be named incorrectly is deleted. The filter grammar was erased to an untyped object by a `value_type` override, so the primary query interface reached generated clients as a freeform dictionary and was discoverable only by reading the source. The override was not load-bearing: the recursive schemas render correctly and the web build is clean without it. Adds document invariants covering all three, each a whole-document walk so it holds for operations added later: no operation may reference a generic wrapper with its argument discarded, path parameters and path templates must agree in both directions, and the filter conditions must reference their grammar. --- crates/codex-api/src/docs.rs | 1 + crates/codex-api/src/routes/v1/dto/common.rs | 116 --------- crates/codex-api/src/routes/v1/dto/filter.rs | 2 - .../codex-api/src/routes/v1/handlers/books.rs | 65 +++-- .../src/routes/v1/handlers/series.rs | 8 +- docs/api/openapi.json | 242 +++++++++++++++--- tests/api/openapi_spec.rs | 151 +++++++++++ web/openapi.json | 242 +++++++++++++++--- web/src/types/api.generated.ts | 165 +++++++++--- 9 files changed, 743 insertions(+), 249 deletions(-) diff --git a/crates/codex-api/src/docs.rs b/crates/codex-api/src/docs.rs index 552584e31..8fc3b0f78 100644 --- a/crates/codex-api/src/docs.rs +++ b/crates/codex-api/src/docs.rs @@ -954,6 +954,7 @@ The following paths are exempt from rate limiting: v1::dto::PaginatedResponse, v1::dto::PaginatedResponse, v1::dto::PaginatedResponse, + v1::dto::PaginatedResponse, // Metrics DTOs v1::dto::MetricsDto, diff --git a/crates/codex-api/src/routes/v1/dto/common.rs b/crates/codex-api/src/routes/v1/dto/common.rs index cf5a2d316..7ac006101 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) // ============================================================================= @@ -678,68 +624,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 c87e44249..19cfccc44 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/handlers/books.rs b/crates/codex-api/src/routes/v1/handlers/books.rs index 1cec458b3..c0893050e 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}, @@ -165,6 +165,41 @@ pub struct BookListQuery { 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)] + pub full: bool, +} + /// Query parameters for getting a single book #[derive(Debug, Deserialize, utoipa::IntoParams)] #[serde(rename_all = "camelCase")] @@ -763,7 +798,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( @@ -883,7 +918,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( @@ -1532,10 +1567,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( @@ -1621,7 +1656,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( @@ -1698,10 +1733,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 +1806,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 +1873,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 +1945,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( @@ -1986,10 +2021,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( diff --git a/crates/codex-api/src/routes/v1/handlers/series.rs b/crates/codex-api/src/routes/v1/handlers/series.rs index 25e6d7f39..dc1d12079 100644 --- a/crates/codex-api/src/routes/v1/handlers/series.rs +++ b/crates/codex-api/src/routes/v1/handlers/series.rs @@ -819,7 +819,7 @@ async fn series_to_full_dtos_batched( 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( @@ -982,7 +982,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( @@ -1389,7 +1389,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( @@ -2552,7 +2552,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( diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 732dc6d66..a2d16ef0c 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -2809,7 +2809,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3399,7 +3399,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3490,7 +3490,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3593,7 +3593,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3696,7 +3696,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -7389,9 +7389,9 @@ }, { "name": "page", - "in": "path", + "in": "query", "description": "Page number (1-indexed, minimum 1)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", @@ -7400,14 +7400,32 @@ }, { "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.", + "required": false, + "schema": { + "type": "boolean" + } } ], "responses": { @@ -7416,7 +7434,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -7455,9 +7473,9 @@ }, { "name": "page", - "in": "path", + "in": "query", "description": "Page number (1-indexed, minimum 1)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", @@ -7466,14 +7484,32 @@ }, { "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.", + "required": false, + "schema": { + "type": "boolean" + } } ], "responses": { @@ -7482,7 +7518,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -7521,9 +7557,9 @@ }, { "name": "page", - "in": "path", + "in": "query", "description": "Page number (1-indexed, minimum 1)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", @@ -7532,14 +7568,32 @@ }, { "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.", + "required": false, + "schema": { + "type": "boolean" + } } ], "responses": { @@ -7548,7 +7602,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -7587,9 +7641,9 @@ }, { "name": "page", - "in": "path", + "in": "query", "description": "Page number (1-indexed, minimum 1)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", @@ -7598,14 +7652,32 @@ }, { "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.", + "required": false, + "schema": { + "type": "boolean" + } } ], "responses": { @@ -7614,7 +7686,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -8055,7 +8127,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" } } } @@ -10566,7 +10638,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" } } } @@ -11306,7 +11378,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesExternalIndexDto" } } } @@ -11456,7 +11528,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" } } } @@ -23766,10 +23838,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": { @@ -34261,7 +34330,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/BookDto" + "$ref": "#/components/schemas/FullBookResponse" }, "description": "The data items for this page" }, @@ -35361,6 +35430,106 @@ } } }, + "PaginatedResponse_SeriesExternalIndexDto": { + "type": "object", + "description": "Generic paginated response wrapper with HATEOAS links", + "required": [ + "data", + "page", + "pageSize", + "total", + "totalPages", + "links" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "description": "Slim per-series projection for external discovery tools.\n\nReturned by `GET /api/v1/series/external-index`. It carries just the\nseries UUID, its linked external IDs, and the locally-owned\nvolume/chapter signals, deliberately omitting the heavy metadata,\ngenres, tags, covers, ratings, and links of [`FullSeriesResponse`].", + "required": [ + "id", + "externalIds" + ], + "properties": { + "externalIds": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesExternalIdRefDto" + }, + "description": "External IDs linked to this series (empty if none have been linked yet)" + }, + "id": { + "type": "string", + "format": "uuid", + "description": "Series ID (build the Codex web deep link `/series/{id}` consumer-side)", + "example": "550e8400-e29b-41d4-a716-446655440002" + }, + "localMaxChapter": { + "type": [ + "number", + "null" + ], + "format": "float", + "description": "Highest `book_metadata.chapter` across non-deleted books, or null if\nno book in the series has a parsed chapter.", + "example": 130.5 + }, + "localMaxVolume": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "description": "Highest `book_metadata.volume` across non-deleted books, or null if\nno book in the series has a parsed volume.", + "example": 12 + }, + "volumesOwned": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Count of complete-volume files (volume set, chapter null). A soft,\ndisplay-only signal; not authoritative for \"how far along\".", + "example": 12 + } + } + }, + "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_SharingTagDto": { "type": "object", "description": "Generic paginated response wrapper with HATEOAS links", @@ -41835,10 +42004,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/tests/api/openapi_spec.rs b/tests/api/openapi_spec.rs index 37f7feac7..cd9034bbd 100644 --- a/tests/api/openapi_spec.rs +++ b/tests/api/openapi_spec.rs @@ -213,3 +213,154 @@ 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 + ); + } +} diff --git a/web/openapi.json b/web/openapi.json index 732dc6d66..a2d16ef0c 100644 --- a/web/openapi.json +++ b/web/openapi.json @@ -2809,7 +2809,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3399,7 +3399,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3490,7 +3490,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3593,7 +3593,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -3696,7 +3696,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -7389,9 +7389,9 @@ }, { "name": "page", - "in": "path", + "in": "query", "description": "Page number (1-indexed, minimum 1)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", @@ -7400,14 +7400,32 @@ }, { "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.", + "required": false, + "schema": { + "type": "boolean" + } } ], "responses": { @@ -7416,7 +7434,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -7455,9 +7473,9 @@ }, { "name": "page", - "in": "path", + "in": "query", "description": "Page number (1-indexed, minimum 1)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", @@ -7466,14 +7484,32 @@ }, { "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.", + "required": false, + "schema": { + "type": "boolean" + } } ], "responses": { @@ -7482,7 +7518,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -7521,9 +7557,9 @@ }, { "name": "page", - "in": "path", + "in": "query", "description": "Page number (1-indexed, minimum 1)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", @@ -7532,14 +7568,32 @@ }, { "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.", + "required": false, + "schema": { + "type": "boolean" + } } ], "responses": { @@ -7548,7 +7602,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -7587,9 +7641,9 @@ }, { "name": "page", - "in": "path", + "in": "query", "description": "Page number (1-indexed, minimum 1)", - "required": true, + "required": false, "schema": { "type": "integer", "format": "int64", @@ -7598,14 +7652,32 @@ }, { "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.", + "required": false, + "schema": { + "type": "boolean" + } } ], "responses": { @@ -7614,7 +7686,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_BookDto" } } } @@ -8055,7 +8127,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" } } } @@ -10566,7 +10638,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" } } } @@ -11306,7 +11378,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesExternalIndexDto" } } } @@ -11456,7 +11528,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse" + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" } } } @@ -23766,10 +23838,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": { @@ -34261,7 +34330,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/BookDto" + "$ref": "#/components/schemas/FullBookResponse" }, "description": "The data items for this page" }, @@ -35361,6 +35430,106 @@ } } }, + "PaginatedResponse_SeriesExternalIndexDto": { + "type": "object", + "description": "Generic paginated response wrapper with HATEOAS links", + "required": [ + "data", + "page", + "pageSize", + "total", + "totalPages", + "links" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "description": "Slim per-series projection for external discovery tools.\n\nReturned by `GET /api/v1/series/external-index`. It carries just the\nseries UUID, its linked external IDs, and the locally-owned\nvolume/chapter signals, deliberately omitting the heavy metadata,\ngenres, tags, covers, ratings, and links of [`FullSeriesResponse`].", + "required": [ + "id", + "externalIds" + ], + "properties": { + "externalIds": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesExternalIdRefDto" + }, + "description": "External IDs linked to this series (empty if none have been linked yet)" + }, + "id": { + "type": "string", + "format": "uuid", + "description": "Series ID (build the Codex web deep link `/series/{id}` consumer-side)", + "example": "550e8400-e29b-41d4-a716-446655440002" + }, + "localMaxChapter": { + "type": [ + "number", + "null" + ], + "format": "float", + "description": "Highest `book_metadata.chapter` across non-deleted books, or null if\nno book in the series has a parsed chapter.", + "example": 130.5 + }, + "localMaxVolume": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "description": "Highest `book_metadata.volume` across non-deleted books, or null if\nno book in the series has a parsed volume.", + "example": 12 + }, + "volumesOwned": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Count of complete-volume files (volume set, chapter null). A soft,\ndisplay-only signal; not authoritative for \"how far along\".", + "example": 12 + } + } + }, + "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_SharingTagDto": { "type": "object", "description": "Generic paginated response wrapper with HATEOAS links", @@ -41835,10 +42004,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/types/api.generated.ts b/web/src/types/api.generated.ts index 845f21d98..35052327a 100644 --- a/web/src/types/api.generated.ts +++ b/web/src/types/api.generated.ts @@ -8929,7 +8929,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) */ @@ -14554,7 +14554,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"]; /** @@ -15329,6 +15329,67 @@ export interface components { totalPages: number; }; /** @description Generic paginated response wrapper with HATEOAS links */ + PaginatedResponse_SeriesExternalIndexDto: { + /** @description The data items for this page */ + data: { + /** @description External IDs linked to this series (empty if none have been linked yet) */ + externalIds: components["schemas"]["SeriesExternalIdRefDto"][]; + /** + * Format: uuid + * @description Series ID (build the Codex web deep link `/series/{id}` consumer-side) + * @example 550e8400-e29b-41d4-a716-446655440002 + */ + id: string; + /** + * Format: float + * @description Highest `book_metadata.chapter` across non-deleted books, or null if + * no book in the series has a parsed chapter. + * @example 130.5 + */ + localMaxChapter?: number | null; + /** + * Format: int32 + * @description Highest `book_metadata.volume` across non-deleted books, or null if + * no book in the series has a parsed volume. + * @example 12 + */ + localMaxVolume?: number | null; + /** + * Format: int64 + * @description Count of complete-volume files (volume set, chapter null). A soft, + * display-only signal; not authoritative for "how far along". + * @example 12 + */ + volumesOwned?: number | null; + }[]; + /** @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_SharingTagDto: { /** @description The data items for this page */ data: { @@ -19177,7 +19238,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; }; @@ -23859,7 +23920,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -24292,7 +24353,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -24335,7 +24396,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -24378,7 +24439,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -24421,7 +24482,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -27279,15 +27340,23 @@ 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; + /** + * @description Return full data including metadata and locks. + * Default is false for backward compatibility. + */ + 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 +27368,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -27313,15 +27382,23 @@ 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; + /** + * @description Return full data including metadata and locks. + * Default is false for backward compatibility. + */ + 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 +27410,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -27347,15 +27424,23 @@ 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; + /** + * @description Return full data including metadata and locks. + * Default is false for backward compatibility. + */ + 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 +27452,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -27381,15 +27466,23 @@ 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; + /** + * @description Return full data including metadata and locks. + * Default is false for backward compatibility. + */ + 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 +27494,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_BookDto"]; }; }; /** @description Forbidden */ @@ -27686,7 +27779,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_SeriesDto"]; }; }; /** @description Forbidden */ @@ -29514,7 +29607,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_SeriesDto"]; }; }; /** @description Forbidden */ @@ -30127,7 +30220,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_SeriesExternalIndexDto"]; }; }; /** @description Forbidden */ @@ -30202,7 +30295,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PaginatedResponse"]; + "application/json": components["schemas"]["PaginatedResponse_SeriesDto"]; }; }; /** @description Forbidden */ From 2ac0f696d2ebd5cddaf830f6895e2812254b4229 Mon Sep 17 00:00:00 2001 From: Sylvain Cau Date: Thu, 20 Aug 2026 21:18:47 -0700 Subject: [PATCH 2/9] fix(api): declare the media types the binary endpoints actually send Six operations annotated a single concrete content type and then set a different Content-Type at runtime. The document was internally consistent, generated without warning, and described a response the server never writes. A strict generated client validates the response content type before decoding and throws on a mismatch, so these were runtime failures on responses the server considers successful: get_book_file declared application/octet-stream, sends zip, rar, epub or pdf chosen from books.format get_page_image declared image/jpeg, sends whatever the archive holds opds_book_page_image same, it delegates to get_page_image get_book_thumbnail declared image/jpeg, sends image/svg+xml for any get_series_thumbnail thumbnail whose generation task has not finished download_export declared application/octet-stream, sends text/csv, text/markdown or application/json The thumbnail pair is the sharp one: the placeholder fires on timing rather than content, so every cover in a freshly scanned library took a branch the document said could not happen. Enumerate where the set is bounded and the distinction is information the caller wants (get_book_file, download_export), and use a wildcard where it is open-ended and the caller cannot act on it (the three image routes), following the image/* already used by komga_get_page. get_book_file lists application/octet-stream alongside the four concrete types so the annotation covers its catch-all arm rather than only the formats the scanner writes. The server was not changed to match the document. Forcing get_book_file to octet-stream would discard the format hint, and rendering the placeholder as JPEG would replace a small vector with a raster to satisfy an annotation. This invariant cannot be checked from inside the document, since nothing in it is inconsistent, so it is asserted from the other side: a test per operation makes the request and compares the response Content-Type against the set ApiDoc declares for it, covering the placeholder and non-JPEG paths rather than only the happy one. Each case also pins which branch it exercises, so one that stops reaching the placeholder fails instead of passing on nothing. Also adds an orphan-component invariant. A schema no operation can reach transitively is the signature the last four defects shared, so the set is now pinned against an explicit allowlist grouped by why each name is acceptable. It fails both ways: a new orphan needs a decision, and an allowlisted name that becomes reachable has to be removed. Two entries are findings rather than exemptions, recorded rather than fixed: the library jobs routes carry no utoipa::path at all, and several registered DTOs are referenced by no handler. --- .../codex-api/src/routes/opds/handlers/pse.rs | 4 +- .../codex-api/src/routes/v1/handlers/books.rs | 11 +- .../codex-api/src/routes/v1/handlers/pages.rs | 11 +- .../src/routes/v1/handlers/series.rs | 5 +- .../src/routes/v1/handlers/series_exports.rs | 9 +- docs/api/openapi.json | 16 +- tests/api/binary_content_types.rs | 440 ++++++++++++++++++ tests/api/mod.rs | 1 + tests/api/openapi_spec.rs | 216 +++++++++ web/openapi.json | 16 +- web/src/types/api.generated.ts | 16 +- 11 files changed, 724 insertions(+), 21 deletions(-) create mode 100644 tests/api/binary_content_types.rs diff --git a/crates/codex-api/src/routes/opds/handlers/pse.rs b/crates/codex-api/src/routes/opds/handlers/pse.rs index b6a6f2ad5..c63dfff7a 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/handlers/books.rs b/crates/codex-api/src/routes/v1/handlers/books.rs index c0893050e..d11e05109 100644 --- a/crates/codex-api/src/routes/v1/handlers/books.rs +++ b/crates/codex-api/src/routes/v1/handlers/books.rs @@ -2204,7 +2204,16 @@ pub async fn list_library_recently_read_books( ("book_id" = Uuid, Path, description = "Book ID") ), 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 = 404, description = "Book not found"), (status = 403, description = "Forbidden"), ), diff --git a/crates/codex-api/src/routes/v1/handlers/pages.rs b/crates/codex-api/src/routes/v1/handlers/pages.rs index 55e851a80..7b105e9a5 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 dc1d12079..8d7e1ed0c 100644 --- a/crates/codex-api/src/routes/v1/handlers/series.rs +++ b/crates/codex-api/src/routes/v1/handlers/series.rs @@ -2030,7 +2030,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"), 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 8906da3a5..3e81672f1 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/docs/api/openapi.json b/docs/api/openapi.json index a2d16ef0c..0e3393486 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -4757,6 +4757,10 @@ "200": { "description": "Book file", "content": { + "application/zip": {}, + "application/x-rar-compressed": {}, + "application/epub+zip": {}, + "application/pdf": {}, "application/octet-stream": {} } }, @@ -5091,7 +5095,7 @@ "200": { "description": "Page image", "content": { - "image/jpeg": {} + "image/*": {} } }, "304": { @@ -5592,7 +5596,7 @@ "200": { "description": "Thumbnail image", "content": { - "image/jpeg": {} + "image/*": {} } }, "304": { @@ -15251,7 +15255,7 @@ "200": { "description": "Thumbnail image", "content": { - "image/jpeg": {} + "image/*": {} } }, "304": { @@ -16529,7 +16533,9 @@ "200": { "description": "Export file", "content": { - "application/octet-stream": {} + "text/csv": {}, + "text/markdown": {}, + "application/json": {} } }, "404": { @@ -18371,7 +18377,7 @@ "200": { "description": "Page image (also records reading progress)", "content": { - "image/jpeg": {} + "image/*": {} } }, "403": { diff --git a/tests/api/binary_content_types.rs b/tests/api/binary_content_types.rs new file mode 100644 index 000000000..4a5bd9af5 --- /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/mod.rs b/tests/api/mod.rs index 5716de1d0..c6fe766cf 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -9,6 +9,7 @@ mod alternate_titles; mod analyze; mod api_keys; mod auth; +mod binary_content_types; mod books; mod bulk_metadata; mod bulk_operations; diff --git a/tests/api/openapi_spec.rs b/tests/api/openapi_spec.rs index cd9034bbd..ebf2e9cb4 100644 --- a/tests/api/openapi_spec.rs +++ b/tests/api/openapi_spec.rs @@ -364,3 +364,219 @@ fn filter_conditions_reference_their_grammar() { ); } } + +/// 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 route set is undocumented ------------------------- + // `list_jobs` and its siblings in `handlers/library_jobs.rs` carry no + // `#[utoipa::path]` at all, so every DTO the routes use is unreachable. + // This is a genuine finding, recorded here rather than fixed: the routes + // exist and work, and no client in play uses them. + "CreateLibraryJobRequest", + "DryRunFieldChange", + "DryRunRequest", + "DryRunResponse", + "DryRunSeriesDelta", + "DryRunSkippedFieldDto", + "FieldGroupDto", + "LibraryJobConfigDto", + "LibraryJobDto", + "ListLibraryJobsResponse", + "MetadataRefreshJobConfigDto", + "PatchLibraryJobRequest", + "RefreshScope", + "RunNowResponse", + // -- The `full=true` alternate response shape --------------------------- + // `list_library_books` and friends answer with these when `full=true`, and + // the document describes only the paginated shape. Deferred deliberately: + // it needs an API decision about whether one operation may return two + // shapes, not an annotation fix. + "BookFullMetadata", + "FullBookResponse", + "FullSeriesResponse", + "SeriesFullMetadata", + // -- 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", + // -- Inlined by its wrapper rather than referenced ---------------------- + // `PaginatedResponse_SeriesExternalIndexDto` expands this DTO inline in its + // `data` array instead of emitting a `$ref`, unlike the other paginated + // wrappers. The document is correct for a client, just duplicated, so this + // is unreachable without being undocumented. + "SeriesExternalIndexDto", + // -- 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 + ); +} diff --git a/web/openapi.json b/web/openapi.json index a2d16ef0c..0e3393486 100644 --- a/web/openapi.json +++ b/web/openapi.json @@ -4757,6 +4757,10 @@ "200": { "description": "Book file", "content": { + "application/zip": {}, + "application/x-rar-compressed": {}, + "application/epub+zip": {}, + "application/pdf": {}, "application/octet-stream": {} } }, @@ -5091,7 +5095,7 @@ "200": { "description": "Page image", "content": { - "image/jpeg": {} + "image/*": {} } }, "304": { @@ -5592,7 +5596,7 @@ "200": { "description": "Thumbnail image", "content": { - "image/jpeg": {} + "image/*": {} } }, "304": { @@ -15251,7 +15255,7 @@ "200": { "description": "Thumbnail image", "content": { - "image/jpeg": {} + "image/*": {} } }, "304": { @@ -16529,7 +16533,9 @@ "200": { "description": "Export file", "content": { - "application/octet-stream": {} + "text/csv": {}, + "text/markdown": {}, + "application/json": {} } }, "404": { @@ -18371,7 +18377,7 @@ "200": { "description": "Page image (also records reading progress)", "content": { - "image/jpeg": {} + "image/*": {} } }, "403": { diff --git a/web/src/types/api.generated.ts b/web/src/types/api.generated.ts index 35052327a..b31a430c8 100644 --- a/web/src/types/api.generated.ts +++ b/web/src/types/api.generated.ts @@ -25277,6 +25277,10 @@ 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; }; }; @@ -25516,7 +25520,7 @@ export interface operations { [name: string]: unknown; }; content: { - "image/jpeg": unknown; + "image/*": unknown; }; }; /** @description Not modified (client cache is valid) */ @@ -25953,7 +25957,7 @@ export interface operations { [name: string]: unknown; }; content: { - "image/jpeg": unknown; + "image/*": unknown; }; }; /** @description Not modified (client cache is valid) */ @@ -33114,7 +33118,7 @@ export interface operations { [name: string]: unknown; }; content: { - "image/jpeg": unknown; + "image/*": unknown; }; }; /** @description Not modified (client cache is valid) */ @@ -34112,7 +34116,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 */ @@ -35664,7 +35670,7 @@ export interface operations { [name: string]: unknown; }; content: { - "image/jpeg": unknown; + "image/*": unknown; }; }; /** @description Forbidden */ From c0553981750389f899bdfd85a70f9fc938c2cd15 Mon Sep 17 00:00:00 2001 From: Sylvain Cau Date: Thu, 20 Aug 2026 22:40:03 -0700 Subject: [PATCH 3/9] feat(api): make the book file endpoint range-capable and conditional An interrupted download of a 200 MB volume had to start again from zero, and the only way to read one page without fetching the whole archive was GET /books/{id}/pages/{n}, which reopens and re-extracts on every request. So the endpoint that costs least to serve was the one clients could not use efficiently, and the one that costs most was the only one offering random access. Range support inverts that. GET /books/{book_id}/file now advertises Accept-Ranges: bytes and a strong ETag on every response, and answers 206 to bytes=a-b, bytes=a- and bytes=-n, 416 to a range that names no byte, and 304 to a current If-None-Match. A request without a Range is byte-for-byte what it was. The suffix form is the one that is easy to skip and should not be: bytes=-n is how a client reads a ZIP central directory, and a CBZ is a ZIP. Handling only bytes=a-b would satisfy resume and none of the partial-read case. The ETag is books.file_hash, 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 do none of those and would invalidate every client's cache on any rescan that touches timestamps. If-Range is honoured against it: a stale validator yields the full 200 rather than a 206, because splicing fresh bytes into a partially downloaded old file produces something that is neither version. A multi-range request would need a multipart/byteranges body no client here asks for, and RFC 9110 permits ignoring a range the server declines, so it gets the whole file. A malformed Range is ignored rather than rejected, for the same reason. Authorisation, the permission check and the content filter all still run before the file is opened, so a 206 is never a way around a check a 200 has to pass. Streaming uses 64 KiB chunks rather than ReaderStream's 4 KiB default. That default was found by the measurement this work called for: it turns a 700 KiB page into ~170 chunks and a 40 MiB volume into ~10,000, and it was the whole of a 2-3x gap against the page endpoint on identical bytes. On a 41 MB 60-page CBZ over a keep-alive connection, one page by range went from 21.1 ms to 4.7 ms and reading all 60 went from 1941 ms to 295 ms, against 10.2 ms and 552 ms for the page endpoint. The whole-file download runs at ~290 MB/s. Folds in RFC 6266 filename encoding, which this route lacked. The encoder is now shared with the Komga download rather than duplicated, and it emits an ASCII-transliterated fallback for the quoted parameter: a header value may only hold visible ASCII, so the previous code put raw UTF-8 on the wire and produced exactly the latin-1 mangling that filename* exists to prevent. Both routes were affected. Verified against a running server as well as in tests. A 42 MB download interrupted at 16,830,464 bytes resumed with 206 and transferred 25,175,438 more, exactly the remainder, and the reassembled file matched the source SHA-256. Reading one page out of 60 by way of the central directory transferred 765,566 bytes, 1.82% of the archive. Range, If-Range and If-None-Match are declared as header parameters and 206, 304 and 416 as responses, so a generated client can construct a resumable download without dropping to a raw request. --- crates/codex-api/src/lib.rs | 1 + crates/codex-api/src/ranged_file.rs | 523 ++++++++++++++++++ .../src/routes/komga/handlers/books.rs | 62 +-- .../codex-api/src/routes/v1/handlers/books.rs | 67 ++- docs/api/openapi.json | 54 +- tests/api/book_file_ranges.rs | 496 +++++++++++++++++ tests/api/mod.rs | 1 + web/openapi.json | 54 +- web/src/types/api.generated.ts | 42 +- 9 files changed, 1214 insertions(+), 86 deletions(-) create mode 100644 crates/codex-api/src/ranged_file.rs create mode 100644 tests/api/book_file_ranges.rs diff --git a/crates/codex-api/src/lib.rs b/crates/codex-api/src/lib.rs index 7372b551d..e27b25f49 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 000000000..a611d39b5 --- /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 e6a22708f..827512757 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/v1/handlers/books.rs b/crates/codex-api/src/routes/v1/handlers/books.rs index d11e05109..87cbcda6f 100644 --- a/crates/codex-api/src/routes/v1/handlers/books.rs +++ b/crates/codex-api/src/routes/v1/handlers/books.rs @@ -2197,11 +2197,27 @@ 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( // One entry per arm of the `content_type` match below, including the @@ -2214,8 +2230,17 @@ pub async fn list_library_recently_read_books( ("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" = []), @@ -2226,6 +2251,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)?; @@ -2236,7 +2262,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)))?; @@ -2267,26 +2294,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/docs/api/openapi.json b/docs/api/openapi.json index 0e3393486..302a82a67 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -4739,7 +4739,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,6 +4751,42 @@ "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": { @@ -4764,11 +4800,27 @@ "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": [ diff --git a/tests/api/book_file_ranges.rs b/tests/api/book_file_ranges.rs new file mode 100644 index 000000000..7a71ad3e7 --- /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/mod.rs b/tests/api/mod.rs index c6fe766cf..1f75795bb 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -10,6 +10,7 @@ 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/web/openapi.json b/web/openapi.json index 0e3393486..302a82a67 100644 --- a/web/openapi.json +++ b/web/openapi.json @@ -4739,7 +4739,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,6 +4751,42 @@ "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": { @@ -4764,11 +4800,27 @@ "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": [ diff --git a/web/src/types/api.generated.ts b/web/src/types/api.generated.ts index b31a430c8..db68814c0 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; @@ -25262,7 +25268,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; @@ -25284,6 +25297,26 @@ export interface operations { "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: { @@ -25298,6 +25331,13 @@ export interface operations { }; content?: never; }; + /** @description The requested range names no byte of the file */ + 416: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; replace_book_metadata: { From ef44fab5eee4e203d5309a8b73ce752eacf28f75 Mon Sep 17 00:00:00 2001 From: Sylvain Cau Date: Fri, 21 Aug 2026 03:56:44 -0700 Subject: [PATCH 4/9] docs(api): document the library-jobs routes, which had no annotations at all Seven routes and eight operations were absent from the OpenAPI document entirely: the per-library job CRUD, run-now, dry-run, and the field-group catalog its editor is built from. None of the handlers in handlers/library_jobs.rs carried a #[utoipa::path], and the module was never listed in docs.rs paths(). Half the work had been done already, which is what made it hard to notice: all fourteen DTOs were registered in docs.rs schemas(), so they shipped in the document as components that no operation could reach. To anyone reading the component list the API looked described; to anyone generating a client it did not exist. The orphan-component invariant added on this branch is what surfaced it, and it is also what verified the fix. Annotating the paths made those fourteen components reachable, so the check's stale-entry half failed and named every one of them, and the allowlist entry that recorded the gap could be deleted rather than edited. That is the shape this check was meant to have: a finding it records is a finding it later insists you close. The utoipa parameter-location trap does not apply here. Every handler extracts its parameters as Path or Path<(Uuid, Uuid)> and none take a Query, so the derive has an extractor to infer from and no explicit parameter_in is needed. Beyond the mechanical transcription, the annotations record three things that are true of the handlers but not evident from their signatures: patch_job's timezone is tri-state, where absent leaves it, null clears it to the server default, and a value sets it; run_job_now answers 409 rather than queueing twice when a run for the job is already in flight; and dry_run_job's configOverride plans against a config the job does not have yet, which is what lets an editor preview an edit before saving it. 353 paths to 358. --- crates/codex-api/src/docs.rs | 11 + .../src/routes/v1/handlers/library_jobs.rs | 178 +++++++ docs/api/openapi.json | 449 +++++++++++++++++ tests/api/openapi_spec.rs | 23 +- web/openapi.json | 449 +++++++++++++++++ web/src/types/api.generated.ts | 454 ++++++++++++++++++ 6 files changed, 1545 insertions(+), 19 deletions(-) diff --git a/crates/codex-api/src/docs.rs b/crates/codex-api/src/docs.rs index 8fc3b0f78..d4c53620a 100644 --- a/crates/codex-api/src/docs.rs +++ b/crates/codex-api/src/docs.rs @@ -487,6 +487,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, 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 3241f0e70..294fc64ab 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/docs/api/openapi.json b/docs/api/openapi.json index 302a82a67..fd66c7c51 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -7877,6 +7877,419 @@ ] } }, + "/api/v1/libraries/{library_id}/jobs": { + "get": { + "tags": [ + "Library Jobs" + ], + "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", + "in": "path", + "description": "Library ID", + "required": true, + "schema": { + "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": [ + { + "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": "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 job", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LibraryJobDto" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Job not found, or it belongs to another library" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + }, + "delete": { + "tags": [ + "Library Jobs" + ], + "summary": "Delete a scheduled job", + "operationId": "delete_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" + } + } + ], + "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": "The updated job", + "content": { + "application/json": { + "schema": { + "$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": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/libraries/{library_id}/jobs/{job_id}/dry-run": { + "post": { + "tags": [ + "Library Jobs" + ], + "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", + "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/DryRunRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "The planned changes", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DryRunResponse" + } + } + } + }, + "400": { + "description": "Override config is of the wrong type, or is invalid" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "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": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, "/api/v1/libraries/{library_id}/purge-deleted": { "delete": { "tags": [ @@ -8534,6 +8947,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": [ diff --git a/tests/api/openapi_spec.rs b/tests/api/openapi_spec.rs index ebf2e9cb4..c532c2d79 100644 --- a/tests/api/openapi_spec.rs +++ b/tests/api/openapi_spec.rs @@ -409,25 +409,10 @@ const ACCEPTED_UNREFERENCED_COMPONENTS: &[&str] = &[ "TaskProgress", "TaskProgressEvent", "TaskStatus", - // -- The library-jobs route set is undocumented ------------------------- - // `list_jobs` and its siblings in `handlers/library_jobs.rs` carry no - // `#[utoipa::path]` at all, so every DTO the routes use is unreachable. - // This is a genuine finding, recorded here rather than fixed: the routes - // exist and work, and no client in play uses them. - "CreateLibraryJobRequest", - "DryRunFieldChange", - "DryRunRequest", - "DryRunResponse", - "DryRunSeriesDelta", - "DryRunSkippedFieldDto", - "FieldGroupDto", - "LibraryJobConfigDto", - "LibraryJobDto", - "ListLibraryJobsResponse", - "MetadataRefreshJobConfigDto", - "PatchLibraryJobRequest", - "RefreshScope", - "RunNowResponse", + // 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` alternate response shape --------------------------- // `list_library_books` and friends answer with these when `full=true`, and // the document describes only the paginated shape. Deferred deliberately: diff --git a/web/openapi.json b/web/openapi.json index 302a82a67..fd66c7c51 100644 --- a/web/openapi.json +++ b/web/openapi.json @@ -7877,6 +7877,419 @@ ] } }, + "/api/v1/libraries/{library_id}/jobs": { + "get": { + "tags": [ + "Library Jobs" + ], + "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", + "in": "path", + "description": "Library ID", + "required": true, + "schema": { + "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": [ + { + "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": "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 job", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LibraryJobDto" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Job not found, or it belongs to another library" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + }, + "delete": { + "tags": [ + "Library Jobs" + ], + "summary": "Delete a scheduled job", + "operationId": "delete_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" + } + } + ], + "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": "The updated job", + "content": { + "application/json": { + "schema": { + "$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": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/libraries/{library_id}/jobs/{job_id}/dry-run": { + "post": { + "tags": [ + "Library Jobs" + ], + "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", + "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/DryRunRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "The planned changes", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DryRunResponse" + } + } + } + }, + "400": { + "description": "Override config is of the wrong type, or is invalid" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "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": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, "/api/v1/libraries/{library_id}/purge-deleted": { "delete": { "tags": [ @@ -8534,6 +8947,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": [ diff --git a/web/src/types/api.generated.ts b/web/src/types/api.generated.ts index db68814c0..d47637fb3 100644 --- a/web/src/types/api.generated.ts +++ b/web/src/types/api.generated.ts @@ -2504,6 +2504,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; @@ -2710,6 +2808,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; @@ -27624,6 +27744,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; @@ -28024,6 +28451,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; From c8b7618151090c2c46bd76ced0dabcceed486f5f Mon Sep 17 00:00:00 2001 From: Sylvain Cau Date: Fri, 21 Aug 2026 11:00:58 -0700 Subject: [PATCH 5/9] build(docs): upgrade Docusaurus and the OpenAPI plugin so the site builds again The docs site has failed to build since the filter grammar stopped being an untyped object. Making SeriesListRequest.condition and BookListRequest.condition `$ref`s exposed the grammar to the docs generator for the first time, and docusaurus-theme-openapi-docs 4.7.1 cannot render it: Can't render static file for pathname "/docs/api/schemas/serieslistrequest" TypeError: schema[key]?.map is not a function at AnyOneOf Two things trip it, and the document is valid OpenAPI on both counts. The plugin dereferences `$ref`s eagerly and replaces the recursion in SeriesCondition with the literal string "circular()", so `schema.oneOf` is a string rather than an array. And the grammar uses `allOf` and `anyOf` as property names, since that is how the Rust enum variants serialize, so a `properties` map reads as a schema carrying an `anyOf` keyword whose value is an object. `AnyOneOf` picks its key with `schema.oneOf ? "oneOf" : "anyOf"` and calls `.map` on the result without checking it is an array. Nothing caught this because the docs build runs on Cloudflare Pages against pull requests, and this branch had no PR until now. Renaming the wire fields was not an option: it addresses the keyword collision but not the recursion, and it would break the filter grammar for both the web app and the iOS client. Upgrading fixes it. 5.x still contains the same unguarded line but no longer feeds it a string, so both schema pages render. The upgrade cascades: the plugin's 5.x peers require @docusaurus/* ^3.10, and 3.10 requires an explicit @docusaurus/faster dependency for the `future.v4` flag this site already sets. docusaurus-plugin-sass is a new peer of the theme. Verified by running the Cloudflare build command, `npm run build`, against a tree with no generated API docs, matching a fresh checkout. Both previously failing pages now render. --- docs/package-lock.json | 4256 +++++++++++++++++++++++++--------------- docs/package.json | 16 +- 2 files changed, 2670 insertions(+), 1602 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 4c5858d13..dd0a5f5fa 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 a4ac47e37..ab8a63c11 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": { From d839da37983248a223fed55f7b9bbdcefc9799be Mon Sep 17 00:00:00 2001 From: Sylvain Cau Date: Fri, 21 Aug 2026 13:16:44 -0700 Subject: [PATCH 6/9] fix(api): reference generic type arguments instead of inlining them utoipa renders a concrete instantiation of a generic by expanding its type argument inline, even when that argument is registered as its own component. Every generic wrapper in the document did it: all ten PaginatedResponse_* components and all four KomgaPage_* ones. The document stayed correct, so nothing flagged it, but any generator that has to name an inline schema invents a fresh type for it. swift-openapi-generator produced PaginatedResponseSeriesDto.DataPayloadPayload where getSeries produced SeriesDto: structurally identical, nominally distinct, and not interchangeable. A client could not pass a row from a list into anything typed on the detail DTO without a hand-written conversion, once per paginated endpoint. GenericArgumentReferencer walks each Base_Argument component and replaces any subschema 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 collapse a copy of a schema the document already contains under that name, so nothing about the described wire format changes. It joins the existing Modify pipeline alongside NullableRefFlattener, so it needs no handler or DTO changes and covers every generic rather than only pagination. Verified against the generator that found the problem: a probe taking a series from a paginated list and one from getSeries as the same type now compiles. SeriesExternalIndexDto was an orphan only because its wrapper inlined it, so it becomes reachable and leaves the accepted-orphan list. The invariant added here fails if any generic wrapper starts inlining again. --- crates/codex-api/src/docs.rs | 126 ++++++++++++++++++++++++++++++++++- tests/api/openapi_spec.rs | 73 +++++++++++++++----- 2 files changed, 183 insertions(+), 16 deletions(-) diff --git a/crates/codex-api/src/docs.rs b/crates/codex-api/src/docs.rs index d4c53620a..6aa64d40e 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, @@ -963,6 +966,7 @@ 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, @@ -1313,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; @@ -1367,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/tests/api/openapi_spec.rs b/tests/api/openapi_spec.rs index c532c2d79..3ca8c14b2 100644 --- a/tests/api/openapi_spec.rs +++ b/tests/api/openapi_spec.rs @@ -413,15 +413,10 @@ const ACCEPTED_UNREFERENCED_COMPONENTS: &[&str] = &[ // 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` alternate response shape --------------------------- - // `list_library_books` and friends answer with these when `full=true`, and - // the document describes only the paginated shape. Deferred deliberately: - // it needs an API decision about whether one operation may return two - // shapes, not an annotation fix. - "BookFullMetadata", - "FullBookResponse", - "FullSeriesResponse", - "SeriesFullMetadata", + // 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 @@ -448,12 +443,10 @@ const ACCEPTED_UNREFERENCED_COMPONENTS: &[&str] = &[ "ExternalRatingContextDto", "MetadataContextDto", "SeriesContextDto", - // -- Inlined by its wrapper rather than referenced ---------------------- - // `PaginatedResponse_SeriesExternalIndexDto` expands this DTO inline in its - // `data` array instead of emitting a `$ref`, unlike the other paginated - // wrappers. The document is correct for a client, just duplicated, so this - // is unreachable without being undocumented. - "SeriesExternalIndexDto", + // `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 @@ -565,3 +558,53 @@ fn unreferenced_components_are_all_accounted_for() { 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 + ); +} From 243f7ddd4f64693f7218640f3fd44d09c526fef9 Mon Sep 17 00:00:00 2001 From: Sylvain Cau Date: Fri, 21 Aug 2026 13:17:18 -0700 Subject: [PATCH 7/9] feat(api): describe the full response shapes as their own routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `?full=true` switches the response to a different schema. OpenAPI cannot express a body whose shape depends on a parameter *value*, so the alternate shape was invisible to every generated client, and FullBookResponse and FullSeriesResponse sat in the document as components no operation referenced. The two shapes are not lean versus rich, which rules out the tidy fixes. BookDto carries chapter, summary and volume that FullBookResponse lacks; SeriesDto carries title, titleSort, publisher, summary and year that FullSeriesResponse lacks, because the full shape moves them inside `metadata`. They are different projections that partially overlap, so they cannot be merged into one schema with optional fields, and a oneOf union would force callers to switch between two arms that put the same field at different paths. Adds three routes, each with one concrete response schema: GET /api/v1/books/{book_id}/full -> FullBookResponse GET /api/v1/series/{series_id}/full -> FullSeriesResponse GET /api/v1/series/full -> PaginatedResponse The listing is there because the parameter has a live external consumer, not on principle: shisho pages GET /api/v1/series?full=true across a whole library for its candidate pools, and calls the series detail form too. Both now have a describable route to move to. Only these three were added — the other nineteen operations accept `full` and nobody sends it, and splitting all twenty-two would oblige every future list endpoint to be added twice. `/series/full` shares a path slot with `/series/{series_id}`, which is the same shape as the thirteen static siblings already there, `/series/external-index` among them. A test pins that the literal route wins. The listing shares one implementation with the plain one rather than duplicating it, and deliberately does not echo `full=true` into its pagination links: the route already means full. `full` is now marked deprecated on all twenty-two operations. Nothing breaks; removal is planned for 3.0, once shisho has moved. The web app is migrated off it. `booksApi.getFull` and `seriesApi.getFull` replace the conditional-return-type `getDetail` / `getById` pair, which existed only to model in TypeScript what the document could not express. Each new route is tested against the `?full=true` form it replaces and asserted byte-identical, so removing the parameter later cannot silently change a payload. The book comparison ignores the metadata row's own timestamps, since a book with no metadata gets one created on read and two requests produce two different values. All four `full=true` orphans become reachable and leave the accepted-orphan list. --- crates/codex-api/src/routes/v1/dto/common.rs | 10 + crates/codex-api/src/routes/v1/dto/series.rs | 9 + .../codex-api/src/routes/v1/handlers/books.rs | 89 + .../src/routes/v1/handlers/series.rs | 219 +- .../codex-api/src/routes/v1/routes/books.rs | 2 + .../codex-api/src/routes/v1/routes/series.rs | 4 + docs/api/openapi.json | 1948 +++++------------ tests/api/access_group_visibility.rs | 1 + tests/api/books.rs | 133 ++ tests/api/search.rs | 5 + tests/api/series.rs | 129 ++ web/openapi.json | 1948 +++++------------ web/src/api/books.ts | 24 +- web/src/api/queryKeys.ts | 2 +- web/src/api/series.ts | 23 +- web/src/pages/BookDetail.tsx | 4 +- web/src/pages/SeriesDetail.tsx | 2 +- web/src/types/api.generated.ts | 1225 ++++------- 18 files changed, 2026 insertions(+), 3751 deletions(-) diff --git a/crates/codex-api/src/routes/v1/dto/common.rs b/crates/codex-api/src/routes/v1/dto/common.rs index 7ac006101..88e1a0141 100644 --- a/crates/codex-api/src/routes/v1/dto/common.rs +++ b/crates/codex-api/src/routes/v1/dto/common.rs @@ -78,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, diff --git a/crates/codex-api/src/routes/v1/dto/series.rs b/crates/codex-api/src/routes/v1/dto/series.rs index 15f93192f..b7d61aeff 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 87cbcda6f..a1dc71a71 100644 --- a/crates/codex-api/src/routes/v1/handlers/books.rs +++ b/crates/codex-api/src/routes/v1/handlers/books.rs @@ -162,6 +162,15 @@ 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, } @@ -197,6 +206,15 @@ pub struct LibraryBookListQuery { /// 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, } @@ -208,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, } @@ -807,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, @@ -927,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, @@ -1164,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, @@ -1182,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, @@ -1579,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, @@ -1665,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, @@ -1954,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, diff --git a/crates/codex-api/src/routes/v1/handlers/series.rs b/crates/codex-api/src/routes/v1/handlers/series.rs index 8d7e1ed0c..b45387fcc 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,6 +865,49 @@ 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, @@ -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"); } @@ -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, @@ -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, @@ -2244,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, } @@ -2262,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, @@ -2317,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, } @@ -2339,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, @@ -2396,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, @@ -2451,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, @@ -2507,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, @@ -2564,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, @@ -2655,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, } @@ -2676,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/routes/books.rs b/crates/codex-api/src/routes/v1/routes/books.rs index 842f6afea..4f1478969 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 2ac88f3c3..168fd05c7 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 fd66c7c51..821fe09ee 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" } @@ -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" } @@ -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" } @@ -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" } @@ -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" } @@ -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" } @@ -4833,6 +4839,54 @@ ] } }, + "/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" + }, + "404": { + "description": "Book not found" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, "/api/v1/books/{book_id}/metadata": { "put": { "tags": [ @@ -7477,8 +7531,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" } @@ -7561,8 +7616,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" } @@ -7645,8 +7701,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" } @@ -7729,8 +7786,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" } @@ -8583,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" } @@ -8636,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" } @@ -8716,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" } @@ -8796,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" } @@ -11130,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" } @@ -11870,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" } @@ -11902,103 +11966,65 @@ ] } }, - "/api/v1/series/in-progress": { + "/api/v1/series/full": { "get": { "tags": [ "Series" ], - "summary": "List series with in-progress books (series that have at least one book with reading progress that is not completed)", - "operationId": "list_in_progress_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": "libraryId", + "name": "page", "in": "query", - "description": "Filter by library ID (optional)", + "description": "Page number (1-indexed, default 1)", "required": false, "schema": { - "type": [ - "string", - "null" - ], - "format": "uuid" + "type": "integer", + "format": "int64", + "minimum": 0 } }, { - "name": "full", + "name": "pageSize", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Number of items per page (max 100, default 50)", "required": false, "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "List of in-progress series (returns Vec when full=true)", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SeriesDto" - } - } - } + "type": "integer", + "format": "int64", + "minimum": 0 } }, - "403": { - "description": "Forbidden" - } - }, - "security": [ - { - "jwt_bearer": [] - }, { - "api_key": [] - } - ] - } - }, - "/api/v1/series/list": { - "post": { - "tags": [ - "Series" - ], - "summary": "List series with advanced filtering", - "description": "Supports complex filter conditions including nested AllOf/AnyOf logic,\ngenre/tag filtering with include/exclude, and more.\n\nPagination parameters (page, pageSize, sort) are passed as query parameters.\nFilter conditions are passed in the request body.", - "operationId": "list_series_filtered", - "parameters": [ - { - "name": "page", + "name": "sort", "in": "query", - "description": "Page number (1-indexed, minimum 1)", + "description": "Sort parameter (format: \"field,direction\" e.g. \"name,asc\")", "required": false, "schema": { - "type": "integer", - "format": "int64", - "default": 1, - "minimum": 1 + "type": [ + "string", + "null" + ] } }, { - "name": "pageSize", + "name": "genres", "in": "query", - "description": "Number of items per page (max 500, default 50)", + "description": "Filter by genres (comma-separated, AND logic - series must have ALL specified genres)", "required": false, "schema": { - "type": "integer", - "format": "int64", - "default": 50, - "maximum": 500, - "minimum": 1 + "type": [ + "string", + "null" + ] } }, { - "name": "sort", + "name": "tags", "in": "query", - "description": "Sort field and direction (e.g., \"name,asc\" or \"createdAt,desc\")", + "description": "Filter by tags (comma-separated, AND logic - series must have ALL specified tags)", "required": false, "schema": { "type": [ @@ -12008,32 +12034,26 @@ } }, { - "name": "full", + "name": "libraryId", "in": "query", - "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.", + "description": "Filter by library ID", "required": false, "schema": { - "type": "boolean" + "type": [ + "string", + "null" + ], + "format": "uuid" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SeriesListRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Paginated list of filtered series (returns FullSeriesListResponse when full=true)", + "description": "Paginated series with their related data", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" + "$ref": "#/components/schemas/PaginatedResponse_FullSeriesResponse" } } } @@ -12052,123 +12072,14 @@ ] } }, - "/api/v1/series/list/alphabetical-groups": { - "post": { - "tags": [ - "Series" - ], - "summary": "Get alphabetical groups for series", - "description": "Returns a list of alphabetical groups with counts, showing how many series\nstart with each letter/character. This is useful for building A-Z navigation.\nThe same filters as list_series_filtered can be applied.", - "operationId": "list_series_alphabetical_groups", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SeriesListRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "List of alphabetical groups with counts", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AlphabeticalGroupDto" - } - } - } - } - }, - "403": { - "description": "Forbidden" - } - }, - "security": [ - { - "jwt_bearer": [] - }, - { - "api_key": [] - } - ] - } - }, - "/api/v1/series/metadata/auto-match/task/bulk": { - "post": { - "tags": [ - "Plugin Actions" - ], - "summary": "Enqueue plugin auto-match tasks for multiple series (bulk operation)", - "description": "Creates background tasks to auto-match metadata for multiple series using the specified plugin.\nEach series gets its own task that runs asynchronously in a worker process.", - "operationId": "enqueue_bulk_auto_match_tasks", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EnqueueBulkAutoMatchRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Tasks enqueued", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EnqueueAutoMatchResponse" - } - } - } - }, - "400": { - "description": "Invalid request" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "No permission to edit series" - }, - "404": { - "description": "Plugin not found" - } - }, - "security": [ - { - "jwt_bearer": [] - }, - { - "api_key": [] - } - ] - } - }, - "/api/v1/series/recently-added": { + "/api/v1/series/in-progress": { "get": { "tags": [ "Series" ], - "summary": "List recently added series", - "operationId": "list_recently_added_series", + "summary": "List series with in-progress books (series that have at least one book with reading progress that is not completed)", + "operationId": "list_in_progress_series", "parameters": [ - { - "name": "limit", - "in": "query", - "description": "Maximum number of series to return (default: 50)", - "required": false, - "schema": { - "type": "integer", - "format": "int64", - "minimum": 0 - } - }, { "name": "libraryId", "in": "query", @@ -12185,8 +12096,270 @@ { "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" + } + } + ], + "responses": { + "200": { + "description": "List of in-progress series (returns Vec when full=true)", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesDto" + } + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/series/list": { + "post": { + "tags": [ + "Series" + ], + "summary": "List series with advanced filtering", + "description": "Supports complex filter conditions including nested AllOf/AnyOf logic,\ngenre/tag filtering with include/exclude, and more.\n\nPagination parameters (page, pageSize, sort) are passed as query parameters.\nFilter conditions are passed in the request body.", + "operationId": "list_series_filtered", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "Page number (1-indexed, minimum 1)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "default": 1, + "minimum": 1 + } + }, + { + "name": "pageSize", + "in": "query", + "description": "Number of items per page (max 500, default 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "default": 50, + "maximum": 500, + "minimum": 1 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sort field and direction (e.g., \"name,asc\" or \"createdAt,desc\")", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "full", + "in": "query", + "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" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SeriesListRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Paginated list of filtered series (returns FullSeriesListResponse when full=true)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/series/list/alphabetical-groups": { + "post": { + "tags": [ + "Series" + ], + "summary": "Get alphabetical groups for series", + "description": "Returns a list of alphabetical groups with counts, showing how many series\nstart with each letter/character. This is useful for building A-Z navigation.\nThe same filters as list_series_filtered can be applied.", + "operationId": "list_series_alphabetical_groups", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SeriesListRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "List of alphabetical groups with counts", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlphabeticalGroupDto" + } + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/series/metadata/auto-match/task/bulk": { + "post": { + "tags": [ + "Plugin Actions" + ], + "summary": "Enqueue plugin auto-match tasks for multiple series (bulk operation)", + "description": "Creates background tasks to auto-match metadata for multiple series using the specified plugin.\nEach series gets its own task that runs asynchronously in a worker process.", + "operationId": "enqueue_bulk_auto_match_tasks", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnqueueBulkAutoMatchRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Tasks enqueued", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnqueueAutoMatchResponse" + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "No permission to edit series" + }, + "404": { + "description": "Plugin not found" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/series/recently-added": { + "get": { + "tags": [ + "Series" + ], + "summary": "List recently added series", + "operationId": "list_recently_added_series", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Maximum number of series to return (default: 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "libraryId", + "in": "query", + "description": "Filter by library ID (optional)", + "required": false, + "schema": { + "type": [ + "string", + "null" + ], + "format": "uuid" + } + }, + { + "name": "full", + "in": "query", + "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" } @@ -12255,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" } @@ -12759,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" } @@ -13356,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" } @@ -14295,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": [ @@ -31717,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" }, @@ -31881,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" }, @@ -31995,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" }, @@ -32114,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" }, @@ -34890,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" }, @@ -35016,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" }, @@ -35249,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" }, @@ -35337,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" }, @@ -35532,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" }, @@ -35714,192 +35166,7 @@ "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" }, @@ -35952,54 +35219,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Slim per-series projection for external discovery tools.\n\nReturned by `GET /api/v1/series/external-index`. It carries just the\nseries UUID, its linked external IDs, and the locally-owned\nvolume/chapter signals, deliberately omitting the heavy metadata,\ngenres, tags, covers, ratings, and links of [`FullSeriesResponse`].", - "required": [ - "id", - "externalIds" - ], - "properties": { - "externalIds": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SeriesExternalIdRefDto" - }, - "description": "External IDs linked to this series (empty if none have been linked yet)" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Series ID (build the Codex web deep link `/series/{id}` consumer-side)", - "example": "550e8400-e29b-41d4-a716-446655440002" - }, - "localMaxChapter": { - "type": [ - "number", - "null" - ], - "format": "float", - "description": "Highest `book_metadata.chapter` across non-deleted books, or null if\nno book in the series has a parsed chapter.", - "example": 130.5 - }, - "localMaxVolume": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "description": "Highest `book_metadata.volume` across non-deleted books, or null if\nno book in the series has a parsed volume.", - "example": 12 - }, - "volumesOwned": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Count of complete-volume files (volume set, chapter null). A soft,\ndisplay-only signal; not authoritative for \"how far along\".", - "example": 12 - } - } + "$ref": "#/components/schemas/SeriesExternalIndexDto" }, "description": "The data items for this page" }, @@ -36052,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" }, @@ -36161,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" }, @@ -36249,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" }, @@ -41198,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": [ diff --git a/tests/api/access_group_visibility.rs b/tests/api/access_group_visibility.rs index 053984aa1..0aebcba58 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/books.rs b/tests/api/books.rs index 1e8180c66..bceb3d4df 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/search.rs b/tests/api/search.rs index 300f320a8..b6586dd84 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 22500f138..aab6d377d 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 fd66c7c51..821fe09ee 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" } @@ -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" } @@ -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" } @@ -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" } @@ -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" } @@ -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" } @@ -4833,6 +4839,54 @@ ] } }, + "/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" + }, + "404": { + "description": "Book not found" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, "/api/v1/books/{book_id}/metadata": { "put": { "tags": [ @@ -7477,8 +7531,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" } @@ -7561,8 +7616,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" } @@ -7645,8 +7701,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" } @@ -7729,8 +7786,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" } @@ -8583,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" } @@ -8636,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" } @@ -8716,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" } @@ -8796,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" } @@ -11130,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" } @@ -11870,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" } @@ -11902,103 +11966,65 @@ ] } }, - "/api/v1/series/in-progress": { + "/api/v1/series/full": { "get": { "tags": [ "Series" ], - "summary": "List series with in-progress books (series that have at least one book with reading progress that is not completed)", - "operationId": "list_in_progress_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": "libraryId", + "name": "page", "in": "query", - "description": "Filter by library ID (optional)", + "description": "Page number (1-indexed, default 1)", "required": false, "schema": { - "type": [ - "string", - "null" - ], - "format": "uuid" + "type": "integer", + "format": "int64", + "minimum": 0 } }, { - "name": "full", + "name": "pageSize", "in": "query", - "description": "Return full series data including metadata, locks, genres, tags, etc.", + "description": "Number of items per page (max 100, default 50)", "required": false, "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "List of in-progress series (returns Vec when full=true)", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SeriesDto" - } - } - } + "type": "integer", + "format": "int64", + "minimum": 0 } }, - "403": { - "description": "Forbidden" - } - }, - "security": [ - { - "jwt_bearer": [] - }, { - "api_key": [] - } - ] - } - }, - "/api/v1/series/list": { - "post": { - "tags": [ - "Series" - ], - "summary": "List series with advanced filtering", - "description": "Supports complex filter conditions including nested AllOf/AnyOf logic,\ngenre/tag filtering with include/exclude, and more.\n\nPagination parameters (page, pageSize, sort) are passed as query parameters.\nFilter conditions are passed in the request body.", - "operationId": "list_series_filtered", - "parameters": [ - { - "name": "page", + "name": "sort", "in": "query", - "description": "Page number (1-indexed, minimum 1)", + "description": "Sort parameter (format: \"field,direction\" e.g. \"name,asc\")", "required": false, "schema": { - "type": "integer", - "format": "int64", - "default": 1, - "minimum": 1 + "type": [ + "string", + "null" + ] } }, { - "name": "pageSize", + "name": "genres", "in": "query", - "description": "Number of items per page (max 500, default 50)", + "description": "Filter by genres (comma-separated, AND logic - series must have ALL specified genres)", "required": false, "schema": { - "type": "integer", - "format": "int64", - "default": 50, - "maximum": 500, - "minimum": 1 + "type": [ + "string", + "null" + ] } }, { - "name": "sort", + "name": "tags", "in": "query", - "description": "Sort field and direction (e.g., \"name,asc\" or \"createdAt,desc\")", + "description": "Filter by tags (comma-separated, AND logic - series must have ALL specified tags)", "required": false, "schema": { "type": [ @@ -12008,32 +12034,26 @@ } }, { - "name": "full", + "name": "libraryId", "in": "query", - "description": "Return full data including metadata, locks, and related entities.\nDefault is false for backward compatibility.", + "description": "Filter by library ID", "required": false, "schema": { - "type": "boolean" + "type": [ + "string", + "null" + ], + "format": "uuid" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SeriesListRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Paginated list of filtered series (returns FullSeriesListResponse when full=true)", + "description": "Paginated series with their related data", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" + "$ref": "#/components/schemas/PaginatedResponse_FullSeriesResponse" } } } @@ -12052,123 +12072,14 @@ ] } }, - "/api/v1/series/list/alphabetical-groups": { - "post": { - "tags": [ - "Series" - ], - "summary": "Get alphabetical groups for series", - "description": "Returns a list of alphabetical groups with counts, showing how many series\nstart with each letter/character. This is useful for building A-Z navigation.\nThe same filters as list_series_filtered can be applied.", - "operationId": "list_series_alphabetical_groups", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SeriesListRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "List of alphabetical groups with counts", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AlphabeticalGroupDto" - } - } - } - } - }, - "403": { - "description": "Forbidden" - } - }, - "security": [ - { - "jwt_bearer": [] - }, - { - "api_key": [] - } - ] - } - }, - "/api/v1/series/metadata/auto-match/task/bulk": { - "post": { - "tags": [ - "Plugin Actions" - ], - "summary": "Enqueue plugin auto-match tasks for multiple series (bulk operation)", - "description": "Creates background tasks to auto-match metadata for multiple series using the specified plugin.\nEach series gets its own task that runs asynchronously in a worker process.", - "operationId": "enqueue_bulk_auto_match_tasks", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EnqueueBulkAutoMatchRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Tasks enqueued", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EnqueueAutoMatchResponse" - } - } - } - }, - "400": { - "description": "Invalid request" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "No permission to edit series" - }, - "404": { - "description": "Plugin not found" - } - }, - "security": [ - { - "jwt_bearer": [] - }, - { - "api_key": [] - } - ] - } - }, - "/api/v1/series/recently-added": { + "/api/v1/series/in-progress": { "get": { "tags": [ "Series" ], - "summary": "List recently added series", - "operationId": "list_recently_added_series", + "summary": "List series with in-progress books (series that have at least one book with reading progress that is not completed)", + "operationId": "list_in_progress_series", "parameters": [ - { - "name": "limit", - "in": "query", - "description": "Maximum number of series to return (default: 50)", - "required": false, - "schema": { - "type": "integer", - "format": "int64", - "minimum": 0 - } - }, { "name": "libraryId", "in": "query", @@ -12185,8 +12096,270 @@ { "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" + } + } + ], + "responses": { + "200": { + "description": "List of in-progress series (returns Vec when full=true)", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeriesDto" + } + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/series/list": { + "post": { + "tags": [ + "Series" + ], + "summary": "List series with advanced filtering", + "description": "Supports complex filter conditions including nested AllOf/AnyOf logic,\ngenre/tag filtering with include/exclude, and more.\n\nPagination parameters (page, pageSize, sort) are passed as query parameters.\nFilter conditions are passed in the request body.", + "operationId": "list_series_filtered", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "Page number (1-indexed, minimum 1)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "default": 1, + "minimum": 1 + } + }, + { + "name": "pageSize", + "in": "query", + "description": "Number of items per page (max 500, default 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "default": 50, + "maximum": 500, + "minimum": 1 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sort field and direction (e.g., \"name,asc\" or \"createdAt,desc\")", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "full", + "in": "query", + "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" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SeriesListRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Paginated list of filtered series (returns FullSeriesListResponse when full=true)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_SeriesDto" + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/series/list/alphabetical-groups": { + "post": { + "tags": [ + "Series" + ], + "summary": "Get alphabetical groups for series", + "description": "Returns a list of alphabetical groups with counts, showing how many series\nstart with each letter/character. This is useful for building A-Z navigation.\nThe same filters as list_series_filtered can be applied.", + "operationId": "list_series_alphabetical_groups", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SeriesListRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "List of alphabetical groups with counts", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlphabeticalGroupDto" + } + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/series/metadata/auto-match/task/bulk": { + "post": { + "tags": [ + "Plugin Actions" + ], + "summary": "Enqueue plugin auto-match tasks for multiple series (bulk operation)", + "description": "Creates background tasks to auto-match metadata for multiple series using the specified plugin.\nEach series gets its own task that runs asynchronously in a worker process.", + "operationId": "enqueue_bulk_auto_match_tasks", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnqueueBulkAutoMatchRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Tasks enqueued", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnqueueAutoMatchResponse" + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "No permission to edit series" + }, + "404": { + "description": "Plugin not found" + } + }, + "security": [ + { + "jwt_bearer": [] + }, + { + "api_key": [] + } + ] + } + }, + "/api/v1/series/recently-added": { + "get": { + "tags": [ + "Series" + ], + "summary": "List recently added series", + "operationId": "list_recently_added_series", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Maximum number of series to return (default: 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "libraryId", + "in": "query", + "description": "Filter by library ID (optional)", + "required": false, + "schema": { + "type": [ + "string", + "null" + ], + "format": "uuid" + } + }, + { + "name": "full", + "in": "query", + "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" } @@ -12255,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" } @@ -12759,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" } @@ -13356,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" } @@ -14295,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": [ @@ -31717,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" }, @@ -31881,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" }, @@ -31995,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" }, @@ -32114,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" }, @@ -34890,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" }, @@ -35016,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" }, @@ -35249,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" }, @@ -35337,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" }, @@ -35532,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" }, @@ -35714,192 +35166,7 @@ "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" }, @@ -35952,54 +35219,7 @@ "data": { "type": "array", "items": { - "type": "object", - "description": "Slim per-series projection for external discovery tools.\n\nReturned by `GET /api/v1/series/external-index`. It carries just the\nseries UUID, its linked external IDs, and the locally-owned\nvolume/chapter signals, deliberately omitting the heavy metadata,\ngenres, tags, covers, ratings, and links of [`FullSeriesResponse`].", - "required": [ - "id", - "externalIds" - ], - "properties": { - "externalIds": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SeriesExternalIdRefDto" - }, - "description": "External IDs linked to this series (empty if none have been linked yet)" - }, - "id": { - "type": "string", - "format": "uuid", - "description": "Series ID (build the Codex web deep link `/series/{id}` consumer-side)", - "example": "550e8400-e29b-41d4-a716-446655440002" - }, - "localMaxChapter": { - "type": [ - "number", - "null" - ], - "format": "float", - "description": "Highest `book_metadata.chapter` across non-deleted books, or null if\nno book in the series has a parsed chapter.", - "example": 130.5 - }, - "localMaxVolume": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "description": "Highest `book_metadata.volume` across non-deleted books, or null if\nno book in the series has a parsed volume.", - "example": 12 - }, - "volumesOwned": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Count of complete-volume files (volume set, chapter null). A soft,\ndisplay-only signal; not authoritative for \"how far along\".", - "example": 12 - } - } + "$ref": "#/components/schemas/SeriesExternalIndexDto" }, "description": "The data items for this page" }, @@ -36052,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" }, @@ -36161,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" }, @@ -36249,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" }, @@ -41198,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": [ diff --git a/web/src/api/books.ts b/web/src/api/books.ts index 6612fc4de..b4211ff6a 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 b9e5c4e56..4133386e0 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 1ad38ec56..ed8875f9d 100644 --- a/web/src/api/series.ts +++ b/web/src/api/series.ts @@ -66,17 +66,20 @@ export const seriesApi = { 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; }, diff --git a/web/src/pages/BookDetail.tsx b/web/src/pages/BookDetail.tsx index 12e4c4ba4..0d06c28ec 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 fd3eda5b4..d9729c877 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 d47637fb3..fca5bfba5 100644 --- a/web/src/types/api.generated.ts +++ b/web/src/types/api.generated.ts @@ -1587,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; @@ -3972,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; @@ -4716,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; @@ -13025,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 */ @@ -13112,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 */ @@ -13171,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 */ @@ -13232,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 */ @@ -14711,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"]; /** @@ -14796,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"]; /** @@ -14963,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"]; /** @@ -15018,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"]; /** @@ -15150,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"]; /** @@ -15262,171 +14867,7 @@ 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"]; /** @@ -15457,37 +14898,7 @@ export interface components { /** @description Generic paginated response wrapper with HATEOAS links */ PaginatedResponse_SeriesExternalIndexDto: { /** @description The data items for this page */ - data: { - /** @description External IDs linked to this series (empty if none have been linked yet) */ - externalIds: components["schemas"]["SeriesExternalIdRefDto"][]; - /** - * Format: uuid - * @description Series ID (build the Codex web deep link `/series/{id}` consumer-side) - * @example 550e8400-e29b-41d4-a716-446655440002 - */ - id: string; - /** - * Format: float - * @description Highest `book_metadata.chapter` across non-deleted books, or null if - * no book in the series has a parsed chapter. - * @example 130.5 - */ - localMaxChapter?: number | null; - /** - * Format: int32 - * @description Highest `book_metadata.volume` across non-deleted books, or null if - * no book in the series has a parsed volume. - * @example 12 - */ - localMaxVolume?: number | null; - /** - * Format: int64 - * @description Count of complete-volume files (volume set, chapter null). A soft, - * display-only signal; not authoritative for "how far along". - * @example 12 - */ - volumesOwned?: number | null; - }[]; + data: components["schemas"]["SeriesExternalIndexDto"][]; /** @description HATEOAS navigation links */ links: components["schemas"]["PaginationLinks"]; /** @@ -15518,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"]; /** @@ -15590,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"]; /** @@ -15645,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"]; /** @@ -18586,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 @@ -24029,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; }; @@ -24462,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; }; @@ -24501,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; }; @@ -24548,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; }; @@ -24591,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; }; @@ -24730,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; }; @@ -25460,6 +24799,43 @@ export interface operations { }; }; }; + 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: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Book not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; replace_book_metadata: { parameters: { query?: never; @@ -27512,8 +26888,13 @@ export interface operations { /** @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; }; @@ -27554,8 +26935,13 @@ export interface operations { /** @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; }; @@ -27596,8 +26982,13 @@ export interface operations { /** @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; }; @@ -27638,8 +27029,13 @@ export interface operations { /** @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; }; @@ -28230,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; }; @@ -28265,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; @@ -28302,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; @@ -28339,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; @@ -30088,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; }; @@ -30701,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; }; @@ -30730,12 +30162,59 @@ export interface operations { }; }; }; + 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 */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; list_in_progress_series: { parameters: { 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; @@ -30772,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; }; @@ -30895,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; @@ -30929,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; @@ -31319,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; @@ -31759,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; }; @@ -32496,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; From fac13a01eeb10a607e5677f86147ad972d017239 Mon Sep 17 00:00:00 2001 From: Sylvain Cau Date: Fri, 21 Aug 2026 13:17:28 -0700 Subject: [PATCH 8/9] test(web): give interaction tests a budget the runner can actually meet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frontend suite failed a different test on every full run while every one of them passed in isolation: three consecutive runs failed BulkMetadataEditModal, then AddLibraryModal twice, then InstallNudgeModal and MediaCard. That signature is a budget problem, not broken tests. Vitest's 5s default is sized for fast unit tests. 74 of the 221 files drive the UI through userEvent, which awaits a React render per keystroke, and the runner gives each of the machine's cores its own jsdom environment. Under that contention the heavier interaction tests genuinely exceed 5s — the worst offender types thirteen characters, walks a file-browser flow with three async lookups, switches tabs and sets four selects. Raises testTimeout and hookTimeout to 20s. Individual findBy* calls keep their own shorter timeouts, so a missing element still fails fast; what this costs is that a truly hung test now takes 20s to report rather than 5s. Three consecutive full runs are green at 3627/3627, against three consecutive red ones before. --- web/vitest.config.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/vitest.config.ts b/web/vitest.config.ts index 5d597bcb9..eba54463b 100644 --- a/web/vitest.config.ts +++ b/web/vitest.config.ts @@ -9,6 +9,18 @@ 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, coverage: { provider: "v8", reporter: ["text", "json", "html"], From db0d6da5d57ab688254b9f8e3b221a977883690c Mon Sep 17 00:00:00 2001 From: Sylvain Cau Date: Fri, 21 Aug 2026 17:34:51 -0700 Subject: [PATCH 9/9] fix(web): serve the new full routes from the mocks and drop the dead full plumbing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BookDetail and SeriesDetail were migrated onto GET /books/{id}/full and GET /series/{id}/full, but the MSW handlers still only knew the deprecated `?full=true` form, so `make frontend-mock` would have 404'd both pages. Nothing caught it: MSW is wired through setupWorker, which only runs in a browser, so vitest never loads the handlers — and neither page has a test. Adds handlers for all three new routes and a coverage test that asserts they exist. The test also pins that the literal /series/full is declared before the /series/:id pattern, since MSW resolves in array order and would otherwise read "full" as a series id — the same ordering constraint the real router has. Writing that assertion caught a flaw in the assertion rather than the code: an early `indexOf` matched the PATCH handler for /series/:id and reported an order violation that cannot exist, because MSW matches on method and path together. It now compares within GET handlers only. Also removes the `full` plumbing from six series API wrappers — getByLibrary, getInProgress, getRecentlyAdded, getRecentlyUpdated, getBooks and search. Each accepted `full?: T` and returned a conditional type, the TypeScript workaround for a response shape the document could not express, and no caller passed it. They collapse to their plain return types, so series.ts no longer references a parameter that goes away in 3.0. Caps the vitest worker pool at 50% of cores in the same change. Raising the per-test timeout earlier was not enough: the suite kept failing one test per run — InstallNudgeModal, TemplateSelector, MediaCard, AddLibraryModal — never the same one twice, and each passed in isolation. Vitest defaults to one worker per logical core, and each runs a full jsdom + React + Mantine pipeline, so twelve of them starve each other. Failures landing on whichever test lost the race is the signature of contention rather than a broken test. The cap halves aggregate test time, 369-403s against 660-681s, because the tests get real CPU instead of fighting for it; wall clock costs about 13s. Seven consecutive green runs, against a rate that had been failing roughly four in ten. --- web/src/api/series.ts | 63 +++++++++---------------- web/src/mocks/handlers/books.ts | 11 +++++ web/src/mocks/handlers/coverage.test.ts | 46 ++++++++++++++++++ web/src/mocks/handlers/series.ts | 46 ++++++++++++++++++ web/vitest.config.ts | 10 ++++ 5 files changed, 134 insertions(+), 42 deletions(-) create mode 100644 web/src/mocks/handlers/coverage.test.ts diff --git a/web/src/api/series.ts b/web/src/api/series.ts index ed8875f9d..6d20dc9eb 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,15 +51,11 @@ 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; }, @@ -84,20 +77,15 @@ export const seriesApi = { }, // 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; }, @@ -182,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; }, @@ -248,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; @@ -256,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; @@ -284,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 = { @@ -295,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 8b76d66f2..0ee5a3d48 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 000000000..db298a7b6 --- /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 94015de61..52e727f44 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/vitest.config.ts b/web/vitest.config.ts index eba54463b..b34516632 100644 --- a/web/vitest.config.ts +++ b/web/vitest.config.ts @@ -21,6 +21,16 @@ export default defineConfig({ // 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"],